[feat] add macOS and mobile resolution controls with disconnect restore

This commit is contained in:
dijunkun
2026-11-11 11:02:41 +08:00
parent a3159b198e
commit 34b59de15d
50 changed files with 1086 additions and 36 deletions
@@ -17,6 +17,7 @@ final class BackgroundSession implements NativeSession.Listener {
String identity = "", platform = "", clipboard = "", fileStatus = "", endReason = "";
JSONArray displays = new JSONArray();
RemoteVideoSettings videoSettings;
RemoteDisplayResolutions displayResolutions=new RemoteDisplayResolutions();
int signalState, selectedDisplay, width, height;
boolean muted, remoteServiceAvailable, background, ended;
boolean autoLandscape;
@@ -55,6 +56,7 @@ final class BackgroundSession implements NativeSession.Listener {
}
@Override public void data(JSONObject message) {
if (view != null) { view.data(message); return; }
displayResolutions.receive(message);
if (message.optInt("type", -1) == 12 && videoSettings != null) videoSettings.receive(message);
if (message.optInt("type", -1) == 5) {
JSONObject service = message.optJSONObject("service_status");
@@ -80,6 +82,7 @@ final class BackgroundSession implements NativeSession.Listener {
if (view != null) view.fileTransfer(name, progress, sending, received);
}
@Override public void controlFeedback(String text) { if (view != null) view.controlFeedback(text); }
@Override public void displayResolutionFailed(long requestId) { if(view!=null)view.displayResolutionFailed(requestId);else displayResolutions.expire(requestId); }
@Override public void privacyChanged() { if (view != null) view.privacyChanged(); }
@Override public void presence(JSONObject message) { if (view != null) view.presence(message); }
@Override public void announcement(JSONObject message) { if (view != null) view.announcement(message); }
@@ -311,6 +311,9 @@ final class LocalizationKeys {
IDS.put(",已保存密码", R.string.text_ba9a792d063a8c86);
IDS.put(",接收 ", R.string.text_f4c1f9d7896760a7);
IDS.put(",点击重试", R.string.text_42237b5ec70c26f3);
IDS.put("被控端分辨率", R.string.text_59c9dca5568a6cd3);
IDS.put("设备不支持", R.string.text_36e0073276c0e39f);
IDS.put("等待电脑端确认…", R.string.text_7b3440ad0f81b811);
}
private LocalizationKeys() {}
}
@@ -93,6 +93,8 @@ public final class MainActivity extends Activity implements NativeSession.Listen
private RemoteVideoSettings videoSettings=new RemoteVideoSettings(1);
private final android.os.Handler mainHandler=new android.os.Handler(android.os.Looper.getMainLooper());
private Runnable videoSettingsTimeout;
private RemoteDisplayResolutions displayResolutions=new RemoteDisplayResolutions();
private Runnable displayResolutionTimeout;
private final Set<Integer> pressed=new HashSet<>();
private Runnable backAction;
@@ -349,8 +351,9 @@ public final class MainActivity extends Activity implements NativeSession.Listen
public void chord(int code,int[] modifiers){if(session==null)return;for(int modifier:modifiers)session.key(modifier,true);tapKey(code);for(int i=modifiers.length-1;i>=0;i--)session.key(modifiers[i],false);}public void shortcut(int code){MainActivity.this.shortcut(code);}public void text(String value){sendText(value);}
public void mute(){muted=!muted;if(session!=null)session.control(2,muted?0:1);controls.setMuted(muted);}
public void mouse(){boolean relative=!preferences.getBoolean("relativeMouse",true);preferences.edit().putBoolean("relativeMouse",relative).apply();video.setRelative(relative);controls.setRelative(relative);}
public void displayResolution(RemoteDisplayResolutions.Mode mode){updateDisplayResolution(mode);}
public void videoSettings(int field,int value){RemoteVideoSettings.Values old=MainActivity.this.videoSettings.selection;updateVideoSettings(new RemoteVideoSettings.Values(field==0?value:old.quality,field==1?value:old.frameRate,field==2?value:old.preference));}
});controls.setHostPlatform(hostPlatform);controls.setRelative(preferences.getBoolean("relativeMouse",true));controls.setMuted(muted);controls.videoSettings(videoSettings);root.addView(controls,new LinearLayout.LayoutParams(-1,-1));getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
});controls.setHostPlatform(hostPlatform);controls.setRelative(preferences.getBoolean("relativeMouse",true));controls.setMuted(muted);controls.videoSettings(videoSettings);controls.displayResolutions(displayResolutions,selectedDisplay);root.addView(controls,new LinearLayout.LayoutParams(-1,-1));getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
controls.attachMouseBar(canvas,new VirtualMouseBar(ui,video.mouseInput));
privacyChanged();
video.remoteFocusChanged=()->{if(controls!=null)controls.resetKeyboardContext();};
@@ -506,7 +509,7 @@ public final class MainActivity extends Activity implements NativeSession.Listen
private void tapKey(int code){if(session!=null){session.key(code,true);session.key(code,false);}}
private void shortcut(int code){if(session!=null){int modifier=hostPlatform.equals("macos")?0x5B:0x11;session.key(modifier,true);tapKey(code);session.key(modifier,false);}}
private void selectDisplay(){
if(!ready)return;int count=Math.min(displays.length(),8);if(count==0){toast(L10n.tr("等待远端屏幕信息"));return;}String[] labels=new String[count];for(int i=0;i<count;i++){JSONObject display=displays.optJSONObject(i);labels[i]=(i+1)+" · "+(display==null?L10n.tr("显示器"):display.optString("name",L10n.tr("显示器")));}new AlertDialog.Builder(this).setTitle(L10n.tr("显示器")).setItems(labels,(d,w)->{if(session!=null){if(w!=selectedDisplay&&video!=null)video.resetViewport();selectedDisplay=w;session.control(4,w);if(controls!=null)controls.resetVideoStatistics();}}).show();
if(!ready)return;int count=Math.min(displays.length(),8);if(count==0){toast(L10n.tr("等待远端屏幕信息"));return;}String[] labels=new String[count];for(int i=0;i<count;i++){JSONObject display=displays.optJSONObject(i);labels[i]=(i+1)+" · "+(display==null?L10n.tr("显示器"):display.optString("name",L10n.tr("显示器")));}new AlertDialog.Builder(this).setTitle(L10n.tr("显示器")).setItems(labels,(d,w)->{if(session!=null){if(w!=selectedDisplay&&video!=null)video.resetViewport();selectedDisplay=w;session.control(4,w);if(controls!=null)controls.displayResolutions(displayResolutions,selectedDisplay);if(controls!=null)controls.resetVideoStatistics();}}).show();
}
private void secureAttention(){
if(!ready||session==null||!hostPlatform.equals("windows"))return;
@@ -645,7 +648,22 @@ public final class MainActivity extends Activity implements NativeSession.Listen
remoteVersionCheck.reset();
if(remoteUpdateDialog!=null){remoteUpdateDialog.dismiss();remoteUpdateDialog=null;}
}
private void cancelDisplayResolutionTimeout(){if(displayResolutionTimeout!=null)mainHandler.removeCallbacks(displayResolutionTimeout);displayResolutionTimeout=null;}
private void updateDisplayResolution(RemoteDisplayResolutions.Mode mode){
if(!ready||session==null||!hasNetworkConsent())return;
RemoteDisplayResolutions.Request request=displayResolutions.begin(selectedDisplay,mode);if(request==null)return;
cancelDisplayResolutionTimeout();session.displayResolution(request);
if(controls!=null)controls.displayResolutions(displayResolutions,selectedDisplay);
displayResolutionTimeout=()->{displayResolutions.expire(request.id);if(controls!=null)controls.displayResolutions(displayResolutions,selectedDisplay);};
mainHandler.postDelayed(displayResolutionTimeout,10000);
}
@Override public void displayResolutionFailed(long requestId){
displayResolutions.expire(requestId);
if(!displayResolutions.pending)cancelDisplayResolutionTimeout();
if(controls!=null)controls.displayResolutions(displayResolutions,selectedDisplay);
}
private void resetVideoSettings(){
cancelDisplayResolutionTimeout();displayResolutions=new RemoteDisplayResolutions();
cancelVideoSettingsTimeout();
android.util.DisplayMetrics metrics=getResources().getDisplayMetrics();
videoSettings=new RemoteVideoSettings(preferences.getInt("videoPreference",1),Math.max(metrics.widthPixels,metrics.heightPixels));
@@ -658,6 +676,11 @@ public final class MainActivity extends Activity implements NativeSession.Listen
}
@Override public void data(JSONObject message){
if(session==null)return;
if(message.optInt("type",-1)==3||message.optInt("type",-1)==16){
displayResolutions.receive(message);if(!displayResolutions.pending)cancelDisplayResolutionTimeout();
if(controls!=null)controls.displayResolutions(displayResolutions,selectedDisplay);
if(message.optInt("type",-1)==16)return;
}
if(message.optInt("type",-1)==5){JSONObject service=message.optJSONObject("service_status");remoteServiceAvailable=service!=null&&service.optBoolean("available",false);return;}
if(message.optInt("type",-1)==12){if(!ready)return;videoSettings.receive(message);if(!videoSettings.pending)cancelVideoSettingsTimeout();if(controls!=null)controls.videoSettings(videoSettings);return;}
JSONObject info=message.optJSONObject("host_info");
@@ -746,7 +769,7 @@ public final class MainActivity extends Activity implements NativeSession.Listen
BackgroundSession retained=backgroundSession;if(retained==null)return;
retained.identity=localIdentity;retained.signalState=signalState;retained.platform=hostPlatform;retained.displays=displays;
retained.muted=muted;retained.selectedDisplay=selectedDisplay;retained.remoteServiceAvailable=remoteServiceAvailable;
retained.clipboard=remoteClipboard;retained.videoSettings=videoSettings;retained.receivedFile=receivedFile;
retained.clipboard=remoteClipboard;retained.videoSettings=videoSettings;retained.displayResolutions=displayResolutions;retained.receivedFile=receivedFile;
retained.width=videoWidth;retained.height=videoHeight;
retained.autoLandscape=remoteOrientation.autoLandscape();
}
@@ -783,10 +806,10 @@ public final class MainActivity extends Activity implements NativeSession.Listen
connectedRemote=retained.remote;localIdentity=retained.identity;signalState=retained.signalState;signalError="";
hostPlatform=retained.platform;displays=retained.displays;muted=retained.muted;selectedDisplay=retained.selectedDisplay;
remoteServiceAvailable=retained.remoteServiceAvailable;remoteClipboard=retained.clipboard;receivedFile=retained.receivedFile;
videoSettings=retained.videoSettings;ready=true;retained.attach(this);
videoSettings=retained.videoSettings;displayResolutions=retained.displayResolutions;ready=true;retained.attach(this);
if(rebuild)sessionScreen(retained.autoLandscape);
if(retained.width>0&&retained.height>0)videoSize(retained.width,retained.height);
if(controls!=null){controls.setHostPlatform(hostPlatform);controls.setMuted(muted);controls.videoSettings(videoSettings);controls.fileStatus(retained.fileStatus,receivedFile!=null);}
if(controls!=null){controls.setHostPlatform(hostPlatform);controls.setMuted(muted);controls.videoSettings(videoSettings);controls.displayResolutions(displayResolutions,selectedDisplay);controls.fileStatus(retained.fileStatus,receivedFile!=null);}
retained.setBackground(false);signaling(signalState,localIdentity);
return true;
}
@@ -844,11 +867,12 @@ public final class MainActivity extends Activity implements NativeSession.Listen
if(!isFinishing()&&!enteredPictureInPicture&&ready&&(backgroundSession!=null||documentSession==null)&&prepareBackgroundSession(false)){
captureBackgroundSession();backgroundSession.detach(this);backgroundSession.setBackground(true);
mainHandler.removeCallbacks(presenceTick);recentPresence.setConnected(false,android.os.SystemClock.elapsedRealtime());announcementInbox.setConnected(false);
cancelDisplayResolutionTimeout();if(displayResolutions.pending)displayResolutions.expire(displayResolutions.requested.id);
cancelVideoSettingsTimeout();if(videoSettings.pending)videoSettings.expire(videoSettings.requestId);
resetRemoteVersionCheck();if(disconnectDialog!=null)disconnectDialog.dismiss();
}else if(documentSession==null||documentSession!=session||isFinishing()){if(session!=null)end(isFinishing()?"":L10n.tr("远程窗口已关闭,或后台保活未能启动,会话已结束"));stopSignaling();}
super.onStop();}
@Override protected void onDestroy(){dismissPrivacyUnlockDialog();mainHandler.removeCallbacks(documentTimeout);documentSession=null;documentToSave=null;appUpdates.setEnabled(false);pageHost.finish();resetRemoteVersionCheck();cancelVideoSettingsTimeout();previews.removeListener(previewChanged);if(privacyDialog!=null)privacyDialog.dismiss();if(previewDialog!=null)previewDialog.dismiss();announcementInbox.close();if(disconnectDialog!=null)disconnectDialog.dismiss();if(passwordDialog!=null)passwordDialog.dismiss();if(progressDialog!=null)progressDialog.dismiss();
@Override protected void onDestroy(){dismissPrivacyUnlockDialog();mainHandler.removeCallbacks(documentTimeout);documentSession=null;documentToSave=null;appUpdates.setEnabled(false);pageHost.finish();resetRemoteVersionCheck();cancelVideoSettingsTimeout();cancelDisplayResolutionTimeout();previews.removeListener(previewChanged);if(privacyDialog!=null)privacyDialog.dismiss();if(previewDialog!=null)previewDialog.dismiss();announcementInbox.close();if(disconnectDialog!=null)disconnectDialog.dismiss();if(passwordDialog!=null)passwordDialog.dismiss();if(progressDialog!=null)progressDialog.dismiss();
if(connectionDialog!=null)connectionDialog.dismiss();
boolean retained=!isFinishing()&&backgroundSession!=null&&!backgroundSession.ended&&RemoteSessionService.current()==backgroundSession;
if(backgroundSession!=null)backgroundSession.detach(this);
@@ -31,6 +31,7 @@ final class NativeSession implements AutoCloseable {
default void fileTransfer(String name,double progress,boolean sending,File received) { }
default void controlFeedback(String message) { }
default void privacyChanged() { }
default void displayResolutionFailed(long requestId) { }
default void signaling(int state, String deviceId) { }
default void presence(JSONObject message) { }
default void announcement(JSONObject message) { }
@@ -243,6 +244,9 @@ final class NativeSession implements AutoCloseable {
if(!settings.valid()||requestId<0||requestId>0xffffffffL)return;
execute(()->{if(connected&&!nVideoSettings(handle,settings.quality,settings.frameRate,settings.preference,requestId))ui(()->listener.data(settings.status(requestId,false)));});
}
void displayResolution(RemoteDisplayResolutions.Request request){
execute(()->{if(!connected||!nDisplayResolution(handle,request.displayId,request.displayKey,request.mode.width,request.mode.height,request.id))ui(()->listener.displayResolutionFailed(request.id));});
}
void announcementRequest(JSONObject request,String requestId){
byte[] bytes=request.toString().getBytes(StandardCharsets.UTF_8);
execute(()->{if(!signalReady||identity.isEmpty()||!nAnnouncementRequest(handle,bytes))ui(()->listener.announcementFailed(requestId));});
@@ -344,6 +348,7 @@ final class NativeSession implements AutoCloseable {
private static native void nKey(long handle, int code, boolean down);
private static native void nText(long handle,byte[] text);
private static native void nControl(long handle, int type, int value);
private static native boolean nDisplayResolution(long handle,int displayId,String displayKey,int width,int height,long requestId);
private static native boolean nVideoSettings(long handle,int quality,int frameRate,int preference,long requestId);
private static native boolean nAnnouncementRequest(long handle,byte[] data);
private static native boolean nPresenceRequest(long handle,byte[] data);
@@ -0,0 +1,90 @@
package cn.crossdesk.mobile;
import org.json.JSONArray;
import org.json.JSONObject;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
/** Host-confirmed desktop modes. Requests are scoped to the native display identity. */
final class RemoteDisplayResolutions {
static final class Mode {
final int width, height;
Mode(int width,int height){this.width=width;this.height=height;}
boolean same(Mode other){return other!=null&&width==other.width&&height==other.height;}
String label(){return width+" × "+height;}
}
static final class Display {
final int id;
final String key;
final Mode current;
final List<Mode> modes=new ArrayList<>();
Display(JSONObject value)throws Exception{
id=(int)integer(value,"display_id",0,31);
Object identity=value.get("display_key");
if(!(identity instanceof String))throw new IllegalArgumentException();
key=(String)identity;
if(key.isEmpty()||key.getBytes(StandardCharsets.UTF_8).length>1024||key.indexOf('\0')>=0)throw new IllegalArgumentException();
current=mode(value);
JSONArray options=value.getJSONArray("modes");
if(options.length()>256)throw new IllegalArgumentException();
for(int i=0;i<options.length();i++){
Mode item=mode(options.getJSONObject(i));
if(!supports(item))modes.add(item);
}
}
boolean supports(Mode value){for(Mode item:modes)if(item.same(value))return true;return false;}
}
static final class Request {
final int displayId;
final String displayKey;
final Mode mode;
final long id;
Request(Display display,Mode mode,long id){this.displayId=display.id;this.displayKey=display.key;this.mode=mode;this.id=id;}
boolean matches(JSONObject value)throws Exception{
return integer(value,"request_id",1,0xffffffffL)==id&&integer(value,"display_id",0,31)==displayId
&&displayKey.equals(value.get("display_key"))&&mode.same(mode(value));
}
}
private List<Display> displays=new ArrayList<>();
Request requested;
boolean pending,failed;
Display display(int id){for(Display display:displays)if(display.id==id)return display;return null;}
Request begin(int displayId,Mode mode){
Display display=display(displayId);
if(pending||display==null||!display.supports(mode)||display.current.same(mode))return null;
long next=requested==null?1:(requested.id+1)&0xffffffffL;
requested=new Request(display,mode,next==0?1:next);pending=true;failed=false;return requested;
}
void expire(long id){if(pending&&requested!=null&&requested.id==id){pending=false;failed=true;}}
void receive(JSONObject message){
try{
int type=message.getInt("type");JSONArray values;JSONObject response=null;
if(type==3){JSONObject host=message.getJSONObject("host_info");values=host.has("display_resolutions")?host.getJSONArray("display_resolutions"):new JSONArray();}
else if(type==16){response=message.getJSONObject("display_resolution");if(requested==null||!requested.matches(response)||!(response.get("accepted") instanceof Boolean))return;values=message.getJSONArray("display_resolutions");}
else return;
if(values.length()>32)return;
List<Display> actual=new ArrayList<>();
for(int i=0;i<values.length();i++){
Display display=new Display(values.getJSONObject(i));
for(Display previous:actual)if(previous.id==display.id)return;
actual.add(display);
}
displays=actual;
if(response!=null){
Display display=display(requested.displayId);pending=false;
failed=!response.getBoolean("accepted")||display==null||!display.key.equals(requested.displayKey)||!display.current.same(requested.mode);
}else if(pending){
Display display=display(requested.displayId);
if(display==null||!display.key.equals(requested.displayKey)||!display.supports(requested.mode))expire(requested.id);
}
}catch(Exception ignored){ /* Ignore malformed or unrelated control messages. */ }
}
String feedback(){return pending?L10n.tr("等待电脑端确认…"):failed?L10n.tr("调整失败,请重试。"):"";}
private static Mode mode(JSONObject value)throws Exception{return new Mode((int)integer(value,"width",1,32768),(int)integer(value,"height",1,32768));}
private static long integer(JSONObject value,String key,long min,long max)throws Exception{
Object raw=value.get(key);
if(!(raw instanceof Integer)&&!(raw instanceof Long))throw new IllegalArgumentException();
long number=((Number)raw).longValue();if(number<min||number>max)throw new IllegalArgumentException();return number;
}
}
@@ -14,6 +14,7 @@ final class SessionControls extends FrameLayout {
void chord(int code,int[] modifiers); void shortcut(int code); void text(String value); void mute(); void mouse();
default boolean textEdit(int deletions,String value){return false;}
void videoSettings(int field,int value);
default void displayResolution(RemoteDisplayResolutions.Mode mode) { }
void sendFile(); void saveFile(); void secureAttention();
default void togglePrivacy() { }
}
@@ -36,7 +37,10 @@ final class SessionControls extends FrameLayout {
int height=Math.max(0,getRootView().getHeight()-visible.bottom);
keyboardInsets(height,height>ui.dp(80),0);
}
private TextView videoFeedback;
private TextView videoFeedback,resolutionButton,resolutionFeedback;
private RemoteDisplayResolutions resolutions=new RemoteDisplayResolutions();
private int resolutionDisplay;
private android.app.AlertDialog resolutionDialog;
private RemotePrivacyScreen privacy=new RemotePrivacyScreen();
private View privacyButton;
private MobileUi.Icon privacyIcon;
@@ -111,13 +115,14 @@ final class SessionControls extends FrameLayout {
void resetVideoStatistics(){statistics(new RemoteNetworkStatistics.Snapshot(network.report,0,0,0,Double.NaN,network.rtt,network.remoteEncoder,network.localDecoder));}
void statistics(RemoteNetworkStatistics.Snapshot value){network=value;renderNetwork();}
void videoSettings(RemoteVideoSettings value){settings=value;renderVideoSettings();}
void displayResolutions(RemoteDisplayResolutions value,int display){resolutions=value;resolutionDisplay=display;renderResolutions();}
void privacyScreen(RemotePrivacyScreen value){privacy=value;renderPrivacy();}
void fileStatus(String status,boolean received){transferStatus=status;hasReceivedFile=received;renderFileStatus();}
private void renderFileStatus(){
if(transferLabel!=null){transferLabel.setText(transferStatus);transferLabel.setVisibility(transferStatus.isEmpty()?GONE:VISIBLE);}
if(saveFileButton!=null)saveFileButton.setVisibility(hasReceivedFile?VISIBLE:GONE);
}
private void closeMenu(){if(menu!=null){removeView(menu);menu=null;menuScroll=null;}if(shield!=null){removeView(shield);shield=null;}videoFeedback=transferLabel=saveFileButton=null;privacyFeedback=privacyCaption=null;privacyButton=null;privacyIcon=null;trafficCells=null;trafficRows=null;networkDetails=null;encryptionLock=encryptionOpen=null;settingRows.clear();orb.setContentDescription(L10n.tr("展开远程控制菜单"));positionMouse();}
private void closeMenu(){if(resolutionDialog!=null){resolutionDialog.dismiss();resolutionDialog=null;}resolutionButton=resolutionFeedback=null;if(menu!=null){removeView(menu);menu=null;menuScroll=null;}if(shield!=null){removeView(shield);shield=null;}videoFeedback=transferLabel=saveFileButton=null;privacyFeedback=privacyCaption=null;privacyButton=null;privacyIcon=null;trafficCells=null;trafficRows=null;networkDetails=null;encryptionLock=encryptionOpen=null;settingRows.clear();orb.setContentDescription(L10n.tr("展开远程控制菜单"));positionMouse();}
private void openMenu(String page){
cancelMouseInput();
closeMenu();menuPage=page;shield=new View(getContext());shield.setOnClickListener(v->closeMenu());addView(shield,new LayoutParams(-1,-1));
@@ -153,6 +158,9 @@ final class SessionControls extends FrameLayout {
}else if(page.equals("network")){
networkPage(body);
}else{
TextView resolutionTitle=ui.text(L10n.tr("被控端分辨率"),12,false);resolutionTitle.setTextColor(SECONDARY);body.addView(resolutionTitle);ui.gap(body,5);
resolutionButton=ui.action("",INK,this::chooseResolution);resolutionButton.setTextSize(13);resolutionButton.setBackground(ui.background(0xFFF0F0F2,7));body.addView(resolutionButton,ui.size(-1,34));
resolutionFeedback=ui.text("",12,false);resolutionFeedback.setTextColor(SECONDARY);resolutionFeedback.setAccessibilityLiveRegion(ACCESSIBILITY_LIVE_REGION_POLITE);body.addView(resolutionFeedback);ui.gap(body,12);renderResolutions();
settingRows.add(new SettingRow(body,0,L10n.tr("画面质量"),new String[]{L10n.tr("低"),L10n.tr("中"),L10n.tr("高")},new int[]{0,1,2}));ui.gap(body,12);
settingRows.add(new SettingRow(body,1,L10n.tr("画面采集帧率"),new String[]{"30 fps","60 fps"},new int[]{30,60}));ui.gap(body,12);
settingRows.add(new SettingRow(body,2,L10n.tr("画面偏好"),RemoteVideoSettings.preferences(),new int[]{0,1,2}));renderVideoSettings();
@@ -192,6 +200,26 @@ final class SessionControls extends FrameLayout {
}
void render(){group.setSelection(selectionIndex());group.setOptionsEnabled(settings.supported);}
}
private void renderResolutions(){
if(resolutionButton==null)return;
RemoteDisplayResolutions.Display display=resolutions.display(resolutionDisplay);
boolean enabled=display!=null&&!display.modes.isEmpty()&&!resolutions.pending;
resolutionButton.setText(display==null?L10n.tr("设备不支持"):display.current.label()+" ▾");
resolutionButton.setEnabled(enabled);resolutionButton.setAlpha(enabled?1f:.45f);
String feedback=resolutions.feedback();resolutionFeedback.setText(feedback);resolutionFeedback.setVisibility(feedback.isEmpty()?GONE:VISIBLE);
}
private void chooseResolution(){
RemoteDisplayResolutions.Display display=resolutions.display(resolutionDisplay);
if(display==null||resolutions.pending||display.modes.isEmpty())return;
String[] labels=new String[display.modes.size()];int selected=-1;
for(int i=0;i<labels.length;i++){labels[i]=display.modes.get(i).label();if(display.current.same(display.modes.get(i)))selected=i;}
resolutionDialog=new android.app.AlertDialog.Builder(getContext()).setTitle(L10n.tr("被控端分辨率"))
.setSingleChoiceItems(labels,selected,(dialog,index)->{
dialog.dismiss();resolutionDialog=null;
RemoteDisplayResolutions.Display current=resolutions.display(resolutionDisplay);
if(current!=null&&current.id==display.id&&current.key.equals(display.key))actions.displayResolution(display.modes.get(index));
}).setNegativeButton(L10n.tr("取消"),null).show();
}
private void renderVideoSettings(){
if(videoFeedback!=null){String feedback=settings.feedback();videoFeedback.setText(feedback);videoFeedback.setVisibility(feedback.isEmpty()?GONE:VISIBLE);}
for(SettingRow row:settingRows)row.render();
@@ -305,4 +305,7 @@
<string name="text_ba9a792d063a8c86" formatted="false">",已保存密码"</string>
<string name="text_f4c1f9d7896760a7" formatted="false">",接收 "</string>
<string name="text_42237b5ec70c26f3" formatted="false">",点击重试"</string>
<string name="text_59c9dca5568a6cd3" formatted="false">"被控端分辨率"</string>
<string name="text_36e0073276c0e39f" formatted="false">"设备不支持"</string>
<string name="text_7b3440ad0f81b811" formatted="false">"等待电脑端确认…"</string>
</resources>
@@ -305,4 +305,7 @@
<string name="text_ba9a792d063a8c86" formatted="false">",已儲存密碼"</string>
<string name="text_f4c1f9d7896760a7" formatted="false">",接收 "</string>
<string name="text_42237b5ec70c26f3" formatted="false">",點選重試"</string>
<string name="text_59c9dca5568a6cd3" formatted="false">"被控端解析度"</string>
<string name="text_36e0073276c0e39f" formatted="false">"裝置不支援"</string>
<string name="text_7b3440ad0f81b811" formatted="false">"等待電腦端確認…"</string>
</resources>
@@ -305,4 +305,7 @@
<string name="text_ba9a792d063a8c86" formatted="false">", Passwort gespeichert"</string>
<string name="text_f4c1f9d7896760a7" formatted="false">", empfangen "</string>
<string name="text_42237b5ec70c26f3" formatted="false">", klicken Sie auf Wiederholen"</string>
<string name="text_59c9dca5568a6cd3" formatted="false">"Auflösung des entfernten Bildschirms"</string>
<string name="text_36e0073276c0e39f" formatted="false">"Nicht unterstützt"</string>
<string name="text_7b3440ad0f81b811" formatted="false">"Warten auf Bestätigung vom Computer..."</string>
</resources>
@@ -305,4 +305,7 @@
<string name="text_ba9a792d063a8c86" formatted="false">", contraseña guardada"</string>
<string name="text_f4c1f9d7896760a7" formatted="false">", recibir "</string>
<string name="text_42237b5ec70c26f3" formatted="false">", haga clic en Reintentar"</string>
<string name="text_59c9dca5568a6cd3" formatted="false">"Resolución de la pantalla remota"</string>
<string name="text_36e0073276c0e39f" formatted="false">"No compatible"</string>
<string name="text_7b3440ad0f81b811" formatted="false">"Esperando confirmación del ordenador..."</string>
</resources>
@@ -305,4 +305,7 @@
<string name="text_ba9a792d063a8c86" formatted="false">", mot de passe enregistré"</string>
<string name="text_f4c1f9d7896760a7" formatted="false">", recevoir "</string>
<string name="text_42237b5ec70c26f3" formatted="false">", cliquez sur Réessayer"</string>
<string name="text_59c9dca5568a6cd3" formatted="false">"Résolution de l’écran distant"</string>
<string name="text_36e0073276c0e39f" formatted="false">"Non pris en charge"</string>
<string name="text_7b3440ad0f81b811" formatted="false">"En attente de confirmation de l\'ordinateur..."</string>
</resources>
@@ -305,4 +305,7 @@
<string name="text_ba9a792d063a8c86" formatted="false">"、パスワードが保存されました"</string>
<string name="text_f4c1f9d7896760a7" formatted="false">"、受け取ります "</string>
<string name="text_42237b5ec70c26f3" formatted="false">"、「再試行」をクリックします"</string>
<string name="text_59c9dca5568a6cd3" formatted="false">"接続先の画面解像度"</string>
<string name="text_36e0073276c0e39f" formatted="false">"サポートされていません"</string>
<string name="text_7b3440ad0f81b811" formatted="false">"コンピューターからの確認を待っています..."</string>
</resources>
@@ -305,4 +305,7 @@
<string name="text_ba9a792d063a8c86" formatted="false">", 비밀번호가 저장되었습니다"</string>
<string name="text_f4c1f9d7896760a7" formatted="false">", 수신 "</string>
<string name="text_42237b5ec70c26f3" formatted="false">", 재시도를 클릭하세요."</string>
<string name="text_59c9dca5568a6cd3" formatted="false">"원격 화면 해상도"</string>
<string name="text_36e0073276c0e39f" formatted="false">"지원되지 않음"</string>
<string name="text_7b3440ad0f81b811" formatted="false">"컴퓨터의 확인을 기다리는 중..."</string>
</resources>
@@ -305,4 +305,7 @@
<string name="text_ba9a792d063a8c86" formatted="false">", senha salva"</string>
<string name="text_f4c1f9d7896760a7" formatted="false">", receba "</string>
<string name="text_42237b5ec70c26f3" formatted="false">", clique em Tentar novamente"</string>
<string name="text_59c9dca5568a6cd3" formatted="false">"Resolução da tela remota"</string>
<string name="text_36e0073276c0e39f" formatted="false">"Não suportado"</string>
<string name="text_7b3440ad0f81b811" formatted="false">"Aguardando confirmação do computador..."</string>
</resources>
@@ -305,4 +305,7 @@
<string name="text_ba9a792d063a8c86" formatted="false">", пароль сохранен"</string>
<string name="text_f4c1f9d7896760a7" formatted="false">", получить "</string>
<string name="text_42237b5ec70c26f3" formatted="false">"нажмите «Повторить»."</string>
<string name="text_59c9dca5568a6cd3" formatted="false">"Разрешение удалённого экрана"</string>
<string name="text_36e0073276c0e39f" formatted="false">"Не поддерживается"</string>
<string name="text_7b3440ad0f81b811" formatted="false">"Ожидание подтверждения от компьютера..."</string>
</resources>
@@ -305,4 +305,7 @@
<string name="text_ba9a792d063a8c86" formatted="false">", password saved"</string>
<string name="text_f4c1f9d7896760a7" formatted="false">", receive "</string>
<string name="text_42237b5ec70c26f3" formatted="false">", click Retry"</string>
<string name="text_59c9dca5568a6cd3" formatted="false">"Remote display resolution"</string>
<string name="text_36e0073276c0e39f" formatted="false">"Unsupported"</string>
<string name="text_7b3440ad0f81b811" formatted="false">"Waiting for confirmation from the computer..."</string>
</resources>
+10
View File
@@ -422,6 +422,16 @@ extern "C" JNIEXPORT jboolean JNICALL JNI_METHOD(nVideoSettings)(
if (data.empty()) return false;
return MiniRtcSendReliableDataFrame(s->controller, data.data(), data.size(), kControlStream) == 0;
}
extern "C" JNIEXPORT jboolean JNICALL JNI_METHOD(nDisplayResolution)(
JNIEnv* env, jclass, jlong handle, jint display_id, jstring display_key,
jint width, jint height, jlong request_id) {
auto* s = From(handle);
if (!s || !s->controller || s->stopping || request_id <= 0 || request_id > UINT32_MAX) return false;
const auto data = mobile_controller::DisplayResolutionMessage(
display_id, String(env, display_key), width, height, static_cast<uint32_t>(request_id));
return !data.empty() && MiniRtcSendReliableDataFrame(
s->controller, data.data(), data.size(), kControlStream) == 0;
}
extern "C" JNIEXPORT jboolean JNICALL JNI_METHOD(nSecureAttention)(JNIEnv*, jclass, jlong handle) {
auto* s = From(handle);
if (!s || !s->controller || s->stopping) return false;