From baa4d5139595b11ce71d9c834d0788e365df6f16 Mon Sep 17 00:00:00 2001 From: Compot Developer Date: Thu, 15 Oct 2026 20:04:02 +0000 Subject: [PATCH 1/3] fix: bars hidden again over the video, LAN-only debug addresses, no leaked menu (#202, #203, #205) - Viewing modes go back to full-screen immersive (status and navigation bars hidden, transient on swipe) on resume and on every window focus regain, so after the menu, dialogs and the WebUI too; bars that stay shown are hidden again after 3 s. Pre-R: IMMERSIVE_STICKY | HIDE_NAVIGATION | FULLSCREEN. WebUI keeps its insets padding. - Debug addresses (overlay, /stats.json, UDP hint) list only private IPv4 addresses of non-cellular interfaces, Wi-Fi first, then Ethernet; pure NetInfo.lanAddresses() tested. - Menu popup and the URL/Reset dialogs are dismissed before the activity finishes. - 2.11 (36) Fixes #202, fixes #203, fixes #205 --- android/app/build.gradle | 4 +- .../java/com/openipc/decoder/Decoder.java | 66 +++++++++++++++--- .../java/com/openipc/decoder/MenuManager.java | 29 ++++++++ .../java/com/openipc/decoder/NetInfo.java | 57 ++++++++++++++-- .../com/openipc/decoder/DebugServerTest.java | 68 ++++++++++++++++++- 5 files changed, 202 insertions(+), 22 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index a725c471..d2f10b2d 100755 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -23,8 +23,8 @@ android { applicationId "com.openipc.decoder" minSdk 21 targetSdk 36 - versionCode 35 - versionName "2.10" + versionCode 36 + versionName "2.11" buildConfigField "String", "GIT_HASH", "\"${getGitHash()}\"" } 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 77a02026..05389517 100755 --- a/android/app/src/main/java/com/openipc/decoder/Decoder.java +++ b/android/app/src/main/java/com/openipc/decoder/Decoder.java @@ -320,17 +320,18 @@ public void onCodecChanged(boolean h265, AudioConfig audio) { FrameLayout.LayoutParams.MATCH_PARENT)); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - WindowInsetsController ctrl = getWindow().getInsetsController(); - if (ctrl != null) { - ctrl.hide(WindowInsets.Type.navigationBars()); - ctrl.setSystemBarsBehavior( - WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE); - } - } else { - //noinspection deprecation - getWindow().getDecorView().setSystemUiVisibility( - View.SYSTEM_UI_FLAG_HIDE_NAVIGATION); + // Bars that stay shown while the video has the focus (not the transient ones after + // a swipe, which the app does not see) go away again after a moment (#202) + View decor = getWindow().getDecorView(); + decor.setOnApplyWindowInsetsListener((v, insets) -> { + v.removeCallbacks(hideBarsRunnable); + if (hasWindowFocus() && insets.isVisible(WindowInsets.Type.systemBars())) { + v.postDelayed(hideBarsRunnable, BARS_REHIDE_MS); + } + return v.onApplyWindowInsets(insets); + }); } + hideSystemBars(); updateStatus("disconnected"); DebugServer.get().setSource(debugSource); @@ -500,7 +501,11 @@ private boolean leaveUdpView() { } @Override - public void onExit() { finishAndRemoveTask(); } + public void onExit() { + // Not left showing over a finishing activity (#205) + menuManager.dismissAll(); + finishAndRemoveTask(); + } /** * Reset (#171): stops every stream, deletes the stored settings, the key of the encrypted @@ -2169,6 +2174,9 @@ private void stopExecutor() { @Override protected void onDestroy() { + // The menu and the dialogs go with the activity, or Android logs them as leaked (#205) + if (menuManager != null) menuManager.dismissAll(); + getWindow().getDecorView().removeCallbacks(hideBarsRunnable); super.onDestroy(); DebugServer.get().clearSource(debugSource); streamProbes.shutdown(); @@ -2189,10 +2197,46 @@ protected void onNewIntent(Intent intent) { } } + /** How long bars that stay shown over the video are left before they are hidden (#202). */ + private static final long BARS_REHIDE_MS = 3000; + private final Runnable hideBarsRunnable = this::hideSystemBars; + + /** + * Full screen over the whole display in every viewing mode (single, grid, U), with no + * status or navigation bar (#202). A swipe from an edge shows them for a moment only. + * Android 15 draws apps edge to edge, so bars left shown cover the picture; some systems + * (an OPPO, #187) bring them back after another window had the focus, so this runs again + * whenever the activity resumes or gets the focus back: after the menu, a dialog or the + * WebUI, which keeps the bars and its insets padding (#169, #180). + */ + private void hideSystemBars() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + WindowInsetsController ctrl = getWindow().getInsetsController(); + if (ctrl != null) { + ctrl.setSystemBarsBehavior( + WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE); + ctrl.hide(WindowInsets.Type.systemBars()); + } + } else { + //noinspection deprecation + getWindow().getDecorView().setSystemUiVisibility( + View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY + | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_FULLSCREEN); + } + } + + @Override + public void onWindowFocusChanged(boolean hasFocus) { + super.onWindowFocusChanged(hasFocus); + if (hasFocus) hideSystemBars(); + } + @Override protected void onResume() { super.onResume(); mResumed = true; + hideSystemBars(); startStreams(); updateDebugUrls(); } 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 87af8ee0..85e8daec 100644 --- a/android/app/src/main/java/com/openipc/decoder/MenuManager.java +++ b/android/app/src/main/java/com/openipc/decoder/MenuManager.java @@ -66,6 +66,8 @@ class MenuManager { private TextView mWebUiBtn; TextView[] camButtons; private Dialog mBrowserDialog; + /** The URL editor or the Reset confirmation while it is shown (#205). */ + private Dialog mDialog; private int mActiveQualityColor = Color.WHITE; interface Listener { @@ -128,6 +130,31 @@ void dismissPopup() { boolean isPopupShowing() { return mMenuPopup != null; } + /** + * The menu, the open dialog and the WebUI closed before the activity finishes: a window + * still showing then is logged as leaked (#205). + */ + void dismissAll() { + PopupWindow popup = mMenuPopup; + mMenuPopup = null; + Dialog dialog = mDialog; + mDialog = null; + try { + if (popup != null && popup.isShowing()) popup.dismiss(); + if (dialog != null && dialog.isShowing()) dialog.dismiss(); + } catch (Exception e) { + Log.e(TAG, "Error dismissing the menu", e); + } + onPauseDismissBrowser(); + } + + private void track(Dialog dialog) { + mDialog = dialog; + dialog.setOnDismissListener(d -> { + if (mDialog == d) mDialog = null; + }); + } + void showMenu(View anchor, boolean quadEnabled, int mActive, String version, String gitHash, String[] mHosts) { boolean single = !quadEnabled && !udpShown; @@ -536,6 +563,7 @@ void showUrlEditor(int mActive, String[] mHosts, Transport.Mode[] mTransports) { AlertDialog dialog = builder.create(); dialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE); + track(dialog); dialog.show(); input.requestFocus(); } @@ -560,6 +588,7 @@ private void confirmReset() { View cancel = dialog.getButton(AlertDialog.BUTTON_NEGATIVE); if (cancel != null) cancel.requestFocus(); }); + track(dialog); dialog.show(); } diff --git a/android/app/src/main/java/com/openipc/decoder/NetInfo.java b/android/app/src/main/java/com/openipc/decoder/NetInfo.java index 2d53a3a1..6602faa2 100644 --- a/android/app/src/main/java/com/openipc/decoder/NetInfo.java +++ b/android/app/src/main/java/com/openipc/decoder/NetInfo.java @@ -15,35 +15,78 @@ import java.net.NetworkInterface; import java.util.ArrayList; import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Locale; +import java.util.Map; final class NetInfo { private static final String TAG = "OpenIPCDecoder"; private NetInfo() {} + /** Interface name prefixes of mobile data (#203): never a LAN the debug page answers. */ + private static final String[] CELLULAR = { + "rmnet", "rev_rmnet", "ccmni", "v4-", "clat", "seth", "pdp", "wwan"}; + /** * IPv4 addresses of every interface that is up, the ones worth showing to a user who has - * to type one in: no loopback (127.0.0.0/8) and no link-local (169.254.0.0/16) (#87, #96). + * to type one in (#87, #96): Wi-Fi first, then Ethernet, then the rest (#203). */ static List addresses() { - List out = new ArrayList<>(); + Map> byInterface = new LinkedHashMap<>(); try { for (NetworkInterface nif : Collections.list(NetworkInterface.getNetworkInterfaces())) { if (!nif.isUp()) continue; - for (InetAddress a : Collections.list(nif.getInetAddresses())) { - if (isShown(a) && !out.contains(a.getHostAddress())) out.add(a.getHostAddress()); - } + byInterface.put(nif.getName(), Collections.list(nif.getInetAddresses())); } } catch (Exception e) { Log.w(TAG, "No local address: " + e.getMessage()); } + return lanAddresses(byInterface); + } + + /** + * The addresses a LAN client can reach the debug server on, from the interfaces in their + * system order (#203): no mobile-data interface and only private IPv4 (isShown), as the + * server answers LAN clients only. Wi-Fi (also a hotspot) first, then Ethernet. + */ + static List lanAddresses(Map> byInterface) { + List out = new ArrayList<>(); + for (int rank = 0; rank <= 2; rank++) { + for (Map.Entry> e : byInterface.entrySet()) { + String name = e.getKey() == null ? "" : e.getKey().toLowerCase(Locale.ROOT); + if (isCellular(name) || rank(name) != rank) continue; + for (InetAddress a : e.getValue()) { + if (isShown(a) && !out.contains(a.getHostAddress())) out.add(a.getHostAddress()); + } + } + } return out; } + static boolean isCellular(String name) { + for (String prefix : CELLULAR) { + if (name.startsWith(prefix)) return true; + } + return false; + } + + /** 0 Wi-Fi, 1 Ethernet, 2 anything else (USB tethering, a bridge ...). */ + private static int rank(String name) { + if (name.startsWith("wlan") || name.startsWith("swlan") || name.startsWith("ap") + || name.startsWith("wifi")) return 0; + if (name.startsWith("eth") || name.startsWith("en")) return 1; + return 2; + } + + /** + * An address to show: private IPv4 (10/8, 172.16/12, 192.168/16) — no loopback, no + * link-local and nothing public or carrier-grade NAT, which the debug server never + * answers (#203). + */ static boolean isShown(InetAddress a) { - return a instanceof Inet4Address && !a.isLoopbackAddress() && !a.isLinkLocalAddress() - && !a.isAnyLocalAddress(); + return a instanceof Inet4Address && a.isSiteLocalAddress(); } /** diff --git a/android/app/src/test/java/com/openipc/decoder/DebugServerTest.java b/android/app/src/test/java/com/openipc/decoder/DebugServerTest.java index 213f9372..e7462314 100644 --- a/android/app/src/test/java/com/openipc/decoder/DebugServerTest.java +++ b/android/app/src/test/java/com/openipc/decoder/DebugServerTest.java @@ -18,8 +18,10 @@ import java.io.IOException; import java.net.InetAddress; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; public class DebugServerTest { @@ -47,10 +49,14 @@ public void otherClientsAreRejected() throws Exception { } @Test - public void shownAddressesSkipLoopbackAndLinkLocal() throws Exception { + public void shownAddressesArePrivateIpv4Only() throws Exception { assertTrue(NetInfo.isShown(ip("192.168.1.20"))); assertTrue(NetInfo.isShown(ip("172.17.32.144"))); - assertTrue(NetInfo.isShown(ip("8.8.8.8"))); + assertTrue(NetInfo.isShown(ip("10.0.0.5"))); + // Public and carrier-grade NAT: the debug server never answers there (#203) + assertFalse(NetInfo.isShown(ip("8.8.8.8"))); + assertFalse(NetInfo.isShown(ip("33.91.55.135"))); + assertFalse(NetInfo.isShown(ip("100.64.1.2"))); assertFalse(NetInfo.isShown(ip("127.0.0.1"))); assertFalse(NetInfo.isShown(ip("127.1.2.3"))); assertFalse(NetInfo.isShown(ip("169.254.1.1"))); @@ -58,6 +64,64 @@ public void shownAddressesSkipLoopbackAndLinkLocal() throws Exception { assertFalse(NetInfo.isShown(ip("fd12::1"))); } + private static Map> interfaces(String... nameAndAddresses) + throws Exception { + Map> m = new LinkedHashMap<>(); + for (String entry : nameAndAddresses) { + String[] parts = entry.split(" "); + List addrs = new ArrayList<>(); + for (int i = 1; i < parts.length; i++) addrs.add(ip(parts[i])); + m.put(parts[0], addrs); + } + return m; + } + + @Test + public void lanAddressesSkipMobileData() throws Exception { + // The OPPO in #187: two mobile-data addresses besides Wi-Fi (#203) + assertEquals(Arrays.asList("192.168.1.42"), NetInfo.lanAddresses(interfaces( + "lo 127.0.0.1 ::1", + "rmnet_data0 33.91.55.135", + "rmnet_data1 33.95.96.154 2a00:1fa0::5", + "wlan0 192.168.1.42 fe80::1 fd00::42"))); + // Mobile data on private ranges (carrier NAT) is not shown either + assertEquals(Arrays.asList("10.0.0.7"), NetInfo.lanAddresses(interfaces( + "ccmni0 10.120.3.4", + "v4-rmnet_data2 192.0.0.4", + "clat4 192.0.0.4", + "seth_lte0 10.11.12.13", + "pdp0 172.20.0.1", + "wwan0 192.168.200.2", + "rev_rmnet0 10.9.9.9", + "wlan0 10.0.0.7"))); + assertEquals(Arrays.asList(), NetInfo.lanAddresses(interfaces( + "lo 127.0.0.1", "rmnet0 100.70.1.2"))); + } + + @Test + public void lanAddressesWifiFirstThenEthernet() throws Exception { + assertEquals(Arrays.asList("192.168.1.42", "192.168.43.1", "172.17.32.144", "10.42.0.1"), + NetInfo.lanAddresses(interfaces( + "rndis0 10.42.0.1", + "eth0 172.17.32.144 169.254.3.3", + "wlan0 192.168.1.42", + "ap0 192.168.43.1 192.168.1.42"))); + // The system order stays within one kind + assertEquals(Arrays.asList("192.168.2.2", "192.168.1.1"), + NetInfo.lanAddresses(interfaces("eth1 192.168.2.2", "eth0 192.168.1.1"))); + } + + @Test + public void cellularInterfaceNames() { + for (String n : new String[] {"rmnet0", "rmnet_data0", "ccmni1", "v4-rmnet_data0", + "clat4", "seth_lte1", "pdp0", "wwan0"}) { + assertTrue(n, NetInfo.isCellular(n)); + } + for (String n : new String[] {"wlan0", "eth0", "ap0", "swlan0", "rndis0", "lo"}) { + assertFalse(n, NetInfo.isCellular(n)); + } + } + @Test public void credentialsAreRedacted() { String r = DebugServer.redact("rtsp://admin:secret@192.168.1.10:554/stream=0"); From c2a83f2c144892879ff721ed8e97686f4a367461 Mon Sep 17 00:00:00 2001 From: Compot Developer Date: Thu, 15 Oct 2026 20:45:38 +0000 Subject: [PATCH 2/3] fix: the U view's hint keeps every IPv4 address (Qodo on #206) A pushed RTP stream can arrive on any address, mobile data included; only the debug page's addresses are LAN-only (#203). --- .../java/com/openipc/decoder/Decoder.java | 8 ++--- .../java/com/openipc/decoder/NetInfo.java | 29 +++++++++++++++++-- .../com/openipc/decoder/DebugServerTest.java | 12 ++++++++ 3 files changed, 43 insertions(+), 6 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 05389517..de5e8c28 100755 --- a/android/app/src/main/java/com/openipc/decoder/Decoder.java +++ b/android/app/src/main/java/com/openipc/decoder/Decoder.java @@ -913,7 +913,7 @@ private void buildQuadGrid(int size) { /** Shown over the U cell until its first frame: where to send the stream. */ private TextView udpHintCell() { TextView hint = new TextView(this); - hint.setText(udpHintText(NetInfo.addresses(), UDP_RTP_PORT)); + hint.setText(udpHintText(NetInfo.udpAddresses(), UDP_RTP_PORT)); hint.setTextColor(Color.GRAY); hint.setTextSize(13); hint.setGravity(Gravity.CENTER); @@ -931,7 +931,7 @@ private void refreshUdpHint() { if (v == null) return; View overlay = ((View) v.getParent()).findViewWithTag("quad_overlay_" + U_CELL); if (!(overlay instanceof TextView) || overlay.getVisibility() != View.VISIBLE) return; - String text = udpHintText(NetInfo.addresses(), UDP_RTP_PORT); + String text = udpHintText(NetInfo.udpAddresses(), UDP_RTP_PORT); TextView hint = (TextView) overlay; if (!text.contentEquals(hint.getText())) hint.setText(text); } @@ -1066,7 +1066,7 @@ private void showQuadOverlay(int i) { overlay.setBackgroundColor(Color.BLACK); cell.addView(overlay); } else if (i == U_CELL) { - ((TextView) overlay).setText(udpHintText(NetInfo.addresses(), UDP_RTP_PORT)); + ((TextView) overlay).setText(udpHintText(NetInfo.udpAddresses(), UDP_RTP_PORT)); } overlay.setVisibility(View.VISIBLE); overlay.bringToFront(); @@ -1526,7 +1526,7 @@ private Map debugState() { if (udpViewShown) { Map udp = new LinkedHashMap<>(); udp.put("port", UDP_RTP_PORT); - udp.put("addresses", NetInfo.addresses()); + udp.put("addresses", NetInfo.udpAddresses()); m.put("udp", udp); } else if (!quadEnabled) { Map single = new LinkedHashMap<>(); diff --git a/android/app/src/main/java/com/openipc/decoder/NetInfo.java b/android/app/src/main/java/com/openipc/decoder/NetInfo.java index 6602faa2..d953e876 100644 --- a/android/app/src/main/java/com/openipc/decoder/NetInfo.java +++ b/android/app/src/main/java/com/openipc/decoder/NetInfo.java @@ -30,8 +30,8 @@ private NetInfo() {} "rmnet", "rev_rmnet", "ccmni", "v4-", "clat", "seth", "pdp", "wwan"}; /** - * IPv4 addresses of every interface that is up, the ones worth showing to a user who has - * to type one in (#87, #96): Wi-Fi first, then Ethernet, then the rest (#203). + * The debug page's addresses (#87, #96): private IPv4 of every interface that is up and + * not mobile data, Wi-Fi first, then Ethernet, then the rest (#203). */ static List addresses() { Map> byInterface = new LinkedHashMap<>(); @@ -46,6 +46,31 @@ static List addresses() { return lanAddresses(byInterface); } + /** + * Every IPv4 address a pushed RTP stream can arrive on (the UDP receiver listens on all of + * them), mobile data and public ones too, for the U view's hint (#68, Qodo on #206): + * no loopback, link-local or wildcard address. + */ + static List udpAddresses() { + List out = new ArrayList<>(); + try { + for (NetworkInterface nif : Collections.list(NetworkInterface.getNetworkInterfaces())) { + if (!nif.isUp()) continue; + for (InetAddress a : Collections.list(nif.getInetAddresses())) { + if (isUsable(a) && !out.contains(a.getHostAddress())) out.add(a.getHostAddress()); + } + } + } catch (Exception e) { + Log.w(TAG, "No local address: " + e.getMessage()); + } + return out; + } + + static boolean isUsable(InetAddress a) { + return a instanceof Inet4Address && !a.isLoopbackAddress() && !a.isLinkLocalAddress() + && !a.isAnyLocalAddress(); + } + /** * The addresses a LAN client can reach the debug server on, from the interfaces in their * system order (#203): no mobile-data interface and only private IPv4 (isShown), as the diff --git a/android/app/src/test/java/com/openipc/decoder/DebugServerTest.java b/android/app/src/test/java/com/openipc/decoder/DebugServerTest.java index e7462314..619a0eff 100644 --- a/android/app/src/test/java/com/openipc/decoder/DebugServerTest.java +++ b/android/app/src/test/java/com/openipc/decoder/DebugServerTest.java @@ -64,6 +64,18 @@ public void shownAddressesArePrivateIpv4Only() throws Exception { assertFalse(NetInfo.isShown(ip("fd12::1"))); } + @Test + public void udpAddressesKeepPublicOnes() throws Exception { + // The U view's hint: a pushed stream may come over any IPv4 address (Qodo on #206) + assertTrue(NetInfo.isUsable(ip("192.168.1.20"))); + assertTrue(NetInfo.isUsable(ip("8.8.8.8"))); + assertTrue(NetInfo.isUsable(ip("100.64.1.2"))); + assertFalse(NetInfo.isUsable(ip("127.0.0.1"))); + assertFalse(NetInfo.isUsable(ip("169.254.1.1"))); + assertFalse(NetInfo.isUsable(ip("0.0.0.0"))); + assertFalse(NetInfo.isUsable(ip("fd12::1"))); + } + private static Map> interfaces(String... nameAndAddresses) throws Exception { Map> m = new LinkedHashMap<>(); From 7741cf0fac7a42a1ad532ed31210a3df6e68872f Mon Sep 17 00:00:00 2001 From: Compot Developer Date: Thu, 15 Oct 2026 21:35:58 +0000 Subject: [PATCH 3/3] chore: version 2.12 (2.11 went to #208) --- android/app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index d2f10b2d..7419bbaa 100755 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -23,8 +23,8 @@ android { applicationId "com.openipc.decoder" minSdk 21 targetSdk 36 - versionCode 36 - versionName "2.11" + versionCode 37 + versionName "2.12" buildConfigField "String", "GIT_HASH", "\"${getGitHash()}\"" }