aboutsummaryrefslogtreecommitdiff
path: root/app/src/main/java/invalid/lena
diff options
context:
space:
mode:
Diffstat (limited to 'app/src/main/java/invalid/lena')
-rw-r--r--app/src/main/java/invalid/lena/scrcpy/ControlMessages.java5
-rw-r--r--app/src/main/java/invalid/lena/scrcpy/Controller.java17
-rw-r--r--app/src/main/java/invalid/lena/scrcpy/Mirror.java61
-rw-r--r--app/src/main/java/invalid/lena/scrcpy/Server.java2
-rw-r--r--app/src/main/java/invalid/lena/scrcpy/Session.java10
-rw-r--r--app/src/main/java/invalid/lena/scrcpy/Settings.java35
-rw-r--r--app/src/main/java/invalid/lena/scrcpy/SettingsActivity.java40
7 files changed, 48 insertions, 122 deletions
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<String> 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);
+ }
}
}