diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-10-19 21:47:34 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-10-19 21:47:34 +0200 |
| commit | 89358ec1e8323d2ce11b4e10e84d0389e806b8dc (patch) | |
| tree | d7a90e91f7c513a73be5a213325e50c506395cc7 /packages | |
| parent | 5e1121fe4f7c2f512d80e71b0fa267bf907c50a1 (diff) | |
| download | meshbay-89358ec1e8323d2ce11b4e10e84d0389e806b8dc.tar.gz | |
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>
Diffstat (limited to 'packages')
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] |