From ff7acf898b48359275a5b09b82ed926a945233f8 Mon Sep 17 00:00:00 2001 From: Lena Date: Wed, 1 Jul 2026 00:00:00 +0000 Subject: app: fix session lifecycle, teardown, and opus playback Opus playback never worked: feed() called dequeueInputBuffer() on a codec in async-callback mode, which always throws, so every packet was silently dropped. Feed input through the async callback with a bounded pending queue instead. Make bring-up transactional: roll back partially opened streams and the server shell on failure, join reader threads on stop, and close the owning ADB streams before releasing sinks. Replace the openAbstract watchdog thread with a real timeout in the vendored AdbConnection.open(), which also removes the half-open stream from the lookup table on failure. Harden the activity: release owned Surfaces, gate callbacks on a destroyed flag and a session generation, serialize reconnect, and handle target replacement via singleTask + onNewIntent. Propagate device-list write failures instead of swallowing them, bound the clipboard payload and the video pending queue against hostile peers, and discard incomplete recordings instead of keeping corrupt files. Use the mediaPlayback foreground-service type; Android 15 stops dataSync services after six hours. Drop the unused ACCESS_NETWORK_STATE permission. --- app/src/main/java/invalid/lena/scrcpy/Mirror.java | 63 ++++++++++++++++++++--- 1 file changed, 56 insertions(+), 7 deletions(-) (limited to 'app/src/main/java/invalid/lena/scrcpy/Mirror.java') diff --git a/app/src/main/java/invalid/lena/scrcpy/Mirror.java b/app/src/main/java/invalid/lena/scrcpy/Mirror.java index 2c361da..2a9d390 100644 --- a/app/src/main/java/invalid/lena/scrcpy/Mirror.java +++ b/app/src/main/java/invalid/lena/scrcpy/Mirror.java @@ -60,6 +60,10 @@ public final class Mirror extends Activity { private Devices.Device target; private Session session; private Surface currentSurface; + private boolean ownsCurrentSurface; + private volatile boolean destroyed; + private long sessionGeneration; + private boolean stoppingSession; private State state = State.CONNECTING; private int connectedW, connectedH; @@ -138,6 +142,7 @@ public final class Mirror extends Activity { try { Adb a = Adb.getInstance(this); runOnUiThread(() -> { + if (destroyed) return; adb = a; if (session == null && currentSurface != null) { startSession(currentSurface); @@ -146,6 +151,7 @@ public final class Mirror extends Activity { } catch (Exception e) { Log.e(e, "mirror: adb init"); runOnUiThread(() -> { + if (destroyed) return; Toast.makeText(this, "adb init: " + e.getMessage(), Toast.LENGTH_LONG).show(); finish(); @@ -161,7 +167,7 @@ public final class Mirror extends Activity { @Override public void onSurfaceTextureAvailable(SurfaceTexture st, int w, int h) { Log.i("mirror: texture available %dx%d", w, h); - attachSurface(new Surface(st), w, h); + attachSurface(new Surface(st), true, w, h); } @Override public void onSurfaceTextureSizeChanged(SurfaceTexture st, int w, int h) { @@ -188,7 +194,7 @@ public final class Mirror extends Activity { @Override public void surfaceCreated(SurfaceHolder holder) { Log.i("mirror: surface created"); - attachSurface(holder.getSurface(), 0, 0); + attachSurface(holder.getSurface(), false, 0, 0); } @Override public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) { @@ -204,30 +210,43 @@ public final class Mirror extends Activity { // ---- session driver ---- - private void attachSurface(Surface s, int w, int h) { + private void attachSurface(Surface s, boolean owned, int w, int h) { + if (session != null && currentSurface != null) session.swapSurface(null); + releaseOwnedSurface(); currentSurface = s; + ownsCurrentSurface = owned; if (session != null) { session.swapSurface(s); if (w > 0) session.setViewSize(w, h); return; } if (adb == null) return; // adb-init thread will start the session + if (stoppingSession) return; // stop worker starts the latest target if (state == State.DISCONNECTED) return; // wait for user to tap reconnect startSession(s); if (w > 0) session.setViewSize(w, h); } private void detachSurface() { - currentSurface = null; if (session != null) session.swapSurface(null); + releaseOwnedSurface(); + currentSurface = null; + } + + private void releaseOwnedSurface() { + if (ownsCurrentSurface && currentSurface != null) currentSurface.release(); + ownsCurrentSurface = false; } private void startSession(Surface s) { + if (destroyed) return; state = State.CONNECTING; updateStatusBar(); + long generation = ++sessionGeneration; session = new Session(this, adb, target, s, new Session.Listener() { @Override public void onConnected(int w, int h) { runOnUiThread(() -> { + if (destroyed || generation != sessionGeneration) return; state = State.CONNECTED; connectedW = w; connectedH = h; updateStatusBar(); @@ -236,12 +255,14 @@ public final class Mirror extends Activity { @Override public void onReconnecting() { Log.i("mirror: link lost, reconnecting"); runOnUiThread(() -> { + if (destroyed || generation != sessionGeneration) return; state = State.CONNECTING; updateStatusBar(); }); } @Override public void onError(Throwable t) { runOnUiThread(() -> { + if (destroyed || generation != sessionGeneration) return; Toast.makeText(Mirror.this, "session error: " + t.getMessage(), Toast.LENGTH_LONG).show(); }); @@ -249,6 +270,7 @@ public final class Mirror extends Activity { @Override public void onStopped() { Log.i("mirror: session stopped"); runOnUiThread(() -> { + if (destroyed || generation != sessionGeneration) return; state = State.DISCONNECTED; updateStatusBar(); }); @@ -286,10 +308,13 @@ public final class Mirror extends Activity { private void reconnect() { Log.i("mirror: reconnect tapped"); - Session old = session; - session = null; state = State.CONNECTING; updateStatusBar(); + if (stoppingSession) return; + stoppingSession = true; + Session old = session; + session = null; + sessionGeneration++; // Stop the old session off the UI thread (teardown closes // sockets and joins the server's log pump), THEN start the new // one. Sequencing matters: both sessions share the singleton @@ -298,6 +323,8 @@ public final class Mirror extends Activity { new Thread(() -> { if (old != null) old.stop(); runOnUiThread(() -> { + if (destroyed) return; + stoppingSession = false; if (session != null) return; // another path already started one if (adb == null || currentSurface == null) return; startSession(currentSurface); @@ -305,6 +332,24 @@ public final class Mirror extends Activity { }, "session-stop").start(); } + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + String host = intent.getStringExtra(EXTRA_HOST); + int port = intent.getIntExtra(EXTRA_PORT, -1); + if (host == null || port <= 0 || port > 65535) { + Log.w("mirror: ignoring bad replacement target host=%s port=%d", host, port); + return; + } + if (target.host.equals(host) && target.port == port) return; + + setIntent(intent); + target = new Devices.Device(host, port); + connectedW = connectedH = 0; + if (overlayTarget != null) overlayTarget.setText("target: " + target); + reconnect(); + } + private void updateStatusBar() { if (statusText == null) return; String s; @@ -394,8 +439,11 @@ public final class Mirror extends Activity { @Override protected void onDestroy() { - super.onDestroy(); + destroyed = true; + sessionGeneration++; ui.removeCallbacksAndMessages(null); + releaseOwnedSurface(); + currentSurface = null; Session s = session; session = null; if (s != null) { @@ -404,6 +452,7 @@ public final class Mirror extends Activity { new Thread(s::stop, "session-stop").start(); } stopService(new Intent(this, Sessions.class)); + super.onDestroy(); } // Android 13+ requires runtime grant for POST_NOTIFICATIONS. The -- cgit v1.2.3