Skip to content

Commit 7620002

Browse files
committed
Add Android PC unpairing
- Add saved PC pairing indexing with legacy token discovery - Show unpair actions for nearby and offline saved PCs - Clear saved tokens and disconnect active sessions on confirmation - Cover saved pairing and unpair flows in unit tests 🤖 Auto-generated
1 parent 6670b8c commit 7620002

7 files changed

Lines changed: 272 additions & 12 deletions

File tree

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

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,13 @@ data class PcConnectionUiState(
1919
val permissionRequired: Boolean = false,
2020
val discoveryStatusText: String = "Searching for Switchify PC...",
2121
val discoveredPcs: List<PcRowState> = emptyList(),
22+
val savedPairings: List<PcSavedPairingRowState> = emptyList(),
2223
val connectedDesktopId: String? = null,
2324
val isDiscovering: Boolean = false,
2425
val isBusy: Boolean = false,
2526
val message: String? = null,
26-
val approvalCode: PcApprovalCodeState? = null
27+
val approvalCode: PcApprovalCodeState? = null,
28+
val pendingUnpair: PcUnpairConfirmationState? = null
2729
)
2830

2931
data class PcRowState(
@@ -32,7 +34,20 @@ data class PcRowState(
3234
val summary: String,
3335
val actionText: String,
3436
val enabled: Boolean,
35-
val status: PcRowStatus
37+
val status: PcRowStatus,
38+
val canUnpair: Boolean
39+
)
40+
41+
data class PcSavedPairingRowState(
42+
val desktopId: String,
43+
val title: String,
44+
val summary: String,
45+
val canUnpair: Boolean = true
46+
)
47+
48+
data class PcUnpairConfirmationState(
49+
val desktopId: String,
50+
val displayName: String
3651
)
3752

3853
enum class PcRowStatus {
@@ -60,9 +75,11 @@ class PcConnectionViewModel(
6075
viewModelScope.launch {
6176
combine(discoveryService.pcs, discoveryService.status, rowStatuses, PcConnectionStateHolder.connectionState) { pcs, status, statuses, connection ->
6277
val connectedDesktopId = (connection as? PcConnectionState.Connected)?.session?.desktopId
78+
val discoveredDesktopIds = pcs.map { it.desktopId }.toSet()
6379
_uiState.value.copy(
6480
discoveryStatusText = discoveryStatusText(status, pcs.isEmpty()),
6581
discoveredPcs = pcs.map { pc -> rowState(pc, statuses[pc.desktopId] ?: PcRowStatus.Idle, connectedDesktopId) },
82+
savedPairings = savedPairings(discoveredDesktopIds),
6683
connectedDesktopId = connectedDesktopId,
6784
isDiscovering = status == PcDiscoveryStatus.Searching
6885
)
@@ -148,6 +165,30 @@ class PcConnectionViewModel(
148165
_uiState.value = _uiState.value.copy(message = null)
149166
}
150167

168+
fun requestUnpair(desktopId: String, displayName: String) {
169+
_uiState.value = _uiState.value.copy(
170+
pendingUnpair = PcUnpairConfirmationState(desktopId, displayName)
171+
)
172+
}
173+
174+
fun dismissUnpair() {
175+
_uiState.value = _uiState.value.copy(pendingUnpair = null)
176+
}
177+
178+
fun confirmUnpair() {
179+
val unpair = _uiState.value.pendingUnpair ?: return
180+
tokenStore.clearToken(unpair.desktopId)
181+
val connected = PcConnectionStateHolder.connectionState.value as? PcConnectionState.Connected
182+
if (connected?.session?.desktopId == unpair.desktopId) {
183+
PcConnectionStateHolder.setDisconnected()
184+
}
185+
rowStatuses.value = rowStatuses.value - unpair.desktopId
186+
_uiState.value = _uiState.value.copy(
187+
message = "Unpaired from ${unpair.displayName}.",
188+
pendingUnpair = null
189+
)
190+
}
191+
151192
override fun onCleared() {
152193
discoveryService.stopDiscovery()
153194
connector.close()
@@ -175,10 +216,23 @@ class PcConnectionViewModel(
175216
summary = summary,
176217
actionText = actionText,
177218
enabled = !connected && !_uiState.value.isBusy,
178-
status = if (connected) PcRowStatus.Connected else status
219+
status = if (connected) PcRowStatus.Connected else status,
220+
canUnpair = hasToken || connected
179221
)
180222
}
181223

224+
private fun savedPairings(discoveredDesktopIds: Set<String>): List<PcSavedPairingRowState> {
225+
return tokenStore.listPairings()
226+
.filterNot { it.desktopId in discoveredDesktopIds }
227+
.map { pairing ->
228+
PcSavedPairingRowState(
229+
desktopId = pairing.desktopId,
230+
title = pairing.serviceName ?: pairing.desktopId,
231+
summary = pairing.lastUrl ?: "Not nearby"
232+
)
233+
}
234+
}
235+
182236
private fun setBusy(
183237
desktopId: String,
184238
status: PcRowStatus,

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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,17 @@ interface PcPairingTokenStore {
77
fun getToken(desktopId: String): String?
88
fun saveToken(desktopId: String, token: String, lastUrl: String, serviceName: String? = null)
99
fun clearToken(desktopId: String)
10+
fun listPairings(): List<PcStoredPairing>
1011
fun getLastUrl(desktopId: String): String?
1112
fun getServiceName(desktopId: String): String?
1213
}
1314

15+
data class PcStoredPairing(
16+
val desktopId: String,
17+
val serviceName: String?,
18+
val lastUrl: String?
19+
)
20+
1421
class PcTokenStore(context: Context) : PcPairingTokenStore {
1522
private val preferences = context
1623
.applicationContext
@@ -23,6 +30,7 @@ class PcTokenStore(context: Context) : PcPairingTokenStore {
2330

2431
override fun saveToken(desktopId: String, token: String, lastUrl: String, serviceName: String?) {
2532
preferences.edit {
33+
putStringSet(pairingIdsKey, pairingIds() + desktopId)
2634
putString(tokenKey(desktopId), token)
2735
putString(lastUrlKey(desktopId), lastUrl)
2836
if (!serviceName.isNullOrBlank()) putString(serviceNameKey(desktopId), serviceName)
@@ -31,12 +39,26 @@ class PcTokenStore(context: Context) : PcPairingTokenStore {
3139

3240
override fun clearToken(desktopId: String) {
3341
preferences.edit {
42+
putStringSet(pairingIdsKey, indexedPairingIds() - desktopId)
3443
remove(tokenKey(desktopId))
3544
remove(lastUrlKey(desktopId))
3645
remove(serviceNameKey(desktopId))
3746
}
3847
}
3948

49+
override fun listPairings(): List<PcStoredPairing> {
50+
return pairingIds()
51+
.filter { getToken(it) != null }
52+
.map { desktopId ->
53+
PcStoredPairing(
54+
desktopId = desktopId,
55+
serviceName = getServiceName(desktopId),
56+
lastUrl = getLastUrl(desktopId)
57+
)
58+
}
59+
.sortedBy { it.serviceName ?: it.desktopId }
60+
}
61+
4062
override fun getLastUrl(desktopId: String): String? {
4163
return preferences.getString(lastUrlKey(desktopId), null)?.takeIf { it.isNotBlank() }
4264
}
@@ -48,4 +70,20 @@ class PcTokenStore(context: Context) : PcPairingTokenStore {
4870
private fun tokenKey(desktopId: String) = "token:$desktopId"
4971
private fun lastUrlKey(desktopId: String) = "last_url:$desktopId"
5072
private fun serviceNameKey(desktopId: String) = "service_name:$desktopId"
73+
74+
private fun pairingIds(): Set<String> {
75+
val legacyIds = preferences.all.keys.mapNotNull { key ->
76+
key.removePrefix(tokenPrefix).takeIf { key.startsWith(tokenPrefix) && it.isNotBlank() }
77+
}
78+
return indexedPairingIds() + legacyIds
79+
}
80+
81+
private fun indexedPairingIds(): Set<String> {
82+
return preferences.getStringSet(pairingIdsKey, emptySet()).orEmpty()
83+
}
84+
85+
private companion object {
86+
const val pairingIdsKey = "paired_desktop_ids"
87+
const val tokenPrefix = "token:"
88+
}
5189
}

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

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.enaboapps.switchify.screens.pc
22

33
import androidx.compose.foundation.layout.Arrangement
44
import androidx.compose.foundation.layout.Column
5+
import androidx.compose.foundation.layout.Row
56
import androidx.compose.foundation.layout.fillMaxWidth
67
import androidx.compose.foundation.layout.padding
78
import androidx.compose.material3.AlertDialog
@@ -63,17 +64,45 @@ fun PcConnectionScreen(navController: NavController) {
6364
runtimeSummary = row.summary,
6465
onClick = { row.perform(viewModel) },
6566
trailing = {
66-
PcRowActionButton(
67-
text = row.actionText,
68-
enabled = row.enabled,
69-
connected = row.status == PcRowStatus.Connected,
70-
onClick = { row.perform(viewModel) }
71-
)
67+
Row(horizontalArrangement = Arrangement.spacedBy(Dimens.spaceS)) {
68+
PcRowActionButton(
69+
text = row.actionText,
70+
enabled = row.enabled,
71+
connected = row.status == PcRowStatus.Connected,
72+
onClick = { row.perform(viewModel) }
73+
)
74+
if (row.canUnpair) {
75+
TextButton(onClick = { viewModel.requestUnpair(row.pc.desktopId, row.title) }) {
76+
Text(stringResource(R.string.pc_connection_unpair))
77+
}
78+
}
79+
}
7280
}
7381
)
7482
}
7583
}
7684
}
85+
if (uiState.savedPairings.isNotEmpty()) {
86+
Section(titleResId = R.string.pc_connection_paired_section) {
87+
Column(modifier = Modifier.padding(vertical = Dimens.spaceS)) {
88+
uiState.savedPairings.forEach { row ->
89+
PanelListRow(
90+
runtimeTitle = row.title,
91+
runtimeSummary = row.summary,
92+
onClick = { viewModel.requestUnpair(row.desktopId, row.title) },
93+
trailing = {
94+
TextButton(
95+
enabled = row.canUnpair,
96+
onClick = { viewModel.requestUnpair(row.desktopId, row.title) }
97+
) {
98+
Text(stringResource(R.string.pc_connection_unpair))
99+
}
100+
}
101+
)
102+
}
103+
}
104+
}
105+
}
77106
}
78107
}
79108
}
@@ -94,6 +123,26 @@ fun PcConnectionScreen(navController: NavController) {
94123
text = { Text(message) }
95124
)
96125
}
126+
127+
uiState.pendingUnpair?.let { pendingUnpair ->
128+
AlertDialog(
129+
onDismissRequest = viewModel::dismissUnpair,
130+
confirmButton = {
131+
TextButton(onClick = viewModel::confirmUnpair) {
132+
Text(stringResource(R.string.pc_connection_unpair))
133+
}
134+
},
135+
dismissButton = {
136+
TextButton(onClick = viewModel::dismissUnpair) {
137+
Text(stringResource(R.string.cancel))
138+
}
139+
},
140+
title = { Text(stringResource(R.string.pc_connection_unpair_title)) },
141+
text = {
142+
Text(stringResource(R.string.pc_connection_unpair_message, pendingUnpair.displayName))
143+
}
144+
)
145+
}
97146
}
98147

99148
@Composable

app/src/main/res/values/strings.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,12 @@
263263
<string name="pc_connection_permission_message">Allow local network access to find Switchify PC.</string>
264264
<string name="pc_connection_permission_action">Allow local network access</string>
265265
<string name="pc_connection_nearby_section">Nearby PCs</string>
266+
<string name="pc_connection_paired_section">Paired PCs</string>
267+
<string name="pc_connection_not_nearby">Not nearby</string>
268+
<string name="pc_connection_unpair">Unpair</string>
269+
<string name="pc_connection_unpair_title">Unpair PC?</string>
270+
<string name="pc_connection_unpair_message">Switchify will forget %1$s. You will need to request access again before controlling this PC.</string>
271+
<string name="pc_connection_unpaired_message">Unpaired from %1$s.</string>
266272
<string name="pc_connection_message_title">PC connection</string>
267273
<string name="pc_pairing_code_title">Check your PC</string>
268274
<string name="pc_pairing_code_pc_name">Connecting to %1$s</string>

0 commit comments

Comments
 (0)