Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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()}\""
}
Expand Down
74 changes: 59 additions & 15 deletions android/app/src/main/java/com/openipc/decoder/Decoder.java
Original file line number Diff line number Diff line change
Expand Up @@ -325,17 +325,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);
Expand Down Expand Up @@ -506,7 +507,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
Expand Down Expand Up @@ -920,7 +925,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);
Expand All @@ -938,7 +943,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);
}
Expand Down Expand Up @@ -1073,7 +1078,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();
Expand Down Expand Up @@ -1533,7 +1538,7 @@ private Map<String, Object> debugState() {
if (udpViewShown) {
Map<String, Object> 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<String, Object> single = new LinkedHashMap<>();
Expand Down Expand Up @@ -2367,6 +2372,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();
Expand All @@ -2387,10 +2395,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();
}
Expand Down
29 changes: 29 additions & 0 deletions android/app/src/main/java/com/openipc/decoder/MenuManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -130,6 +132,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;
Expand Down Expand Up @@ -556,6 +583,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();
}
Expand All @@ -580,6 +608,7 @@ private void confirmReset() {
View cancel = dialog.getButton(AlertDialog.BUTTON_NEGATIVE);
if (cancel != null) cancel.requestFocus();
});
track(dialog);
dialog.show();
}

Expand Down
76 changes: 72 additions & 4 deletions android/app/src/main/java/com/openipc/decoder/NetInfo.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,49 @@
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).
* 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<String> addresses() {
Map<String, List<InetAddress>> byInterface = new LinkedHashMap<>();
try {
for (NetworkInterface nif : Collections.list(NetworkInterface.getNetworkInterfaces())) {
if (!nif.isUp()) continue;
byInterface.put(nif.getName(), Collections.list(nif.getInetAddresses()));
}
} catch (Exception e) {
Log.w(TAG, "No local address: " + e.getMessage());
}
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<String> udpAddresses() {
List<String> out = new ArrayList<>();
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());
if (isUsable(a) && !out.contains(a.getHostAddress())) out.add(a.getHostAddress());
}
}
} catch (Exception e) {
Expand All @@ -41,11 +66,54 @@ static List<String> addresses() {
return out;
}

static boolean isShown(InetAddress a) {
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
* server answers LAN clients only. Wi-Fi (also a hotspot) first, then Ethernet.
*/
static List<String> lanAddresses(Map<String, List<InetAddress>> byInterface) {
List<String> out = new ArrayList<>();
for (int rank = 0; rank <= 2; rank++) {
for (Map.Entry<String, List<InetAddress>> 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()) {
Comment on lines +81 to +85

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. Usb-tethered addresses are advertised 📎 Requirement gap ≡ Correctness

NetInfo.lanAddresses() processes rank-2 interfaces after Wi-Fi and Ethernet instead of limiting
results to those two interface classes. A private address on an interface such as rndis0, a
bridge, or another non-cellular rank-2 interface therefore reaches the debug overlay and advertised
endpoints.
Agent Prompt
## Issue description
The address collector includes rank-2 interfaces even though advertised addresses must come only from eligible Wi-Fi and Ethernet interfaces.

## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/NetInfo.java[54-80]
- android/app/src/test/java/com/openipc/decoder/DebugServerTest.java[101-112]

## Recommended Fix
Stop iterating rank 2, or explicitly exclude every interface that is not classified as Wi-Fi or Ethernet. Preserve Wi-Fi-before-Ethernet ordering and add a test proving that private addresses on `rndis`, bridge, and other non-Wi-Fi/non-Ethernet interfaces are omitted.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kept on purpose: the debug server answers every private client, and a phone sharing its connection over USB (rndis) or a bridge is such a LAN, so its private address is reachable. They come last, after Wi-Fi and Ethernet, as #203 asks for the order; only mobile-data interfaces are left out.

if (isShown(a) && !out.contains(a.getHostAddress())) out.add(a.getHostAddress());
Comment thread
qodo-free-for-open-source-projects[bot] marked this conversation as resolved.
}
}
}
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.isSiteLocalAddress();
}
Comment on lines 113 to 115

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Local ipv6 addresses disappear 📎 Requirement gap ≡ Correctness

NetInfo.isShown() accepts only IPv4 site-local addresses, so link-local and unique-local IPv6
addresses are discarded. lanAddresses() applies this filter to every interface used by the
overlay, /stats.json, and UDP hint, preventing eligible local addresses from being advertised.
Agent Prompt
## Issue description
The address filter rejects link-local and unique-local addresses even though the debug address lists must support eligible local Wi-Fi and Ethernet addresses in those ranges.

## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/NetInfo.java[83-90]
- android/app/src/test/java/com/openipc/decoder/DebugServerTest.java[52-65]

## Recommended Fix
Update `isShown()` to accept the required private IPv4, link-local, and unique-local address ranges while continuing to reject public, loopback, and carrier-grade NAT addresses. Add tests covering eligible IPv4 and IPv6 link-local and unique-local addresses.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not changed: the shown addresses were IPv4 only before this PR as well (#87, #96). An IPv6 link-local URL needs a zone id (fe80::1%wlan0) that a browser cannot use, and ULA addresses are rare on the LANs this app runs in; a user types the IPv4 address. Only public/CGNAT IPv4 and mobile-data interfaces are dropped (#203).


/**
* A client on the local network: loopback, private IPv4 (10/8, 172.16/12, 192.168/16),
* link-local (169.254/16, fe80::/10) or IPv6 unique-local (fc00::/7). An IPv4 client on a
Expand Down
Loading
Loading