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
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,8 @@ private void requestMode(Mode target) {

private void setMode(Mode m) {
mode = m;
apRetries = 0; // ADFA-5158: fresh AP-IP poll budget on a mode switch
netHandler.removeCallbacks(netRetry);
if (secJoin != null) secJoin.fbOpen = false; // ADFA-4815: each mode starts with ①'s fallback collapsed
if (m == Mode.HOTSPOT) ensureHotspot();
render(); // ADFA-4785: keep the current step; switching Hotspot/Wi-Fi no longer resets to step 1
Expand All @@ -433,6 +435,10 @@ private void setMode(Mode m) {

private void ensureHotspot() {
if (!LocalHotspotManager.isSupported() || hs.isOn()) return;
// ADFA-5158: renderPrepare runs on every render; don't re-request a start while one is in flight
// (the "Caller already has an active LocalOnlyHotspot request" log spam).
LocalHotspotManager.State st = hs.state().getValue();
if (st != null && st.phase == LocalHotspotManager.Phase.STARTING) return;
if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.ACCESS_FINE_LOCATION)
== PackageManager.PERMISSION_GRANTED) {
hs.start(requireContext().getApplicationContext());
Expand Down Expand Up @@ -713,14 +719,19 @@ private void renderPrepare() {
shareWifi.setVisibility(View.VISIBLE);
}

// ---- Section ② : Get the app ----
// ---- Section ② : Get the app ---- (own method so the AP-IP poll can redraw just this)
renderGetAppSection();
}

private void renderGetAppSection() {
if (mode == Mode.HOTSPOT) ensureHotspot();
startApkServer();
NetworkInterfaces.LanIps net = NetworkInterfaces.discover();
String appIp = (mode == Mode.HOTSPOT) ? net.hotspotIp : net.wifiIp;
String appIp = peerReachableIp();
if (appIp == null || apkServer == null) {
secGetApp.setQr(requireContext(), null, getString(R.string.k2go_clone_starting_service));
scheduleNetRetry(); // ADFA-5158: the AP IP lands with latency — poll and redraw when it does
} else {
apRetries = 0; // resolved — reset the poll budget
String url = "http://" + appIp + ":" + shareConfig.apkPort + "/" + apkFileName;
secGetApp.setQr(requireContext(), url, null);
}
Expand All @@ -736,14 +747,61 @@ private void renderPrepare() {
* point of no easy return: the confinement callback (armed at acceptance) keeps the user here.
*/
private void renderCopy() {
NetworkInterfaces.LanIps net = NetworkInterfaces.discover();
String ip = (mode == Mode.HOTSPOT) ? net.hotspotIp : net.wifiIp;
if (mode == Mode.HOTSPOT && ip == null) ip = "192.168.49.1";
if (ip == null) { simpleState(getString(R.string.k2go_connect_no_wifi), getString(R.string.k2go_connect_join_wifi)); return; }
String ip = peerReachableIp();
if (ip == null) {
// ADFA-5158: no IP to advertise. Wi-Fi -> genuinely no network. Hotspot -> the AP IP just
// hasn't been assigned yet; wait and poll instead of guessing a fixed address that isn't
// universal across OEMs.
if (mode == Mode.HOTSPOT) { simpleState(getString(R.string.k2go_clone_starting_service), ""); scheduleNetRetry(); }
else simpleState(getString(R.string.k2go_connect_no_wifi), getString(R.string.k2go_connect_join_wifi));
return;
}
apRetries = 0; // ADFA-5158: resolved — reset the poll budget
ensureDaemon(ip);
renderStartState(ip, mode == Mode.HOTSPOT);
}

/**
* ADFA-5158: the IP the other phone reaches this one at, per mode — one source for both the get-app
* URL and the Copy daemon (they had diverged: Copy hardcoded 192.168.49.1, get-app had no fallback).
* Returns null when the hotspot AP IP is not assigned yet; callers poll via {@link #scheduleNetRetry()}.
*/
private String peerReachableIp() {
NetworkInterfaces.LanIps net = NetworkInterfaces.discover();
return (mode == Mode.HOTSPOT) ? net.hotspotIp : net.wifiIp;
}

// ADFA-5158: while a Send page needs the AP IP and it isn't up yet, re-draw shortly so the QR appears
// the moment the interface gets its address — instead of waiting for an incidental render. Re-renders
// only the affected section, not the whole screen.
private final android.os.Handler netHandler = new android.os.Handler(android.os.Looper.getMainLooper());
private static final long AP_IP_POLL_MS = 1000L;
private static final int AP_IP_MAX_RETRIES = 120; // safety cap (~2 min) so the poll can't spin forever
private int apRetries = 0;
private final Runnable netRetry = new Runnable() {
@Override public void run() {
if (!isAdded() || atFork || side != Side.SEND) return; // left Send -> stop; no self-reschedule
if (page == Page.PREPARE) renderGetAppSection(); else renderCopy(); // reschedule only if still pending
}
};
/**
* Only worth polling when the AP IP can still plausibly arrive: hotspot mode, not FAILED/unsupported.
* Wi-Fi doesn't need it (the IP is present once joined; the network observer re-renders on join), and a
* failed hotspot won't ever provide one — so we don't spin (which also stopped ensureHotspot being
* re-requested every second on failure).
*/
private boolean apIpMayArrive() {
if (mode != Mode.HOTSPOT || !LocalHotspotManager.isSupported()) return false;
LocalHotspotManager.State st = hs.state().getValue();
return st == null || st.phase != LocalHotspotManager.Phase.FAILED;
}
private void scheduleNetRetry() {
if (!apIpMayArrive() || apRetries >= AP_IP_MAX_RETRIES) return;
apRetries++;
netHandler.removeCallbacks(netRetry);
netHandler.postDelayed(netRetry, AP_IP_POLL_MS);
}

/** Copy state: nothing-to-share -> starting -> stopped (Start sharing) -> running (QR + Stop). */
private void renderStartState(String ip, boolean twoCode) {
stepTitle.setVisibility(View.VISIBLE);
Expand Down Expand Up @@ -1335,6 +1393,7 @@ public void onDestroyView() {
exitHandler.removeCallbacks(exitTick);
exitHandler.removeCallbacks(exitPollRunnable);
if (exitDots != null) exitDots.stop();
netHandler.removeCallbacks(netRetry); // ADFA-5158: stop the AP-IP poll
// ADFA-4782: release protection only when nothing is running; an active share daemon or pull
// keeps the (app-scoped) CloneShareService alive so leaving the tab doesn't cut the transfer.
// ADFA-4956: same gate for the deep-env lock — only boot the server back + drop the lock when
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ public static LanIps discover() {
String hotspotIp = null;
try {
List<NetworkInterface> interfaces = Collections.list(NetworkInterface.getNetworkInterfaces());
// Pass 1: known interface names (precise when they match).
for (NetworkInterface intf : interfaces) {
String name = intf.getName();
if (!intf.isUp()) continue;
Expand All @@ -59,6 +60,26 @@ public static LanIps discover() {
}
}
}
// ADFA-5158: Pass 2 fallback — some OEMs name the LocalOnlyHotspot interface outside the
// whitelist above, so pass 1 misses it and the get-app QR never gets an IP. Take an up,
// non-loopback, site-local IPv4 that is not wlan0 and not the Wi-Fi address: that is the AP,
// whatever it is called. Site-local (10/172.16-31/192.168) excludes cellular/CGNAT.
if (hotspotIp == null) {
for (NetworkInterface intf : interfaces) {
String name = intf.getName();
// Skip wlan0 (Wi-Fi) and VPN/point-to-point ifaces, which can also carry a site-local
// IPv4 but are not the AP the other phone joins.
if (!intf.isUp() || name.equals("wlan0") || name.startsWith("tun") || name.startsWith("ppp")) continue;
for (InetAddress addr : Collections.list(intf.getInetAddresses())) {
if (addr.isLoopbackAddress() || !(addr instanceof Inet4Address)) continue;
if (addr.isSiteLocalAddress() && !addr.getHostAddress().equals(wifiIp)) {
hotspotIp = addr.getHostAddress();
break;
}
}
if (hotspotIp != null) break;
}
}
} catch (Exception ignored) {
}
return new LanIps(wifiIp, hotspotIp);
Expand Down
Loading