[feat] remember remote display resolutions on mobile clients

This commit is contained in:
dijunkun
2026-11-11 15:26:04 +08:00
parent 23f48dc761
commit 25fb2a3d7f
15 changed files with 253 additions and 33 deletions
@@ -56,7 +56,8 @@ final class BackgroundSession implements NativeSession.Listener {
}
@Override public void data(JSONObject message) {
if (view != null) { view.data(message); return; }
displayResolutions.receive(message);
RemoteDisplayResolutions.Request confirmed=displayResolutions.receive(message);
if(confirmed!=null)session.rememberDisplayResolution(confirmed.displayKey,confirmed.mode);
if (message.optInt("type", -1) == 12 && videoSettings != null) videoSettings.receive(message);
if (message.optInt("type", -1) == 5) {
JSONObject service = message.optJSONObject("service_status");
@@ -651,6 +651,10 @@ public final class MainActivity extends Activity implements NativeSession.Listen
private void cancelDisplayResolutionTimeout(){if(displayResolutionTimeout!=null)mainHandler.removeCallbacks(displayResolutionTimeout);displayResolutionTimeout=null;}
private void updateDisplayResolution(RemoteDisplayResolutions.Mode mode){
if(!ready||session==null||!hasNetworkConsent())return;
RemoteDisplayResolutions.Display display=displayResolutions.display(selectedDisplay);
if(!displayResolutions.pending&&display!=null&&display.supports(mode)&&display.current.same(mode)){
session.rememberDisplayResolution(display.key,mode);return;
}
RemoteDisplayResolutions.Request request=displayResolutions.begin(selectedDisplay,mode);if(request==null)return;
cancelDisplayResolutionTimeout();session.displayResolution(request);
if(controls!=null)controls.displayResolutions(displayResolutions,selectedDisplay);
@@ -677,7 +681,9 @@ 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();
RemoteDisplayResolutions.Request confirmed=displayResolutions.receive(message);
if(confirmed!=null&&session!=null)session.rememberDisplayResolution(confirmed.displayKey,confirmed.mode);
if(!displayResolutions.pending)cancelDisplayResolutionTimeout();
if(controls!=null)controls.displayResolutions(displayResolutions,selectedDisplay);
if(message.optInt("type",-1)==16)return;
}
@@ -44,6 +44,7 @@ final class NativeSession implements AutoCloseable {
private final Context context;
private volatile Listener listener;
private final SecretStore secrets;
private final RemoteResolutionStore resolutionStore;
private final String host, scope;
private volatile String remote;
private String password, identity = "";
@@ -80,6 +81,7 @@ final class NativeSession implements AutoCloseable {
this.host = host; this.port = port; this.remote = remote; this.password = password;
scope = host.toLowerCase(java.util.Locale.ROOT) + ":" + port;
secrets = new SecretStore(this.context);
resolutionStore = new RemoteResolutionStore(this.context);
documents = new SessionDocuments(this.context);
}
void start() {
@@ -184,7 +186,8 @@ final class NativeSession implements AutoCloseable {
int status = event.getInt("status");
if (status == 1) {
if (connected) return; // Repeated transport readiness is the same desktop session.
connected = true; main.removeCallbacks(timeout); nReady(handle);
connected = true; main.removeCallbacks(timeout);
nReady(handle,resolutionStore.load(host,port,remote).toString().getBytes(StandardCharsets.UTF_8));
audio = new AudioPlayer();
nControl(handle, 2, 1);
ui(() -> { listener.connected(); if(!closed){statisticsTick.run();fileTick.run();} });
@@ -247,6 +250,9 @@ final class NativeSession implements AutoCloseable {
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 rememberDisplayResolution(String displayKey,RemoteDisplayResolutions.Mode mode){
if(!closed&&connected)resolutionStore.remember(host,port,remote,displayKey,mode);
}
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));});
@@ -343,7 +349,7 @@ final class NativeSession implements AutoCloseable {
static native String nCheckMobileUpdate(String version, byte[] releaseJson);
private static native long nCreate(NativeSession owner, String host, int port, String identity, String logs, String roots);
private static native boolean nConnect(long handle, String identity, String remote, String password);
private static native void nReady(long handle);
private static native void nReady(long handle,byte[] displayResolutions);
private static native void nPointer(long handle, float x, float y, int flag, int wheel);
private static native void nKey(long handle, int code, boolean down);
private static native void nText(long handle,byte[] text);
@@ -57,28 +57,30 @@ final class RemoteDisplayResolutions {
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){
Request 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;
else if(type==16){response=message.getJSONObject("display_resolution");if(requested==null||!requested.matches(response)||!(response.get("accepted") instanceof Boolean))return null;values=message.getJSONArray("display_resolutions");}
else return null;
if(values.length()>32)return null;
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;
for(Display previous:actual)if(previous.id==display.id)return null;
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);
if(!failed)return requested;
}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. */ }
return null;
}
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));}
@@ -0,0 +1,62 @@
package cn.crossdesk.mobile;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
import org.json.JSONArray;
import org.json.JSONObject;
import java.nio.charset.StandardCharsets;
import java.util.HashSet;
import java.util.Locale;
/** Successful selections, scoped to the signaling server, device and native screen. */
final class RemoteResolutionStore {
private final SharedPreferences preferences;
RemoteResolutionStore(Context context) {
preferences=context.getSharedPreferences("remote_display_resolutions",Context.MODE_PRIVATE);
}
private String key(String host,int port,String remote) {
return new JSONArray().put(host.trim().toLowerCase(Locale.ROOT)).put(port).put(remote).toString();
}
private static boolean validKey(String key) {
return !key.isEmpty()&&key.getBytes(StandardCharsets.UTF_8).length<=1024&&key.indexOf('\0')<0;
}
private static int dimension(JSONObject value,String key)throws Exception {
Object raw=value.get(key);
if(!(raw instanceof Integer)&&!(raw instanceof Long))throw new IllegalArgumentException();
long number=((Number)raw).longValue();
if(number<1||number>32768)throw new IllegalArgumentException();
return (int)number;
}
synchronized JSONArray load(String host,int port,String remote) {
if(remote.isEmpty())return new JSONArray();
try {
String text=preferences.getString(key(host,port,remote),"[]");
if(text==null||text.length()>65536)return new JSONArray();
JSONArray values=new JSONArray(text);
if(values.length()>32)return new JSONArray();
HashSet<String> keys=new HashSet<>();
JSONArray result=new JSONArray();
for(int i=0;i<values.length();i++) {
JSONObject value=values.getJSONObject(i);
Object identity=value.get("display_key");
if(!(identity instanceof String)||!validKey((String)identity)||!keys.add((String)identity))return new JSONArray();
result.put(new JSONObject().put("display_key",identity).put("width",dimension(value,"width")).put("height",dimension(value,"height")));
}
return result;
} catch(Exception ignored) { return new JSONArray(); }
}
synchronized void remember(String host,int port,String remote,String displayKey,RemoteDisplayResolutions.Mode mode) {
if(remote.isEmpty()||!validKey(displayKey)||mode==null||mode.width<1||mode.width>32768||mode.height<1||mode.height>32768)return;
try {
JSONArray saved=load(host,port,remote),updated=new JSONArray();
for(int i=0;i<saved.length();i++) {
JSONObject value=saved.getJSONObject(i);
if(!displayKey.equals(value.getString("display_key")))updated.put(value);
}
if(updated.length()>=32)updated.remove(0);
updated.put(new JSONObject().put("display_key",displayKey).put("width",mode.width).put("height",mode.height));
preferences.edit().putString(key(host,port,remote),updated.toString()).apply();
} catch(Exception error) { Log.w("CrossDesk","Could not save remote display resolution",error); }
}
}
+7 -1
View File
@@ -366,10 +366,16 @@ extern "C" JNIEXPORT jboolean JNICALL JNI_METHOD(nConnect)(
const auto join = s->remote + "@" + String(env, password);
return MiniRtcJoinConnection(s->controller, join.c_str()) == 0;
}
extern "C" JNIEXPORT void JNICALL JNI_METHOD(nReady)(JNIEnv*, jclass, jlong handle) {
extern "C" JNIEXPORT void JNICALL JNI_METHOD(nReady)(JNIEnv* env, jclass, jlong handle, jbyteArray preferences) {
auto* s = From(handle); if (!s || !s->controller) return;
auto action = MakeHostInformation("CrossDesk Android", {}, true, "1.6.0", HostPlatform::Android);
action.i.supports_video_codec_status = true;
if (preferences && env->GetArrayLength(preferences) <= 65536) {
std::string bytes(env->GetArrayLength(preferences), '\0');
env->GetByteArrayRegion(preferences, 0, bytes.size(), reinterpret_cast<jbyte*>(bytes.data()));
if (!env->ExceptionCheck())
ParseDisplayResolutionPreferences(bytes, action.preferred_display_resolutions);
}
s->Send(kControlStream, action.to_json()); FreeRemoteAction(action);
}
extern "C" JNIEXPORT void JNICALL JNI_METHOD(nPointer)(