diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 6075acaa..23f0da2b 100755
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -6,6 +6,8 @@
+
+
-
+
+
{
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);
+ }
});
}
}
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));
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]);