Skip to content

Commit e327cf4

Browse files
committed
Add PC pairing cancellation
- Add cancellable pairing dialogs on Connect to PC and Switch PC flows - Cancel active pairing jobs and clear busy/switching UI state without replacing the current PC session - Cover cancelled pairing with ViewModel tests so late pairing completions do not save tokens or switch PCs 🤖 Auto-generated
1 parent 4bb5491 commit e327cf4

7 files changed

Lines changed: 187 additions & 8 deletions

File tree

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,28 @@ class PcConnectionViewModel(
235235
}
236236
}
237237

238+
fun cancelPairing() {
239+
activeConnectionJob?.cancel()
240+
activeConnectionJob = null
241+
connector.close()
242+
rowStatuses.update { statuses ->
243+
statuses.mapValues { (_, status) ->
244+
if (status == PcRowStatus.WaitingApproval || status == PcRowStatus.Connecting) {
245+
PcRowStatus.Idle
246+
} else {
247+
status
248+
}
249+
}
250+
}
251+
_uiState.update {
252+
it.copy(
253+
isBusy = false,
254+
approvalCode = null,
255+
message = null
256+
)
257+
}
258+
}
259+
238260
fun clearMessage() {
239261
_uiState.update { it.copy(message = null) }
240262
}

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

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,10 @@ fun PcConnectionScreen(navController: NavController) {
143143
}
144144

145145
uiState.approvalCode?.let { approvalCode ->
146-
PcApprovalCodeDialog(approvalCode)
146+
PcApprovalCodeDialog(
147+
approvalCode = approvalCode,
148+
onCancel = viewModel::cancelPairing
149+
)
147150
}
148151

149152
uiState.message?.let { message ->
@@ -618,10 +621,18 @@ private fun PcConnectionRowState.perform(viewModel: PcConnectionViewModel) {
618621
}
619622

620623
@Composable
621-
private fun PcApprovalCodeDialog(approvalCode: PcApprovalCodeState) {
624+
private fun PcApprovalCodeDialog(
625+
approvalCode: PcApprovalCodeState,
626+
onCancel: () -> Unit
627+
) {
622628
AlertDialog(
623-
onDismissRequest = {},
629+
onDismissRequest = onCancel,
624630
confirmButton = {},
631+
dismissButton = {
632+
TextButton(onClick = onCancel) {
633+
Text(stringResource(R.string.cancel))
634+
}
635+
},
625636
title = { Text(stringResource(R.string.pc_pairing_code_title)) },
626637
text = {
627638
Column(verticalArrangement = Arrangement.spacedBy(Dimens.spaceM)) {

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -218,10 +218,18 @@ private fun PcSwitchPcRow(
218218
}
219219

220220
@Composable
221-
fun PcSwitchPcApprovalDialog(approvalCode: com.enaboapps.switchify.pc.PcApprovalCodeState) {
221+
fun PcSwitchPcApprovalDialog(
222+
approvalCode: com.enaboapps.switchify.pc.PcApprovalCodeState,
223+
onCancel: () -> Unit
224+
) {
222225
AlertDialog(
223-
onDismissRequest = {},
226+
onDismissRequest = onCancel,
224227
confirmButton = {},
228+
dismissButton = {
229+
TextButton(onClick = onCancel) {
230+
Text(stringResource(R.string.cancel))
231+
}
232+
},
225233
title = { Text(stringResource(R.string.pc_pairing_code_title)) },
226234
text = {
227235
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,9 @@ private fun PcMouseControlScreen(
201201
}
202202

203203
uiState.switchPcApprovalCode?.let { approvalCode ->
204-
PcSwitchPcApprovalDialog(approvalCode)
204+
PcSwitchPcApprovalDialog(
205+
approvalCode = approvalCode,
206+
onCancel = viewModel::cancelSwitchPcPairing
207+
)
205208
}
206209
}

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

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import com.enaboapps.switchify.pc.supportsTextStreams
2323
import com.enaboapps.switchify.service.core.ServiceCore
2424
import java.util.UUID
2525
import kotlinx.coroutines.CoroutineStart
26+
import kotlinx.coroutines.Job
2627
import kotlinx.coroutines.delay
2728
import kotlinx.coroutines.flow.MutableStateFlow
2829
import kotlinx.coroutines.flow.StateFlow
@@ -87,6 +88,7 @@ class PcMouseControlViewModel(
8788
val uiState: StateFlow<PcMouseControlUiState> = _uiState.asStateFlow()
8889
private var movementSteps = FALLBACK_MOVEMENT_STEPS
8990
private var switchPcCandidates: List<DiscoveredPc> = emptyList()
91+
private var switchPcConnectionJob: Job? = null
9092

9193
init {
9294
val selectedSize = movementSizeStore.getSelectedSize()
@@ -317,6 +319,7 @@ class PcMouseControlViewModel(
317319
}
318320

319321
fun dismissSwitchPcChooser() {
322+
clearActiveSwitchPcConnection()
320323
_uiState.update {
321324
it.copy(
322325
switchPcChooserVisible = false,
@@ -379,6 +382,7 @@ class PcMouseControlViewModel(
379382
return
380383
}
381384
mouseRepeatManager.clearServiceState()
385+
switchPcConnectionJob?.cancel()
382386
_uiState.update {
383387
it.copy(
384388
isDragging = false,
@@ -388,11 +392,12 @@ class PcMouseControlViewModel(
388392
switchPcRows = switchRowsFor(switchPcCandidates, it.copy(switchingDesktopId = desktopId, isDragging = false))
389393
)
390394
}
391-
viewModelScope.launch {
395+
switchPcConnectionJob = viewModelScope.launch {
392396
when (val result = controller.connectTo(pc) { approvalCode ->
393397
_uiState.update { it.copy(switchPcApprovalCode = approvalCode) }
394398
}) {
395399
is PcServiceConnectResult.Connected -> {
400+
switchPcConnectionJob = null
396401
_uiState.update {
397402
it.copy(
398403
switchPcChooserVisible = false,
@@ -404,6 +409,7 @@ class PcMouseControlViewModel(
404409
}
405410
}
406411
is PcServiceConnectResult.Failed -> {
412+
switchPcConnectionJob = null
407413
_uiState.update {
408414
val next = it.copy(
409415
switchingDesktopId = null,
@@ -417,6 +423,23 @@ class PcMouseControlViewModel(
417423
}
418424
}
419425

426+
fun cancelSwitchPcPairing() {
427+
clearActiveSwitchPcConnection()
428+
_uiState.update {
429+
val next = it.copy(
430+
switchingDesktopId = null,
431+
switchPcApprovalCode = null,
432+
message = null
433+
)
434+
next.copy(switchPcRows = switchRowsFor(switchPcCandidates, next))
435+
}
436+
}
437+
438+
private fun clearActiveSwitchPcConnection() {
439+
switchPcConnectionJob?.cancel()
440+
switchPcConnectionJob = null
441+
}
442+
420443
fun updateTypingText(text: String) {
421444
_uiState.update {
422445
it.copy(
@@ -703,6 +726,7 @@ class PcMouseControlViewModel(
703726
}
704727

705728
override fun onCleared() {
729+
clearActiveSwitchPcConnection()
706730
mouseRepeatManager.clearServiceState()
707731
serviceControllerProvider()?.onPcUiPaused()
708732
super.onCleared()

app/src/test/java/com/enaboapps/switchify/pc/PcConnectionViewModelTest.kt

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,66 @@ class PcConnectionViewModelTest {
152152
advanceUntilIdle()
153153
}
154154

155+
@Test
156+
fun cancelPairingDismissesApprovalAndClearsBusyState() = runTest(dispatcher) {
157+
val pairingDeferred = CompletableDeferred<PcPairingResult>()
158+
val connector = FakeConnector(pairingDeferred = pairingDeferred)
159+
val viewModel = viewModel(FakeDiscovery(listOf(pc)), FakeTokenStore(), connector, requestNonceProvider = { "nonce-1" })
160+
advanceUntilIdle()
161+
162+
viewModel.requestAccess(pc)
163+
advanceUntilIdle()
164+
viewModel.cancelPairing()
165+
advanceUntilIdle()
166+
167+
val row = viewModel.uiState.value.pcRows.single()
168+
assertNull(viewModel.uiState.value.approvalCode)
169+
assertEquals(false, viewModel.uiState.value.isBusy)
170+
assertEquals(PcRowStatus.Idle, row.status)
171+
assertEquals(true, row.enabled)
172+
assertEquals(1, connector.closeCalls)
173+
}
174+
175+
@Test
176+
fun cancelPairingDoesNotSaveTokenWhenPairingCompletesLater() = runTest(dispatcher) {
177+
val pairingDeferred = CompletableDeferred<PcPairingResult>()
178+
val tokens = FakeTokenStore()
179+
val viewModel = viewModel(
180+
FakeDiscovery(listOf(pc)),
181+
tokens,
182+
FakeConnector(
183+
pairingDeferred = pairingDeferred,
184+
pingResult = PcPingResult.Connected("AA:BB:CC:DD:EE:FF")
185+
)
186+
)
187+
advanceUntilIdle()
188+
189+
viewModel.requestAccess(pc)
190+
advanceUntilIdle()
191+
viewModel.cancelPairing()
192+
pairingDeferred.complete(PcPairingResult.Paired("desktop-1", "token", "AA:BB:CC:DD:EE:FF"))
193+
advanceUntilIdle()
194+
195+
assertNull(tokens.getToken("desktop-1"))
196+
assertNull(tokens.getLastConnectedDesktopId())
197+
assertTrue(PcConnectionStateHolder.connectionState.value !is PcConnectionState.Connected)
198+
}
199+
200+
@Test
201+
fun cancelPairingIsNoOpWithoutActivePairing() = runTest(dispatcher) {
202+
val connector = FakeConnector()
203+
val viewModel = viewModel(FakeDiscovery(listOf(pc)), FakeTokenStore(), connector)
204+
advanceUntilIdle()
205+
206+
viewModel.cancelPairing()
207+
advanceUntilIdle()
208+
209+
assertNull(viewModel.uiState.value.approvalCode)
210+
assertEquals(false, viewModel.uiState.value.isBusy)
211+
assertEquals(PcRowStatus.Idle, viewModel.uiState.value.pcRows.single().status)
212+
assertEquals(1, connector.closeCalls)
213+
}
214+
155215
@Test
156216
fun stopPcBluetoothStopsDiscoveryAndClosesConnector() = runTest(dispatcher) {
157217
val discovery = FakeDiscovery(listOf(pc))

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

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ class PcMouseControlViewModelTest {
142142

143143
advanceUntilIdle()
144144

145-
assertEquals("Switchify PC", viewModel.uiState.value.connectedDisplayName)
145+
assertEquals("Oliver Laptop", viewModel.uiState.value.connectedDisplayName)
146146
assertEquals("Oliver Laptop", viewModel.uiState.value.switcherConnectedDisplayName)
147147
}
148148

@@ -314,6 +314,32 @@ class PcMouseControlViewModelTest {
314314
advanceUntilIdle()
315315
}
316316

317+
@Test
318+
fun cancelSwitchPcPairingDismissesApprovalAndClearsSwitchingState() = runTest(dispatcher) {
319+
val pairingDeferred = CompletableDeferred<PcPairingResult>()
320+
val connector = FakeConnector(
321+
liveResults = mutableListOf(PcLiveControlResult.Connected(FakeLiveConnection())),
322+
pingResultsByDesktop = mapOf("desktop-2" to PcPingResult.AuthFailed()),
323+
pairingResultsByDesktop = mapOf("desktop-2" to pairingDeferred)
324+
)
325+
val controller = pairedController(connector)
326+
val viewModel = viewModel(controller)
327+
advanceUntilIdle()
328+
329+
viewModel.openSwitchPcChooser()
330+
advanceUntilIdle()
331+
viewModel.switchToPc("desktop-2")
332+
runCurrent()
333+
viewModel.cancelSwitchPcPairing()
334+
advanceUntilIdle()
335+
336+
assertNull(viewModel.uiState.value.switchPcApprovalCode)
337+
assertNull(viewModel.uiState.value.switchingDesktopId)
338+
assertTrue(viewModel.uiState.value.switchPcChooserVisible)
339+
assertEquals("Switchify PC", viewModel.uiState.value.connectedDisplayName)
340+
assertTrue(viewModel.uiState.value.switchPcRows.first { it.desktopId == "desktop-2" }.enabled)
341+
}
342+
317343
@Test
318344
fun dismissSwitchPcChooserClearsSwitchState() = runTest(dispatcher) {
319345
val pairingDeferred = CompletableDeferred<PcPairingResult>()
@@ -340,6 +366,31 @@ class PcMouseControlViewModelTest {
340366
advanceUntilIdle()
341367
}
342368

369+
@Test
370+
fun cancelSwitchPcPairingDoesNotReplaceCurrentConnectionWhenPairingCompletesLater() = runTest(dispatcher) {
371+
val pairingDeferred = CompletableDeferred<PcPairingResult>()
372+
val connector = FakeConnector(
373+
liveResults = mutableListOf(PcLiveControlResult.Connected(FakeLiveConnection())),
374+
pingResultsByDesktop = mapOf("desktop-2" to PcPingResult.AuthFailed()),
375+
pairingResultsByDesktop = mapOf("desktop-2" to pairingDeferred)
376+
)
377+
val controller = pairedController(connector)
378+
val viewModel = viewModel(controller)
379+
advanceUntilIdle()
380+
381+
viewModel.openSwitchPcChooser()
382+
advanceUntilIdle()
383+
viewModel.switchToPc("desktop-2")
384+
runCurrent()
385+
viewModel.cancelSwitchPcPairing()
386+
pairingDeferred.complete(PcPairingResult.Paired("desktop-2", "new-token", "11:22:33:44:55:66"))
387+
advanceUntilIdle()
388+
389+
assertEquals("Switchify PC", viewModel.uiState.value.connectedDisplayName)
390+
assertNull(viewModel.uiState.value.switchingDesktopId)
391+
assertNull(viewModel.uiState.value.switchPcApprovalCode)
392+
}
393+
343394
@Test
344395
fun firstRunDefaultsToSmallFallbackStep() = runTest(dispatcher) {
345396
val viewModel = viewModel(null)

0 commit comments

Comments
 (0)