Merge pull request #35 from adrianoftyriel/fix/scorekeeper-join-diagnostics

Scorekeeper sharing: join from any sheet, show waiting requests, diagnostics
This commit is contained in:
adrianoftyriel
2026-10-09 21:29:09 -06:00
committed by GitHub
4 changed files with 36 additions and 4 deletions
@@ -135,11 +135,19 @@ class LanTransport(private val context: Context) : Transport {
awaitClose { awaitClose {
acceptJob.cancel() acceptJob.cancel()
stopAdvertising() // Only what this call set up. Hosting stopped and started again
// runs this for the old host after the new one has begun, and
// letting it clear the shared fields would unregister the new
// host's advertisement and blank the address it shows.
runCatching { nsdManager.unregisterService(listener) }
if (registrationListener === listener) registrationListener = null
runCatching { server.close() } runCatching { server.close() }
if (serverSocket === server) {
serverSocket = null
_endpoint.value = null _endpoint.value = null
} }
} }
}
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
// Discovery // Discovery
@@ -70,6 +70,13 @@ class ScorekeeperSession(
val discovered: List<DiscoveredHost> = emptyList(), val discovered: List<DiscoveredHost> = emptyList(),
/** Host: where this device is listening, for reading out. */ /** Host: where this device is listening, for reading out. */
val endpoint: HostEndpoint? = null, val endpoint: HostEndpoint? = null,
/**
* Host: links accepted, and hellos read off them. For telling "nobody
* reached this phone" from "they reached it and were not understood",
* which look the same from the other end.
*/
val connections: Int = 0,
val hellos: Int = 0,
val message: String? = null, val message: String? = null,
) )
@@ -142,6 +149,7 @@ class ScorekeeperSession(
} }
private fun listenToGuest(connection: Connection) { private fun listenToGuest(connection: Connection) {
_state.update { it.copy(connections = it.connections + 1) }
jobs += scope.launch { jobs += scope.launch {
connection.incoming.collect { message -> connection.incoming.collect { message ->
synchronized(lock) { onHostMessage(connection, message) } synchronized(lock) { onHostMessage(connection, message) }
@@ -192,6 +200,7 @@ class ScorekeeperSession(
} }
private fun onHello(connection: Connection, hello: Hello) { private fun onHello(connection: Connection, hello: Hello) {
_state.update { it.copy(hellos = it.hellos + 1) }
val refusal = when { val refusal = when {
hello.purpose != PURPOSE_SCOREKEEPER -> "That is a score sheet, not a game." hello.purpose != PURPOSE_SCOREKEEPER -> "That is a score sheet, not a game."
hello.protocol != PROTOCOL_VERSION -> "Different app version — update both devices." hello.protocol != PROTOCOL_VERSION -> "Different app version — update both devices."
@@ -377,6 +386,7 @@ class ScorekeeperSession(
link = connection link = connection
heardBack = false heardBack = false
replica.reset() replica.reset()
_state.update { it.copy(message = "Connected. Waiting for ${it.hostName} to answer…") }
} }
val hello = Hello(peerId = deviceId, displayName = joinedName, purpose = PURPOSE_SCOREKEEPER) val hello = Hello(peerId = deviceId, displayName = joinedName, purpose = PURPOSE_SCOREKEEPER)
linkJob = scope.launch { linkJob = scope.launch {
@@ -173,6 +173,9 @@ fun ScorekeeperScreen(
actions = { actions = {
when (role) { when (role) {
Role.NONE -> if (sheet.started && !joining) { Role.NONE -> if (sheet.started && !joining) {
// A sheet already on the go is no reason to hide the way to
// somebody else's.
TextButton(onClick = { joining = true }) { Text("Join") }
TextButton(onClick = { TextButton(onClick = {
session.startHosting(playerName, local) session.startHosting(playerName, local)
showShare = true showShare = true
@@ -182,7 +185,13 @@ fun ScorekeeperScreen(
Role.HOST -> { Role.HOST -> {
TextButton(onClick = { showShare = true }) { TextButton(onClick = { showShare = true }) {
Text(if (shared.guests.isEmpty()) "Sharing" else "Sharing (${shared.guests.size})") Text(
when {
shared.requests.isNotEmpty() -> "Sharing · ${shared.requests.size} waiting"
shared.guests.isNotEmpty() -> "Sharing (${shared.guests.size})"
else -> "Sharing"
}
)
} }
if (sheet.started) { if (sheet.started) {
TextButton(onClick = { confirmNewGame = true }) { Text("New game") } TextButton(onClick = { confirmNewGame = true }) { Text("New game") }
@@ -259,7 +268,8 @@ fun ScorekeeperScreen(
onDeny = { session.deny(request.id) }, onDeny = { session.deny(request.id) },
) )
} }
if (showShare) { // Not under a request: two dialogs stacked is how an answer gets missed.
if (showShare && shared.requests.isEmpty()) {
ShareDialog( ShareDialog(
shared = shared, shared = shared,
onRemove = { session.remove(it) }, onRemove = { session.remove(it) },
@@ -210,6 +210,10 @@ internal fun ShareDialog(
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
) )
} }
Text(
text = "Links received: ${shared.connections} · join requests: ${shared.hellos}",
style = MaterialTheme.typography.labelSmall,
)
Divider() Divider()
Text("On the sheet", fontWeight = FontWeight.Bold) Text("On the sheet", fontWeight = FontWeight.Bold)
if (shared.guests.isEmpty()) { if (shared.guests.isEmpty()) {