Skip to content

Commit f3467e2

Browse files
Add PC Bluetooth close diagnostics (#2428)
- Add safe close reasons for live PC control sessions - Log GATT close reasons and unexpected Bluetooth disconnect status without payloads - Pass explicit reasons for pause timeout, reconnect cleanup, auth failure, command recovery, and connector shutdown - Update tests and fakes for reason-aware live connection cleanup ?? Auto-generated Co-authored-by: Owen McGirr <o.a.mcgirr@gmail.com>
1 parent 27eeb1e commit f3467e2

6 files changed

Lines changed: 45 additions & 23 deletions

File tree

app/src/main/java/com/enaboapps/switchify/pc/PcConnector.kt

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,17 @@ interface PcControlConnection {
4141
val connectionEvents: Flow<PcControlConnectionEvent>
4242
suspend fun checkHealth(): PcCommandResult
4343
suspend fun sendCommand(command: PcControlCommand): PcCommandResult
44-
fun close()
44+
fun close(reason: PcControlCloseReason = PcControlCloseReason.ExplicitStop)
45+
}
46+
47+
enum class PcControlCloseReason(val logName: String) {
48+
UiPauseGraceExpired("ui_pause_grace_expired"),
49+
ExplicitStop("explicit_stop"),
50+
CommandFailureRecovery("command_failure_recovery"),
51+
AuthFailure("auth_failure"),
52+
ConnectorShutdown("connector_shutdown"),
53+
Reconnect("reconnect"),
54+
UnexpectedDisconnect("unexpected_disconnect")
4555
}
4656

4757
sealed class PcControlConnectionEvent {

app/src/main/java/com/enaboapps/switchify/pc/bluetooth/PcBleGattClient.kt

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import android.bluetooth.BluetoothManager
1111
import android.content.Context
1212
import android.content.pm.PackageManager
1313
import android.os.Build
14+
import android.util.Log
1415
import androidx.core.content.ContextCompat
1516
import com.enaboapps.switchify.pc.PcBluetoothEndpoint
1617
import kotlinx.coroutines.CompletableDeferred
@@ -33,7 +34,7 @@ interface PcBleTransportConnection {
3334
val endpoint: PcBluetoothEndpoint
3435
val events: Flow<PcBleTransportEvent>
3536
suspend fun sendAndReceive(message: String, timeoutMs: Long): String
36-
fun close()
37+
fun close(reason: String = "client_close")
3738
}
3839

3940
sealed class PcBleTransportEvent {
@@ -65,9 +66,10 @@ private class PcBleGattConnection private constructor(
6566
return withTimeout(timeoutMs) { incomingMessages.receive() }
6667
}
6768

68-
override fun close() {
69+
override fun close(reason: String) {
6970
if (closed) return
7071
closed = true
72+
Log.d(TAG, "PC BLE GATT closing endpoint=${endpoint.deviceAddress} reason=$reason")
7173
onClose()
7274
runCatching { gatt.disconnect() }
7375
runCatching { gatt.close() }
@@ -110,8 +112,10 @@ private class PcBleGattConnection private constructor(
110112
private var pendingWrite: GattWriteRequest? = null
111113
private var setupComplete = false
112114
private var closedByClient = false
115+
private var deviceAddress = "unknown"
113116

114117
suspend fun awaitConnected(context: Context, endpoint: PcBluetoothEndpoint): PcBleGattConnection {
118+
deviceAddress = endpoint.deviceAddress
115119
withTimeout(GATT_CONNECT_TIMEOUT_MS) { connected.await() }
116120
@SuppressLint("MissingPermission")
117121
if (!gatt.discoverServices()) throw IllegalStateException("Could not discover Bluetooth services.")
@@ -163,6 +167,7 @@ private class PcBleGattConnection private constructor(
163167
} else if (newState == BluetoothGatt.STATE_DISCONNECTED) {
164168
if (!connected.isCompleted) connected.completeExceptionally(IllegalStateException("Bluetooth disconnected."))
165169
if (setupComplete && !closedByClient) {
170+
Log.d(TAG, "PC BLE GATT disconnected unexpectedly status=$status endpoint=$deviceAddress")
166171
events.tryEmit(PcBleTransportEvent.Disconnected)
167172
}
168173
incomingMessages.close()
@@ -262,3 +267,4 @@ internal fun Context.isBluetoothEnabled(): Boolean {
262267

263268
private const val GATT_CONNECT_TIMEOUT_MS = 10_000L
264269
private const val GATT_NOTIFY_TIMEOUT_MS = 5_000L
270+
private const val TAG = "PcBleGattClient"

app/src/main/java/com/enaboapps/switchify/pc/bluetooth/SwitchifyPcBleClient.kt

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import com.enaboapps.switchify.pc.PcBluetoothEndpoint
77
import com.enaboapps.switchify.pc.PcCommandResult
88
import com.enaboapps.switchify.pc.PcConnector
99
import com.enaboapps.switchify.pc.PcControlCommand
10+
import com.enaboapps.switchify.pc.PcControlCloseReason
1011
import com.enaboapps.switchify.pc.PcControlConnection
1112
import com.enaboapps.switchify.pc.PcControlConnectionEvent
1213
import com.enaboapps.switchify.pc.PcDeviceIdentity
@@ -118,13 +119,13 @@ class SwitchifyPcBleClient(
118119
)
119120
)
120121
is PcProtocolResponse.Error -> {
121-
connection.close()
122+
connection.close(PcControlCloseReason.AuthFailure.logName)
122123
openConnections -= connection
123124
if (response.code == "invalid_auth") PcLiveControlResult.AuthFailed()
124125
else PcLiveControlResult.Failed()
125126
}
126127
else -> {
127-
connection.close()
128+
connection.close(PcControlCloseReason.CommandFailureRecovery.logName)
128129
openConnections -= connection
129130
PcLiveControlResult.Failed()
130131
}
@@ -144,7 +145,7 @@ class SwitchifyPcBleClient(
144145
}
145146

146147
override fun close() {
147-
openConnections.toList().forEach { it.close() }
148+
openConnections.toList().forEach { it.close(PcControlCloseReason.ConnectorShutdown.logName) }
148149
openConnections.clear()
149150
}
150151

@@ -154,7 +155,7 @@ class SwitchifyPcBleClient(
154155
try {
155156
block(connection)
156157
} finally {
157-
connection.close()
158+
connection.close(PcControlCloseReason.ExplicitStop.logName)
158159
}
159160
}
160161
}
@@ -317,8 +318,8 @@ class SwitchifyPcBleClient(
317318
}
318319
}
319320

320-
override fun close() {
321-
connection.close()
321+
override fun close(reason: PcControlCloseReason) {
322+
connection.close(reason.logName)
322323
openConnections -= connection
323324
}
324325
}

app/src/main/java/com/enaboapps/switchify/screens/pc/PcMouseControlViewModel.kt

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import com.enaboapps.switchify.pc.PcConnectionStateHolder
99
import com.enaboapps.switchify.pc.PcConnector
1010
import com.enaboapps.switchify.pc.PcLiveControlResult
1111
import com.enaboapps.switchify.pc.PcControlConnection
12+
import com.enaboapps.switchify.pc.PcControlCloseReason
1213
import com.enaboapps.switchify.pc.PcControlConnectionEvent
1314
import com.enaboapps.switchify.pc.PcDeviceIdentityRepository
1415
import com.enaboapps.switchify.pc.PcKeyboardKey
@@ -233,6 +234,7 @@ class PcMouseControlViewModel(
233234
}
234235
is PcCommandResult.AuthFailed -> {
235236
tokenStore.clearToken(connected.session.desktopId)
237+
closeLiveConnection(PcControlCloseReason.AuthFailure)
236238
PcConnectionStateHolder.setDisconnected()
237239
_uiState.update {
238240
it.copy(
@@ -245,7 +247,7 @@ class PcMouseControlViewModel(
245247
}
246248
}
247249
is PcCommandResult.Failed -> {
248-
closeLiveConnection()
250+
closeLiveConnection(PcControlCloseReason.CommandFailureRecovery)
249251
_uiState.update {
250252
it.copy(
251253
isBusy = false,
@@ -274,7 +276,7 @@ class PcMouseControlViewModel(
274276
repeat(LIVE_COMMAND_ATTEMPTS) { attempt ->
275277
val result = sendWithCurrentOrNewConnection(session, command)
276278
if (result !is PcCommandResult.Failed) return result
277-
if (attempt < LIVE_COMMAND_ATTEMPTS - 1) closeLiveConnection()
279+
if (attempt < LIVE_COMMAND_ATTEMPTS - 1) closeLiveConnection(PcControlCloseReason.CommandFailureRecovery)
278280
}
279281
return PcCommandResult.Failed()
280282
}
@@ -322,17 +324,17 @@ class PcMouseControlViewModel(
322324
pendingUiPauseShutdownJob?.cancel()
323325
pendingUiPauseShutdownJob = viewModelScope.launch {
324326
delay(PC_CONTROL_UI_PAUSE_SHUTDOWN_GRACE_MS)
325-
stopPcBluetooth()
327+
stopPcBluetooth(PcControlCloseReason.UiPauseGraceExpired)
326328
}
327329
}
328330

329-
fun stopPcBluetooth() {
331+
fun stopPcBluetooth(reason: PcControlCloseReason = PcControlCloseReason.ExplicitStop) {
330332
pcUiActive = false
331333
pendingUiPauseShutdownJob?.cancel()
332334
pendingUiPauseShutdownJob = null
333335
reconnectJob?.cancel()
334336
reconnectJob = null
335-
closeLiveConnection()
337+
closeLiveConnection(reason)
336338
connector.close()
337339
PcConnectionStateHolder.setDisconnected()
338340
movementSteps = FALLBACK_MOVEMENT_STEPS
@@ -360,7 +362,7 @@ class PcMouseControlViewModel(
360362
return it
361363
}
362364

363-
closeLiveConnection()
365+
closeLiveConnection(PcControlCloseReason.Reconnect)
364366
liveSession = session
365367
return viewModelScope.async {
366368
when (val result = retryPcAuthFailure(
@@ -409,12 +411,12 @@ class PcMouseControlViewModel(
409411
}
410412
}
411413

412-
private fun closeLiveConnection() {
414+
private fun closeLiveConnection(reason: PcControlCloseReason = PcControlCloseReason.ExplicitStop) {
413415
liveConnectionEventsJob?.cancel()
414416
liveConnectionEventsJob = null
415417
liveHeartbeatJob?.cancel()
416418
liveHeartbeatJob = null
417-
liveConnection?.close()
419+
liveConnection?.close(reason)
418420
liveConnection = null
419421
liveSession = null
420422
liveConnectionDeferred?.cancel()
@@ -457,7 +459,7 @@ class PcMouseControlViewModel(
457459
busyCommand = null
458460
)
459461
}
460-
closeLiveConnection()
462+
closeLiveConnection(PcControlCloseReason.AuthFailure)
461463
return@launch
462464
}
463465
is PcCommandResult.Failed -> {
@@ -472,7 +474,7 @@ class PcMouseControlViewModel(
472474
private fun handleLiveConnectionFailed(session: PcAuthenticatedSession) {
473475
val displayName = lastDisplayName ?: _uiState.value.connectedDisplayName ?: "Switchify PC"
474476
viewModelScope.launch {
475-
closeLiveConnection()
477+
closeLiveConnection(PcControlCloseReason.UnexpectedDisconnect)
476478
if (pcUiActive || pendingUiPauseShutdownJob?.isActive == true) {
477479
reconnectSavedSession(session, displayName)
478480
}
@@ -494,7 +496,7 @@ class PcMouseControlViewModel(
494496
)
495497
}
496498
for ((index, backoffMs) in RECONNECT_BACKOFF_MS.withIndex()) {
497-
closeLiveConnection()
499+
closeLiveConnection(PcControlCloseReason.Reconnect)
498500
when (val result = connector.openControlSession(session)) {
499501
is PcLiveControlResult.Connected -> {
500502
liveConnection = result.connection
@@ -543,7 +545,7 @@ class PcMouseControlViewModel(
543545
}
544546
if (index < RECONNECT_BACKOFF_MS.lastIndex) delay(backoffMs)
545547
}
546-
closeLiveConnection()
548+
closeLiveConnection(PcControlCloseReason.CommandFailureRecovery)
547549
PcConnectionStateHolder.setFailed(DISCONNECTED_MESSAGE)
548550
_uiState.update {
549551
it.copy(

app/src/test/java/com/enaboapps/switchify/pc/bluetooth/SwitchifyPcBleClientTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,6 @@ class SwitchifyPcBleClientTest {
308308
return responseProvider(message)
309309
}
310310

311-
override fun close() = Unit
311+
override fun close(reason: String) = Unit
312312
}
313313
}

app/src/test/java/com/enaboapps/switchify/screens/pc/PcMouseControlViewModelTest.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import com.enaboapps.switchify.pc.PcCommandResult
66
import com.enaboapps.switchify.pc.PcConnectionState
77
import com.enaboapps.switchify.pc.PcConnectionStateHolder
88
import com.enaboapps.switchify.pc.PcConnector
9+
import com.enaboapps.switchify.pc.PcControlCloseReason
910
import com.enaboapps.switchify.pc.PcKeyboardKey
1011
import com.enaboapps.switchify.pc.PcLiveControlResult
1112
import com.enaboapps.switchify.pc.PcControlConnection
@@ -975,6 +976,7 @@ class PcMouseControlViewModelTest {
975976
override val connectionEvents = eventsFlow
976977
var healthChecks = 0
977978
var closeCalls = 0
979+
val closeReasons = mutableListOf<PcControlCloseReason>()
978980

979981
override suspend fun checkHealth(): PcCommandResult {
980982
healthChecks++
@@ -983,8 +985,9 @@ class PcMouseControlViewModelTest {
983985

984986
override suspend fun sendCommand(command: PcControlCommand): PcCommandResult = onCommand(command)
985987

986-
override fun close() {
988+
override fun close(reason: PcControlCloseReason) {
987989
closeCalls++
990+
closeReasons += reason
988991
}
989992
}
990993

0 commit comments

Comments
 (0)