From 852a8a00273c9128efeed0217a8e5d3fcd8bf780 Mon Sep 17 00:00:00 2001 From: Lena Date: Sat, 1 Aug 2026 00:00:00 +0000 Subject: app: harden mirroring lifecycle and state --- .../main/java/invalid/lena/scrcpy/Settings.java | 119 ++++++++++++++++++--- 1 file changed, 105 insertions(+), 14 deletions(-) (limited to 'app/src/main/java/invalid/lena/scrcpy/Settings.java') diff --git a/app/src/main/java/invalid/lena/scrcpy/Settings.java b/app/src/main/java/invalid/lena/scrcpy/Settings.java index c4bb83b..fab6ed1 100644 --- a/app/src/main/java/invalid/lena/scrcpy/Settings.java +++ b/app/src/main/java/invalid/lena/scrcpy/Settings.java @@ -18,16 +18,32 @@ public final class Settings { public static final String MAX_FPS = "max_fps"; // fps; 0 = unlimited public static final String HINT_BACK_SHOWN = "hint_back_shown"; // first-run UI hint public static final String STATUS_BAR = "status_bar"; // show status bar while mirroring + public static final String CLIPBOARD = "clipboard"; // two-way clipboard sync public static final String DEFAULT_VIDEO_CODEC = "h264"; - public static final String DEFAULT_AUDIO_CODEC = "raw"; - public static final int DEFAULT_MAX_SIZE = 0; - public static final int DEFAULT_VIDEO_BIT_RATE = 8_000_000; + public static final String DEFAULT_AUDIO_CODEC = "opus"; + // Defaults sized for the worst link this is meant to work over, not + // the best. The constraint is the target's UPLINK: an uncapped + // stream at 8 Mbit/s is fine on a LAN and unusable over a VPN or a + // home broadband uplink, which is a supported way to reach a target. + // 1080p at 4 Mbit/s looks fine on a phone and fits an ordinary + // uplink; raise both in Settings when both devices are on the LAN. + public static final int DEFAULT_MAX_SIZE = 1080; + public static final int DEFAULT_VIDEO_BIT_RATE = 4_000_000; public static final int DEFAULT_MAX_FPS = 0; - // Hidden while mirroring by default: an always-on bar over the video - // is intrusive. It still auto-shows when not CONNECTED so Reconnect - // stays reachable. Record lives in the bar, so enable this to keep it. - public static final boolean DEFAULT_STATUS_BAR = false; + // Shown by default. It carries Back, Home and Recents, which are the + // only way to reach the target's navigation: a swipe from the + // source's bottom edge is taken by the source's own gesture + // detector and never reaches us. Hiding it is a deliberate choice + // for an unobstructed picture, not the default, because a control + // the user cannot find is a control that does not exist. + public static final boolean DEFAULT_STATUS_BAR = true; + // On by default: it is a headline feature and the target is one the + // user deliberately paired with. It is a setting because that trust + // is not absolute - a compromised target + // can read whatever is copied on this device and write anything it + // likes back - and because there was previously no way to decline. + public static final boolean DEFAULT_CLIPBOARD = true; private Settings() {} @@ -36,58 +52,133 @@ public final class Settings { } public static String videoCodec(Context ctx) { - return prefs(ctx).getString(VIDEO_CODEC, DEFAULT_VIDEO_CODEC); + String value = string(ctx, VIDEO_CODEC, DEFAULT_VIDEO_CODEC); + return "h264".equals(value) || "h265".equals(value) || "av1".equals(value) + ? value : DEFAULT_VIDEO_CODEC; } public static String audioCodec(Context ctx) { - return prefs(ctx).getString(AUDIO_CODEC, DEFAULT_AUDIO_CODEC); + String value = string(ctx, AUDIO_CODEC, DEFAULT_AUDIO_CODEC); + return "opus".equals(value) || "raw".equals(value) + ? value : DEFAULT_AUDIO_CODEC; } public static int maxSize(Context ctx) { - return prefs(ctx).getInt(MAX_SIZE, DEFAULT_MAX_SIZE); + int value = integer(ctx, MAX_SIZE, DEFAULT_MAX_SIZE); + switch (value) { + case 0: case 480: case 720: case 1080: case 1440: case 2160: + return value; + default: + return DEFAULT_MAX_SIZE; + } } public static int videoBitRate(Context ctx) { - return prefs(ctx).getInt(VIDEO_BIT_RATE, DEFAULT_VIDEO_BIT_RATE); + int value = integer(ctx, VIDEO_BIT_RATE, DEFAULT_VIDEO_BIT_RATE); + switch (value) { + case 1_000_000: case 2_000_000: case 4_000_000: + case 8_000_000: case 16_000_000: + return value; + default: + return DEFAULT_VIDEO_BIT_RATE; + } } public static int maxFps(Context ctx) { - return prefs(ctx).getInt(MAX_FPS, DEFAULT_MAX_FPS); + int value = integer(ctx, MAX_FPS, DEFAULT_MAX_FPS); + switch (value) { + case 0: case 30: case 60: case 90: case 120: + return value; + default: + return DEFAULT_MAX_FPS; + } } public static void setVideoCodec(Context ctx, String v) { + if (!"h264".equals(v) && !"h265".equals(v) && !"av1".equals(v)) { + throw new IllegalArgumentException("invalid video codec"); + } prefs(ctx).edit().putString(VIDEO_CODEC, v).apply(); } public static void setAudioCodec(Context ctx, String a) { + if (!"opus".equals(a) && !"raw".equals(a)) { + throw new IllegalArgumentException("invalid audio codec"); + } prefs(ctx).edit().putString(AUDIO_CODEC, a).apply(); } public static void setMaxSize(Context ctx, int v) { + if (v != 0 && v != 480 && v != 720 && v != 1080 && v != 1440 && v != 2160) { + throw new IllegalArgumentException("invalid maximum size"); + } prefs(ctx).edit().putInt(MAX_SIZE, v).apply(); } public static void setVideoBitRate(Context ctx, int v) { + if (v != 1_000_000 && v != 2_000_000 && v != 4_000_000 + && v != 8_000_000 && v != 16_000_000) { + throw new IllegalArgumentException("invalid video bit rate"); + } prefs(ctx).edit().putInt(VIDEO_BIT_RATE, v).apply(); } public static void setMaxFps(Context ctx, int v) { + if (v != 0 && v != 30 && v != 60 && v != 90 && v != 120) { + throw new IllegalArgumentException("invalid maximum frame rate"); + } prefs(ctx).edit().putInt(MAX_FPS, v).apply(); } public static boolean hintBackShown(Context ctx) { - return prefs(ctx).getBoolean(HINT_BACK_SHOWN, false); + return bool(ctx, HINT_BACK_SHOWN, false); } public static void setHintBackShown(Context ctx, boolean v) { prefs(ctx).edit().putBoolean(HINT_BACK_SHOWN, v).apply(); } + public static boolean clipboardSync(Context ctx) { + return bool(ctx, CLIPBOARD, DEFAULT_CLIPBOARD); + } + + public static void setClipboardSync(Context ctx, boolean v) { + prefs(ctx).edit().putBoolean(CLIPBOARD, v).apply(); + } + public static boolean showStatusBar(Context ctx) { - return prefs(ctx).getBoolean(STATUS_BAR, DEFAULT_STATUS_BAR); + return bool(ctx, STATUS_BAR, DEFAULT_STATUS_BAR); } public static void setShowStatusBar(Context ctx, boolean v) { prefs(ctx).edit().putBoolean(STATUS_BAR, v).apply(); } + + private static String string(Context ctx, String key, String fallback) { + try { + String value = prefs(ctx).getString(key, fallback); + return value == null ? fallback : value; + } catch (ClassCastException e) { + Log.w("settings: %s has the wrong type", key); + return fallback; + } + } + + private static int integer(Context ctx, String key, int fallback) { + try { + return prefs(ctx).getInt(key, fallback); + } catch (ClassCastException e) { + Log.w("settings: %s has the wrong type", key); + return fallback; + } + } + + private static boolean bool(Context ctx, String key, boolean fallback) { + try { + return prefs(ctx).getBoolean(key, fallback); + } catch (ClassCastException e) { + Log.w("settings: %s has the wrong type", key); + return fallback; + } + } } -- cgit v1.2.3