From 3037f9f8fdcc6cdadfe94ba248d10e647578cf53 Mon Sep 17 00:00:00 2001 From: Lena Date: Sat, 1 Aug 2026 00:00:00 +0000 Subject: app: refine physical-device interface Use target-safe capture defaults and remove redundant navigation and frame-rate controls. --- .../java/invalid/lena/scrcpy/ControlMessages.java | 5 -- .../main/java/invalid/lena/scrcpy/Controller.java | 17 ------ app/src/main/java/invalid/lena/scrcpy/Mirror.java | 61 ++++++++++++---------- app/src/main/java/invalid/lena/scrcpy/Server.java | 2 - app/src/main/java/invalid/lena/scrcpy/Session.java | 10 ---- .../main/java/invalid/lena/scrcpy/Settings.java | 35 ------------- .../java/invalid/lena/scrcpy/SettingsActivity.java | 40 ++++++-------- 7 files changed, 48 insertions(+), 122 deletions(-) (limited to 'app/src/main/java/invalid/lena/scrcpy') diff --git a/app/src/main/java/invalid/lena/scrcpy/ControlMessages.java b/app/src/main/java/invalid/lena/scrcpy/ControlMessages.java index 6455a65..2a593d0 100644 --- a/app/src/main/java/invalid/lena/scrcpy/ControlMessages.java +++ b/app/src/main/java/invalid/lena/scrcpy/ControlMessages.java @@ -32,11 +32,6 @@ public final class ControlMessages { public static final int ACTION_DOWN = 0; public static final int ACTION_UP = 1; - // AOSP keycodes, mirrored rather than imported so this stays - // android-free. KeyEvent.KEYCODE_HOME / KEYCODE_APP_SWITCH. - public static final int KEYCODE_HOME = 3; - public static final int KEYCODE_APP_SWITCH = 187; - public static final int TOUCH_MSG_LEN = 32; // 1 + 1 + 8 + 4 + 4 + 2 + 2 + 2 + 4 + 4 public static final int KEY_MSG_LEN = 14; // 1 + 1 + 4 + 4 + 4 diff --git a/app/src/main/java/invalid/lena/scrcpy/Controller.java b/app/src/main/java/invalid/lena/scrcpy/Controller.java index 3cc321c..59dc84d 100644 --- a/app/src/main/java/invalid/lena/scrcpy/Controller.java +++ b/app/src/main/java/invalid/lena/scrcpy/Controller.java @@ -189,23 +189,6 @@ public final class Controller implements ControlStream.InboundSink { sender.accept(ControlMessages.backOrScreenOn(ControlMessages.ACTION_UP)); } - // Home and Recents go as keycodes, not as gestures. On a - // gesture-navigation source a swipe from the bottom edge is claimed - // by the source's own gesture detector and never reaches this app, - // and unlike the side edges that area cannot be released with - // setSystemGestureExclusionRects - the system always keeps it. So - // the target's navigation is unreachable by forwarding touches, on - // any modern source device, and has to be driven explicitly. - public void onHome() { - sendKeycode(ControlMessages.ACTION_DOWN, ControlMessages.KEYCODE_HOME, 0, 0); - sendKeycode(ControlMessages.ACTION_UP, ControlMessages.KEYCODE_HOME, 0, 0); - } - - public void onRecents() { - sendKeycode(ControlMessages.ACTION_DOWN, ControlMessages.KEYCODE_APP_SWITCH, 0, 0); - sendKeycode(ControlMessages.ACTION_UP, ControlMessages.KEYCODE_APP_SWITCH, 0, 0); - } - public void resetVideo() { sender.accept(ControlMessages.resetVideo()); Log.i("controller: reset video"); diff --git a/app/src/main/java/invalid/lena/scrcpy/Mirror.java b/app/src/main/java/invalid/lena/scrcpy/Mirror.java index bc9db34..51e861a 100644 --- a/app/src/main/java/invalid/lena/scrcpy/Mirror.java +++ b/app/src/main/java/invalid/lena/scrcpy/Mirror.java @@ -11,13 +11,13 @@ import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.os.SystemClock; +import android.view.Gravity; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.Surface; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.View; -import android.view.ViewGroup; import android.view.WindowInsets; import android.view.WindowInsetsController; import android.view.WindowManager; @@ -65,6 +65,7 @@ public final class Mirror extends Activity { private State state = State.CONNECTING; private int connectedW, connectedH; private long connectedGeometryVersion; + private int gestureBottomInset; private SurfaceView surfaceView; @@ -114,26 +115,25 @@ public final class Mirror extends Activity { // video surface, so re-fit from here as well. root.addOnLayoutChangeListener( (view, l, t, r, b, ol, ot, or, ob) -> applyLetterbox()); + // Keep the target's bottom edge above the source's mandatory Home + // gesture area. A target gesture can then start on the mirrored + // handle instead of being claimed by the source system. + root.setOnApplyWindowInsetsListener((view, insets) -> { + int bottom = insets.getInsetsIgnoringVisibility( + WindowInsets.Type.mandatorySystemGestures()).bottom; + if (gestureBottomInset != bottom) { + gestureBottomInset = bottom; + applyLetterbox(); + } + return insets; + }); + root.requestApplyInsets(); statusBar = findViewById(R.id.status_bar); statusText = findViewById(R.id.status_text); reconnectBtn = findViewById(R.id.reconnect); insetStatusBar(); reconnectBtn.setOnClickListener(view -> reconnect()); - // Back, Home and Recents as explicit controls. On a - // gesture-navigation source the target's own navigation cannot be - // reached by forwarding touches: the source claims the bottom - // edge swipe for itself and, unlike the side edges, that region - // cannot be released with setSystemGestureExclusionRects. - findViewById(R.id.nav_back).setOnClickListener(view -> { - if (session != null) session.onBack(); - }); - findViewById(R.id.nav_home).setOnClickListener(view -> { - if (session != null) session.onHome(); - }); - findViewById(R.id.nav_recents).setOnClickListener(view -> { - if (session != null) session.onRecents(); - }); updateStatusBar(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { @@ -220,22 +220,29 @@ public final class Mirror extends Activity { View v = surfaceView; if (v == null || root == null || session == null) return; int cw = root.getWidth(), ch = root.getHeight(); + int availableH = ch - gestureBottomInset; int tw = connectedW, th = connectedH; - if (cw <= 0 || ch <= 0 || tw <= 0 || th <= 0) return; + if (cw <= 0 || availableH <= 0 || tw <= 0 || th <= 0) return; - float scale = Math.min(cw / (float) tw, ch / (float) th); + float scale = Math.min(cw / (float) tw, availableH / (float) th); int w = Math.min(cw, Math.max(1, Math.round(tw * scale))); - int h = Math.min(ch, Math.max(1, Math.round(th * scale))); + int h = Math.min(availableH, Math.max(1, Math.round(th * scale))); + int x = (cw - w) / 2; + int y = (availableH - h) / 2; - ViewGroup.LayoutParams lp = v.getLayoutParams(); - if (lp.width != w || lp.height != h) { + FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) v.getLayoutParams(); + if (lp.width != w || lp.height != h || lp.leftMargin != x + || lp.topMargin != y || lp.gravity != (Gravity.TOP | Gravity.START)) { lp.width = w; lp.height = h; + lp.leftMargin = x; + lp.topMargin = y; + lp.gravity = Gravity.TOP | Gravity.START; v.setLayoutParams(lp); // re-layout re-enters here, then converges - Log.i("mirror: letterbox %dx%d -> %dx%d in %dx%d", tw, th, w, h, cw, ch); + Log.i("mirror: letterbox %dx%d -> %dx%d in %dx%d gesture_bottom=%d", + tw, th, w, h, cw, ch, gestureBottomInset); } - session.setViewport(connectedGeometryVersion, - (cw - w) / 2, (ch - h) / 2, w, h); + session.setViewport(connectedGeometryVersion, x, y, w, h); } private void detachSurface() { @@ -377,12 +384,10 @@ public final class Mirror extends Activity { statusText.setText(s); reconnectBtn.setVisibility(state == State.DISCONNECTED ? View.VISIBLE : View.GONE); - // Hide the whole bar while actively mirroring if the user opted - // out; keep it up whenever not CONNECTED so the status and the - // Reconnect button stay reachable. + // Status is only needed during bring-up and after a terminal error. + // Target navigation remains inside the mirrored frame. if (statusBar != null) { - boolean show = Settings.showStatusBar(this) || state != State.CONNECTED; - statusBar.setVisibility(show ? View.VISIBLE : View.GONE); + statusBar.setVisibility(state == State.CONNECTED ? View.GONE : View.VISIBLE); } } diff --git a/app/src/main/java/invalid/lena/scrcpy/Server.java b/app/src/main/java/invalid/lena/scrcpy/Server.java index dbc6738..f4d9b93 100644 --- a/app/src/main/java/invalid/lena/scrcpy/Server.java +++ b/app/src/main/java/invalid/lena/scrcpy/Server.java @@ -183,7 +183,6 @@ public final class Server { String audioCodec = Settings.audioCodec(ctx); int maxSize = Settings.maxSize(ctx); int videoBitR = Settings.videoBitRate(ctx); - int maxFps = Settings.maxFps(ctx); List args = new ArrayList<>(); args.add("CLASSPATH=" + REMOTE_PATH); args.add("app_process"); @@ -199,7 +198,6 @@ public final class Server { args.add("audio_codec=" + audioCodec); args.add("max_size=" + maxSize); args.add("video_bit_rate=" + videoBitR); - if (maxFps > 0) args.add("max_fps=" + maxFps); // Off means off at the source: the server never sends the // target's clipboard, rather than us receiving and discarding it. args.add("clipboard_autosync=" + Settings.clipboardSync(ctx)); diff --git a/app/src/main/java/invalid/lena/scrcpy/Session.java b/app/src/main/java/invalid/lena/scrcpy/Session.java index 73cecb7..2e8e005 100644 --- a/app/src/main/java/invalid/lena/scrcpy/Session.java +++ b/app/src/main/java/invalid/lena/scrcpy/Session.java @@ -178,16 +178,6 @@ public final class Session { if (c != null) c.onBack(); } - public void onHome() { - Controller c = controller; - if (c != null) c.onHome(); - } - - public void onRecents() { - Controller c = controller; - if (c != null) c.onRecents(); - } - public void syncClipboard() { Controller c = controller; if (c != null) c.syncLocalClipboard(); diff --git a/app/src/main/java/invalid/lena/scrcpy/Settings.java b/app/src/main/java/invalid/lena/scrcpy/Settings.java index fab6ed1..70cf46a 100644 --- a/app/src/main/java/invalid/lena/scrcpy/Settings.java +++ b/app/src/main/java/invalid/lena/scrcpy/Settings.java @@ -15,9 +15,7 @@ public final class Settings { public static final String AUDIO_CODEC = "audio_codec"; public static final String MAX_SIZE = "max_size"; // px, long edge; 0 = no cap public static final String VIDEO_BIT_RATE = "video_bit_rate"; // bits/sec - 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"; @@ -30,14 +28,6 @@ public final class Settings { // 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; - // 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 @@ -84,16 +74,6 @@ public final class Settings { } } - public static int maxFps(Context ctx) { - 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"); @@ -123,13 +103,6 @@ public final class Settings { 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 bool(ctx, HINT_BACK_SHOWN, false); } @@ -146,14 +119,6 @@ public final class Settings { prefs(ctx).edit().putBoolean(CLIPBOARD, v).apply(); } - public static boolean showStatusBar(Context ctx) { - 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); diff --git a/app/src/main/java/invalid/lena/scrcpy/SettingsActivity.java b/app/src/main/java/invalid/lena/scrcpy/SettingsActivity.java index b650bc4..fce776b 100644 --- a/app/src/main/java/invalid/lena/scrcpy/SettingsActivity.java +++ b/app/src/main/java/invalid/lena/scrcpy/SettingsActivity.java @@ -1,11 +1,14 @@ package invalid.lena.scrcpy; import android.app.Activity; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; import android.os.Bundle; import android.view.WindowInsets; import android.widget.CheckBox; import android.widget.RadioButton; import android.widget.RadioGroup; +import android.widget.TextView; // Codec choice + streaming knobs. All persisted to SharedPreferences // via Settings on click; the next session bringup reads them and @@ -17,13 +20,12 @@ public final class SettingsActivity extends Activity { super.onCreate(saved); setContentView(R.layout.settings); Ui.padForInsets(findViewById(R.id.root), WindowInsets.Type.systemBars()); + showVersion(); RadioGroup videoGroup = findViewById(R.id.video_codec); RadioGroup audioGroup = findViewById(R.id.audio_codec); RadioGroup maxSizeGroup = findViewById(R.id.max_size); RadioGroup bitRateGroup = findViewById(R.id.video_bit_rate); - RadioGroup maxFpsGroup = findViewById(R.id.max_fps); - CheckBox statusBarBox = findViewById(R.id.show_status_bar); switch (Settings.videoCodec(this)) { case "h265": ((RadioButton) findViewById(R.id.video_h265)).setChecked(true); break; @@ -49,14 +51,6 @@ public final class SettingsActivity extends Activity { case 16_000_000: ((RadioButton) findViewById(R.id.bit_rate_16m)).setChecked(true); break; default: ((RadioButton) findViewById(R.id.bit_rate_4m)).setChecked(true); } - switch (Settings.maxFps(this)) { - case 30: ((RadioButton) findViewById(R.id.max_fps_30)).setChecked(true); break; - case 60: ((RadioButton) findViewById(R.id.max_fps_60)).setChecked(true); break; - case 90: ((RadioButton) findViewById(R.id.max_fps_90)).setChecked(true); break; - case 120: ((RadioButton) findViewById(R.id.max_fps_120)).setChecked(true); break; - default: ((RadioButton) findViewById(R.id.max_fps_0)).setChecked(true); - } - videoGroup.setOnCheckedChangeListener((g, id) -> { String v = "h264"; if (id == R.id.video_h265) v = "h265"; @@ -98,16 +92,6 @@ public final class SettingsActivity extends Activity { Log.i("settings: video_bit_rate=%d", v); }); - maxFpsGroup.setOnCheckedChangeListener((g, id) -> { - int v = 0; - if (id == R.id.max_fps_30) v = 30; - else if (id == R.id.max_fps_60) v = 60; - else if (id == R.id.max_fps_90) v = 90; - else if (id == R.id.max_fps_120) v = 120; - Settings.setMaxFps(this, v); - Log.i("settings: max_fps=%d", v); - }); - findViewById(R.id.licenses).setOnClickListener(v -> startActivity(new android.content.Intent(this, Licenses.class))); @@ -118,10 +102,16 @@ public final class SettingsActivity extends Activity { Log.i("settings: clipboard=%b", checked); }); - statusBarBox.setChecked(Settings.showStatusBar(this)); - statusBarBox.setOnCheckedChangeListener((b, checked) -> { - Settings.setShowStatusBar(this, checked); - Log.i("settings: status_bar=%b", checked); - }); + } + + @SuppressWarnings("deprecation") + private void showVersion() { + try { + PackageInfo info = getPackageManager().getPackageInfo(getPackageName(), 0); + ((TextView) findViewById(R.id.version)).setText(getString( + R.string.version_format, info.versionName, info.getLongVersionCode())); + } catch (PackageManager.NameNotFoundException e) { + throw new IllegalStateException("installed package is missing", e); + } } } -- cgit v1.2.3