From d3e3c9c0b8dab67bf4d157b73fda1746522da3c8 Mon Sep 17 00:00:00 2001 From: Compot Developer Date: Mon, 12 Oct 2026 10:34:47 +0000 Subject: [PATCH 1/4] fix: mark touchscreen as optional for TV devices Without the uses-feature entry Google Play assumes a touchscreen is required and hides the app on Android TV and set-top boxes (fixes #29). --- android/app/src/main/AndroidManifest.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 6075acaa..1d921193 100755 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -6,6 +6,8 @@ + + Date: Mon, 12 Oct 2026 10:34:47 +0000 Subject: [PATCH 2/4] fix: cancel stale status overlay timers on every status change The 1 s details refresh and the 5 s hide posted on "connected" were never cancelled, so a later "Disconnected" could be overwritten or hidden (fixes #26). --- .../java/com/openipc/decoder/Decoder.java | 62 ++++++++++--------- 1 file changed, 34 insertions(+), 28 deletions(-) diff --git a/android/app/src/main/java/com/openipc/decoder/Decoder.java b/android/app/src/main/java/com/openipc/decoder/Decoder.java index 15e2dce5..ff7ae467 100755 --- a/android/app/src/main/java/com/openipc/decoder/Decoder.java +++ b/android/app/src/main/java/com/openipc/decoder/Decoder.java @@ -391,9 +391,40 @@ private void saveSettings() { // ====================== Status overlay ====================== + // Delayed overlay updates after "connected"; cancelled on every status change + private final Runnable statusDetails = this::showConnectedDetails; + private final Runnable statusHide = () -> { + if (statusText != null) statusText.setVisibility(View.GONE); + }; + + private void showConnectedDetails() { + if (statusText == null || statusText.getVisibility() != View.VISIBLE) return; + String videoCodec = codecH265 ? "H.265" : "H.264"; + String aCodec; + switch (audioCodec) { + case AudioConfig.AAC: aCodec = " • AAC"; break; + case AudioConfig.PCMU: + case AudioConfig.PCMA: aCodec = " • G.711"; break; + case AudioConfig.PCM: aCodec = " • PCM"; break; + default: aCodec = ""; break; + } + long elapsedNs = System.nanoTime() - statusBitrateNs; + String bps = ""; + long bytes = rtspClient != null ? rtspClient.getStreamBytes() : 0; + if (elapsedNs > 500_000_000 && bytes > 0) { + double mbps = (bytes * 8.0 / 1_000_000) + / (elapsedNs / 1_000_000_000.0); + bps = String.format(Locale.US, " • %.1f Mbps", mbps); + } + statusText.setText("Connected • " + videoCodec + aCodec + bps); + } + private void updateStatus(String status) { runOnUiThread(() -> { if (statusText == null) return; + // A stale timer from an earlier "connected" must not overwrite or hide this status + statusText.removeCallbacks(statusDetails); + statusText.removeCallbacks(statusHide); switch (status) { case "connecting": @@ -405,34 +436,9 @@ private void updateStatus(String status) { statusText.setVisibility(View.VISIBLE); statusStreamBytes = rtspClient != null ? rtspClient.getStreamBytes() : 0; statusBitrateNs = rtspClient != null ? rtspClient.getBitrateMarkNs() : System.nanoTime(); - // After 1 second, refresh with codec + bitrate info - statusText.postDelayed(() -> { - if (statusText == null - || statusText.getVisibility() != View.VISIBLE) return; - String videoCodec = codecH265 ? "H.265" : "H.264"; - String aCodec; - switch (audioCodec) { - case AudioConfig.AAC: aCodec = " • AAC"; break; - case AudioConfig.PCMU: - case AudioConfig.PCMA: aCodec = " • G.711"; break; - case AudioConfig.PCM: aCodec = " • PCM"; break; - default: aCodec = ""; break; - } - long elapsedNs = System.nanoTime() - statusBitrateNs; - String bps = ""; - long bytes = rtspClient != null ? rtspClient.getStreamBytes() : 0; - if (elapsedNs > 500_000_000 && bytes > 0) { - double mbps = (bytes * 8.0 / 1_000_000) - / (elapsedNs / 1_000_000_000.0); - bps = String.format(Locale.US, " • %.1f Mbps", mbps); - } - statusText.setText("Connected • " + videoCodec + aCodec + bps); - }, 1000); - statusText.postDelayed(() -> { - if (statusText != null) { - statusText.setVisibility(View.GONE); - } - }, 5000); + // After 1 second, refresh with codec + bitrate info; hide after 5 seconds + statusText.postDelayed(statusDetails, 1000); + statusText.postDelayed(statusHide, 5000); break; case "disconnected": statusText.setText(getString(R.string.status_disconnected)); From af7aef70a54ec0cac8eaeb3b575c555abbd2c518 Mon Sep 17 00:00:00 2001 From: Compot Developer Date: Mon, 12 Oct 2026 10:34:47 +0000 Subject: [PATCH 3/4] fix: end the process after an uncaught exception The handler started the Crash activity but left the crashed process running in an undefined state (or frozen, after a main-thread crash). The process is now killed; Crash runs in its own :crash process so it still shows the report (fixes #24). --- android/app/src/main/AndroidManifest.xml | 3 ++- .../src/main/java/com/openipc/decoder/App.java | 17 ++++++++++++----- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 1d921193..23f0da2b 100755 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -18,7 +18,8 @@ android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:usesCleartextTraffic="true"> - + + { Log.e("OpenIPCDecoder", "Uncaught exception", throwable); - Intent intent = new Intent(getApplicationContext(), Crash.class); - intent.putExtra("error", Log.getStackTraceString(throwable)); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); - startActivity(intent); - // Process will terminate naturally with the crash activity visible + try { + Intent intent = new Intent(getApplicationContext(), Crash.class); + intent.putExtra("error", Log.getStackTraceString(throwable)); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + startActivity(intent); + } finally { + // The app state is undefined after an uncaught exception (and the main looper + // may be dead), so end this process; Crash runs in its own ":crash" process. + Process.killProcess(Process.myPid()); + System.exit(10); + } }); } } From 3a0f69f3f25072c74fc44ee15b8dfa961c6eb156 Mon Sep 17 00:00:00 2001 From: Compot Developer Date: Mon, 12 Oct 2026 10:34:47 +0000 Subject: [PATCH 4/4] fix: send WebUI credentials only to the camera host onReceivedHttpAuthRequest answered every host with the user and password from the RTSP URL, so a redirect or link to another host received them (fixes #25). --- android/app/src/main/java/com/openipc/decoder/MenuManager.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/android/app/src/main/java/com/openipc/decoder/MenuManager.java b/android/app/src/main/java/com/openipc/decoder/MenuManager.java index fa85657c..8aab8d6d 100644 --- a/android/app/src/main/java/com/openipc/decoder/MenuManager.java +++ b/android/app/src/main/java/com/openipc/decoder/MenuManager.java @@ -337,7 +337,8 @@ void startBrowser(String mHost) { public void onReceivedHttpAuthRequest( WebView v, HttpAuthHandler handler, String h, String realm) { String userInfo = finalUri.getUserInfo(); - if (userInfo != null) { + // Only the camera itself gets its credentials, never a host reached by a redirect or link + if (userInfo != null && link.equalsIgnoreCase(h)) { String[] parts = userInfo.split(":", 2); if (parts.length == 2) { handler.proceed(parts[0], parts[1]);