aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-10-19 21:47:34 +0200
committerChristophe Besson <cbesson@gmail.com>2026-10-19 21:47:34 +0200
commit89358ec1e8323d2ce11b4e10e84d0389e806b8dc (patch)
treed7a90e91f7c513a73be5a213325e50c506395cc7
parent5e1121fe4f7c2f512d80e71b0fa267bf907c50a1 (diff)
downloadmeshbay-89358ec1e8323d2ce11b4e10e84d0389e806b8dc.tar.gz
fix(android): cast from the Wi-Fi the phone is on, not its hotspotHEADmain
Android 17 lists the phone's own hotspot as a Wi-Fi network; taken first, the television was handed an address it cannot reach. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
-rw-r--r--packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/cast/CastChannels.kt27
-rw-r--r--packages/meshbay-android/app/src/test/kotlin/org/meshbay/client/CastLanTest.kt29
-rw-r--r--packages/meshbay-hub/tests/test_android_cast.py9
3 files changed, 61 insertions, 4 deletions
diff --git a/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/cast/CastChannels.kt b/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/cast/CastChannels.kt
index edaec67a..ced0ed3b 100644
--- a/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/cast/CastChannels.kt
+++ b/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/cast/CastChannels.kt
@@ -134,19 +134,38 @@ class CastChannels(
* The Wi-Fi (or Ethernet) address, never the mobile network's: a TV cannot
* be reached there. Nor a VPN's: a VPN network carries the transports of
* the network under it, Wi-Fi included, and its address is a tunnel the
- * television cannot route to.
+ * television cannot route to. Nor the phone's own hotspot when it is
+ * joined to a Wi-Fi: Android 17 lists the hotspot to applications as a
+ * Wi-Fi network of its own, local only, and taken first it handed the
+ * television an address on a network it is not on (`pickLan`).
*/
private fun lanAddress(): InetAddress? {
val cm = context.getSystemService(ConnectivityManager::class.java)
+ val active = cm.activeNetwork
+ val found = ArrayList<Lan>()
for (network in cm.allNetworks) {
val caps = cm.getNetworkCapabilities(network) ?: continue
if (caps.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) continue
if (!caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) &&
!caps.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) continue
val addr = cm.getLinkProperties(network)?.linkAddresses?.map { it.address }
- ?.firstOrNull { it is Inet4Address && !it.isLoopbackAddress }
- if (addr != null) return addr
+ ?.firstOrNull { it is Inet4Address && !it.isLoopbackAddress } ?: continue
+ found.add(Lan(addr, network == active,
+ caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)))
}
- return null
+ return pickLan(found)
+ }
+
+ /** A candidate network for the relay: its address, and what says it is the one the phone is on. */
+ class Lan(val address: InetAddress, val active: Boolean, val internet: Boolean)
+
+ companion object {
+ /**
+ * The network the phone uses, then one with Internet, then any: a
+ * home Wi-Fi without Internet still casts, a hotspot is the last resort.
+ */
+ fun pickLan(candidates: List<Lan>): InetAddress? =
+ (candidates.firstOrNull { it.active } ?: candidates.firstOrNull { it.internet }
+ ?: candidates.firstOrNull())?.address
}
}
diff --git a/packages/meshbay-android/app/src/test/kotlin/org/meshbay/client/CastLanTest.kt b/packages/meshbay-android/app/src/test/kotlin/org/meshbay/client/CastLanTest.kt
new file mode 100644
index 00000000..8bdeed15
--- /dev/null
+++ b/packages/meshbay-android/app/src/test/kotlin/org/meshbay/client/CastLanTest.kt
@@ -0,0 +1,29 @@
+package org.meshbay.client
+
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Test
+import org.meshbay.client.cast.CastChannels.Companion.pickLan
+import org.meshbay.client.cast.CastChannels.Lan
+import java.net.InetAddress
+
+/** Which address the relay is offered on, when the phone has more than one Wi-Fi. */
+class CastLanTest {
+ private val hotspot = Lan(InetAddress.getByName("10.105.183.74"), active = false, internet = false)
+ private val home = Lan(InetAddress.getByName("192.168.1.13"), active = true, internet = true)
+
+ @Test fun theWifiThePhoneIsOnWinsOverItsOwnHotspotListedFirst() =
+ assertEquals(home.address, pickLan(listOf(hotspot, home)))
+
+ @Test fun onMobileDataAWifiWithInternetWins() {
+ val wifi = Lan(InetAddress.getByName("192.168.1.13"), active = false, internet = true)
+ assertEquals(wifi.address, pickLan(listOf(hotspot, wifi)))
+ }
+
+ @Test fun aWifiWithoutInternetStillCasts() {
+ val offline = Lan(InetAddress.getByName("192.168.50.2"), active = false, internet = false)
+ assertEquals(offline.address, pickLan(listOf(offline)))
+ }
+
+ @Test fun noWifiNoRelay() = assertNull(pickLan(emptyList()))
+}
diff --git a/packages/meshbay-hub/tests/test_android_cast.py b/packages/meshbay-hub/tests/test_android_cast.py
index 82b36d61..da8b912b 100644
--- a/packages/meshbay-hub/tests/test_android_cast.py
+++ b/packages/meshbay-hub/tests/test_android_cast.py
@@ -93,6 +93,15 @@ def test_the_relay_is_on_wifi_never_the_mobile_network():
assert "TRANSPORT_CELLULAR" not in lan
+def test_the_relay_prefers_the_wifi_the_phone_is_on():
+ """Android 17 lists the phone's own hotspot as a Wi-Fi network; taken first,
+ the television was handed an address on a network it is not on."""
+ channels = _read(CAST / "CastChannels.kt")
+ lan = channels.split("private fun lanAddress()", 1)[1]
+ assert "activeNetwork" in lan and "NET_CAPABILITY_INTERNET" in lan
+ assert "return pickLan(found)" in lan
+
+
def test_the_receiver_is_pointed_at_the_relay_and_nothing_else():
channels = _read(CAST / "CastChannels.kt")
check = channels.split("private fun relayUrl(", 1)[1].split("\n }", 1)[0]