Make LAN play work over a phone's hotspot
Two phones on one phone's hotspot could not find or reach each other. Nothing was wrong with the Wi-Fi. There were three faults, and the first one is the whole bug. A hotspot has no internet behind it, and Android takes that literally: the Wi-Fi network fails its connectivity check, stays unvalidated, and mobile data remains the *default* network. Every socket opened without naming a network is therefore routed out over cellular — where 192.168.43.1 means nothing. The join socket is now bound to the Wi-Fi network before it connects, and the search for that network deliberately does not ask whether it has internet, because asking is what breaks it. Second, the manifest has asked for CHANGE_WIFI_MULTICAST_STATE since the beginning, with a comment saying discovery needs it, and nothing ever took the lock. Without it the radio drops multicast frames not addressed to this device before the system sees them, and mDNS is multicast. Both advertising and discovering now hold it. Third, and not fixable: a phone sharing its own hotspot does not reliably advertise over the tethering interface, whatever the other end does. So the host now shows where it is and the joiner can type it in. That path is offered always rather than kept back for when discovery has failed, because discovery here does not fail loudly — it just never finds anything, and a screen saying only "no games found" leaves nowhere to go. Hosts take a known port when they can get one, so what gets typed is an address and not an address and five more digits. The lobby scrolls instead of filling. Its lazy lists took the whole screen for a handful of rows and pushed the Start button off the bottom; they would have done the same to the address box. Parsing what somebody types off another phone's screen is the one pure piece of this and is tested, including that what a host displays is exactly what the other end accepts. The rest needs two phones and a hotspot, which CI does not have. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_0132V9gE6DF9itz2z3Hn7BK9
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
package org.prolibertate.games.net
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
* Reading an address somebody has typed off another phone's screen.
|
||||
*
|
||||
* This is the way into a game when discovery cannot work, which over a phone's
|
||||
* own hotspot is the normal case — so it has to accept what a person will
|
||||
* actually type, including the trailing space the keyboard adds and the port
|
||||
* they will usually leave off.
|
||||
*/
|
||||
class ManualAddressTest {
|
||||
|
||||
@Test
|
||||
fun `a bare address gets the usual port`() {
|
||||
assertEquals(
|
||||
ManualAddress("192.168.43.1", DEFAULT_HOST_PORT),
|
||||
parseManualAddress("192.168.43.1"),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a port after a colon is used instead`() {
|
||||
assertEquals(
|
||||
ManualAddress("192.168.43.1", 50000),
|
||||
parseManualAddress("192.168.43.1:50000"),
|
||||
)
|
||||
}
|
||||
|
||||
/** Typing on a phone puts spaces in places nobody intended. */
|
||||
@Test
|
||||
fun `surrounding space is ignored`() {
|
||||
assertEquals(
|
||||
ManualAddress("192.168.1.20", DEFAULT_HOST_PORT),
|
||||
parseManualAddress(" 192.168.1.20 "),
|
||||
)
|
||||
assertEquals(
|
||||
ManualAddress("192.168.1.20", 47654),
|
||||
parseManualAddress(" 192.168.1.20 : 47654 "),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a hostname works as well as an address`() {
|
||||
assertEquals(
|
||||
ManualAddress("pixel-9.local", DEFAULT_HOST_PORT),
|
||||
parseManualAddress("pixel-9.local"),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `nothing typed is nothing to connect to`() {
|
||||
assertNull(parseManualAddress(""))
|
||||
assertNull(parseManualAddress(" "))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a port that is not a number is refused rather than guessed`() {
|
||||
assertNull(parseManualAddress("192.168.43.1:door"))
|
||||
assertNull(parseManualAddress("192.168.43.1:"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a port outside the range is refused`() {
|
||||
assertNull(parseManualAddress("192.168.43.1:0"))
|
||||
assertNull(parseManualAddress("192.168.43.1:70000"))
|
||||
assertNull(parseManualAddress("192.168.43.1:-1"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `an address with a hole in it is refused`() {
|
||||
assertNull(parseManualAddress(":47654"))
|
||||
assertNull(parseManualAddress("192.168.43.1 20:47654"))
|
||||
}
|
||||
|
||||
/**
|
||||
* The default is only a default. A host that found the usual port taken
|
||||
* shows a different one, and that is what gets typed.
|
||||
*/
|
||||
@Test
|
||||
fun `the default port is only used when none is given`() {
|
||||
assertEquals(DEFAULT_HOST_PORT, parseManualAddress("10.0.0.5")?.port)
|
||||
assertEquals(1234, parseManualAddress("10.0.0.5:1234")?.port)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `an endpoint reads out as address and port`() {
|
||||
val endpoint = HostEndpoint(listOf("192.168.43.1", "10.0.0.5"), DEFAULT_HOST_PORT)
|
||||
assertEquals("192.168.43.1:$DEFAULT_HOST_PORT", endpoint.primary)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `an endpoint with no addresses has nothing to read out`() {
|
||||
assertNull(HostEndpoint(emptyList(), DEFAULT_HOST_PORT).primary)
|
||||
}
|
||||
|
||||
/** What the host shows must be what the joiner can type. */
|
||||
@Test
|
||||
fun `what a host shows is what the other end accepts`() {
|
||||
val shown = HostEndpoint(listOf("192.168.43.1"), 47654).primary!!
|
||||
assertEquals(ManualAddress("192.168.43.1", 47654), parseManualAddress(shown))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user