Skip to content

Commit e49eeb4

Browse files
Add default PC selection (#2588)
- Store an explicit default paired PC in the PC token store so the preference survives service and app restarts - Show Make default and Default states on paired nearby and saved PC rows - Prefer the discovered default PC from the Switchify menu while preserving chooser fallback behavior - Cover default row state, unpair cleanup, and main-menu selection rules in unit tests 🤖 Auto-generated Co-authored-by: Owen McGirr <o.a.mcgirr@gmail.com>
1 parent 01d9428 commit e49eeb4

11 files changed

Lines changed: 379 additions & 10 deletions

File tree

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

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,19 @@ data class PcRowState(
4242
val actionText: String,
4343
val enabled: Boolean,
4444
val status: PcRowStatus,
45-
val canUnpair: Boolean
45+
val canUnpair: Boolean,
46+
val canSetDefault: Boolean,
47+
val isDefault: Boolean
4648
)
4749

4850
data class PcSavedPairingRowState(
4951
val desktopId: String,
5052
val title: String,
5153
val summary: String,
5254
val canUnpair: Boolean = true,
53-
val canConnect: Boolean = false
55+
val canConnect: Boolean = false,
56+
val canSetDefault: Boolean = false,
57+
val isDefault: Boolean = false
5458
)
5559

5660
data class PcUnpairConfirmationState(
@@ -105,6 +109,9 @@ class PcConnectionViewModel(
105109
val savedPairings = withContext(backgroundDispatcher) {
106110
savedPairings(discoveredDesktopIds)
107111
}
112+
val defaultDesktopId = withContext(backgroundDispatcher) {
113+
tokenStore.getDefaultDesktopId()
114+
}
108115
_uiState.update { current ->
109116
current.copy(
110117
discoveryStatusText = discoveryStatusText(inputs.status, inputs.pcs.isEmpty()),
@@ -114,10 +121,13 @@ class PcConnectionViewModel(
114121
status = inputs.statuses[pc.desktopId] ?: PcRowStatus.Idle,
115122
connectedDesktopId = connectedDesktopId,
116123
hasToken = hasTokenByDesktopId[pc.desktopId] == true,
124+
defaultDesktopId = defaultDesktopId,
117125
isBusy = current.isBusy
118126
)
119127
},
120-
savedPairings = savedPairings,
128+
savedPairings = savedPairings.map { row ->
129+
row.copy(isDefault = row.desktopId == defaultDesktopId)
130+
},
121131
connectedDesktopId = connectedDesktopId,
122132
isDiscovering = inputs.status == PcDiscoveryStatus.Searching
123133
)
@@ -207,6 +217,20 @@ class PcConnectionViewModel(
207217
}
208218
}
209219

220+
fun setDefaultPc(desktopId: String, displayName: String) {
221+
if (tokenStore.getToken(desktopId).isNullOrBlank()) return
222+
tokenStore.setDefaultDesktopId(desktopId)
223+
tokenRevision.update { it + 1 }
224+
_uiState.update {
225+
it.copy(message = "Default PC set to $displayName.")
226+
}
227+
}
228+
229+
fun clearDefaultPc() {
230+
tokenStore.clearDefaultDesktopId()
231+
tokenRevision.update { it + 1 }
232+
}
233+
210234
fun stopPcBluetooth() {
211235
activeConnectionJob?.cancel()
212236
activeConnectionJob = null
@@ -233,6 +257,7 @@ class PcConnectionViewModel(
233257
status: PcRowStatus,
234258
connectedDesktopId: String?,
235259
hasToken: Boolean,
260+
defaultDesktopId: String?,
236261
isBusy: Boolean
237262
): PcRowState {
238263
val connected = connectedDesktopId == pc.desktopId || status == PcRowStatus.Connected
@@ -255,7 +280,9 @@ class PcConnectionViewModel(
255280
actionText = actionText,
256281
enabled = !connected && !isBusy,
257282
status = if (connected) PcRowStatus.Connected else status,
258-
canUnpair = hasToken || connected
283+
canUnpair = hasToken || connected,
284+
canSetDefault = hasToken,
285+
isDefault = pc.desktopId == defaultDesktopId
259286
)
260287
}
261288

@@ -267,7 +294,8 @@ class PcConnectionViewModel(
267294
desktopId = pairing.desktopId,
268295
title = pairing.serviceName ?: pairing.desktopId,
269296
summary = savedPairingSummary(pairing.lastEndpointId),
270-
canConnect = canConnectSavedPairing(pairing.lastEndpointId)
297+
canConnect = canConnectSavedPairing(pairing.lastEndpointId),
298+
canSetDefault = true
271299
)
272300
}
273301
}

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ interface PcPairingTokenStore {
1010
fun listPairings(): List<PcStoredPairing>
1111
fun getLastEndpointId(desktopId: String): String?
1212
fun getServiceName(desktopId: String): String?
13+
fun getDefaultDesktopId(): String?
14+
fun setDefaultDesktopId(desktopId: String)
15+
fun clearDefaultDesktopId()
1316
}
1417

1518
data class PcStoredPairing(
@@ -50,6 +53,9 @@ class PcTokenStore(context: Context) : PcPairingTokenStore {
5053
remove(tokenKey(desktopId))
5154
remove(lastEndpointIdKey(desktopId))
5255
remove(serviceNameKey(desktopId))
56+
if (preferences.getString(defaultDesktopIdKey, null) == desktopId) {
57+
remove(defaultDesktopIdKey)
58+
}
5359
}
5460
}
5561

@@ -74,6 +80,26 @@ class PcTokenStore(context: Context) : PcPairingTokenStore {
7480
return preferences.getString(serviceNameKey(desktopId), null)?.takeIf { it.isNotBlank() }
7581
}
7682

83+
override fun getDefaultDesktopId(): String? {
84+
val desktopId = preferences.getString(defaultDesktopIdKey, null)?.takeIf { it.isNotBlank() } ?: return null
85+
if (getToken(desktopId) != null) return desktopId
86+
clearDefaultDesktopId()
87+
return null
88+
}
89+
90+
override fun setDefaultDesktopId(desktopId: String) {
91+
if (getToken(desktopId) == null) return
92+
preferences.edit {
93+
putString(defaultDesktopIdKey, desktopId)
94+
}
95+
}
96+
97+
override fun clearDefaultDesktopId() {
98+
preferences.edit {
99+
remove(defaultDesktopIdKey)
100+
}
101+
}
102+
77103
private fun tokenKey(desktopId: String) = "token:$desktopId"
78104
private fun lastEndpointIdKey(desktopId: String) = "last_endpoint_id:$desktopId"
79105
private fun serviceNameKey(desktopId: String) = "service_name:$desktopId"
@@ -91,6 +117,7 @@ class PcTokenStore(context: Context) : PcPairingTokenStore {
91117

92118
private companion object {
93119
const val pairingIdsKey = "paired_desktop_ids"
120+
const val defaultDesktopIdKey = "default_desktop_id"
94121
const val tokenPrefix = "token:"
95122
}
96123
}

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

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,11 @@ fun PcConnectionScreen(navController: NavController) {
175175
Text(stringResource(R.string.pc_connection_connect))
176176
}
177177
}
178+
PcDefaultAction(
179+
isDefault = row.isDefault,
180+
canSetDefault = row.canSetDefault,
181+
onSetDefault = { viewModel.setDefaultPc(row.desktopId, row.title) }
182+
)
178183
TextButton(
179184
enabled = row.canUnpair,
180185
onClick = { viewModel.requestUnpair(row.desktopId, row.title) }
@@ -307,13 +312,37 @@ private fun PcNearbyRowActions(
307312
onClick = { row.perform(viewModel) }
308313
)
309314
if (row.canUnpair) {
315+
PcDefaultAction(
316+
isDefault = row.isDefault,
317+
canSetDefault = row.canSetDefault,
318+
onSetDefault = { viewModel.setDefaultPc(row.pc.desktopId, row.title) }
319+
)
310320
TextButton(onClick = { viewModel.requestUnpair(row.pc.desktopId, row.title) }) {
311321
Text(stringResource(R.string.pc_connection_unpair))
312322
}
313323
}
314324
}
315325
}
316326

327+
@Composable
328+
private fun PcDefaultAction(
329+
isDefault: Boolean,
330+
canSetDefault: Boolean,
331+
onSetDefault: () -> Unit
332+
) {
333+
when {
334+
isDefault -> Text(
335+
text = stringResource(R.string.pc_connection_default),
336+
style = MaterialTheme.typography.labelLarge,
337+
color = MaterialTheme.colorScheme.primary,
338+
modifier = Modifier.padding(horizontal = Dimens.spaceS)
339+
)
340+
canSetDefault -> TextButton(onClick = onSetDefault) {
341+
Text(stringResource(R.string.pc_connection_make_default))
342+
}
343+
}
344+
}
345+
317346
@Composable
318347
private fun PcApprovalCodeDialog(approvalCode: PcApprovalCodeState) {
319348
AlertDialog(

app/src/main/java/com/enaboapps/switchify/service/menu/menus/main/MainMenuStructure.kt

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import com.enaboapps.switchify.pc.DiscoveredPc
77
import com.enaboapps.switchify.pc.PcErrorReason
88
import com.enaboapps.switchify.pc.PcServiceConnectResult
99
import com.enaboapps.switchify.pc.PcServiceConnectionController
10+
import com.enaboapps.switchify.pc.PcTokenStore
1011
import com.enaboapps.switchify.service.actions.GlobalActionManager
1112
import com.enaboapps.switchify.service.core.ServiceCore
1213
import com.enaboapps.switchify.service.core.SwitchifyAccessibilityService
@@ -37,6 +38,7 @@ class MainMenuStructure(
3738
private val gestureMenuStructure = GestureMenuStructure(accessibilityService, coroutineScope)
3839
private val deviceLockObserver = DeviceLockObserver(accessibilityService)
3940
private val preferenceManager = PreferenceManager(accessibilityService)
41+
private val pcTokenStore = PcTokenStore(accessibilityService.applicationContext)
4042
private val repository = MenuConfigurationRepository(accessibilityService)
4143

4244
val deviceItem = MenuItem(
@@ -190,11 +192,12 @@ class MainMenuStructure(
190192
showMessage(R.string.pc_control_connecting, MessageSeverity.Info)
191193
coroutineScope.launch {
192194
val discovered = controller.discoverPairedPcs()
195+
val defaultDesktopId = pcTokenStore.getDefaultDesktopId()
193196
withContext(Dispatchers.Main) {
194-
when {
195-
discovered.isEmpty() -> showMessage(R.string.pc_control_no_pc_found, MessageSeverity.Warning)
196-
discovered.size == 1 -> connectToPcAndLaunch(controller, discovered.single())
197-
else -> MenuManager.getInstance().openChoosePcMenu(discovered) { pc ->
197+
when (val selection = selectPcForMainMenu(discovered, defaultDesktopId)) {
198+
PcMainMenuSelection.NoPcFound -> showMessage(R.string.pc_control_no_pc_found, MessageSeverity.Warning)
199+
is PcMainMenuSelection.Connect -> connectToPcAndLaunch(controller, selection.pc)
200+
is PcMainMenuSelection.ShowChooser -> MenuManager.getInstance().openChoosePcMenu(selection.pcs) { pc ->
198201
connectToPcAndLaunch(controller, pc)
199202
}
200203
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.enaboapps.switchify.service.menu.menus.main
2+
3+
import com.enaboapps.switchify.pc.DiscoveredPc
4+
5+
internal sealed class PcMainMenuSelection {
6+
data object NoPcFound : PcMainMenuSelection()
7+
data class Connect(val pc: DiscoveredPc) : PcMainMenuSelection()
8+
data class ShowChooser(val pcs: List<DiscoveredPc>) : PcMainMenuSelection()
9+
}
10+
11+
internal fun selectPcForMainMenu(
12+
discovered: List<DiscoveredPc>,
13+
defaultDesktopId: String?
14+
): PcMainMenuSelection {
15+
if (discovered.isEmpty()) return PcMainMenuSelection.NoPcFound
16+
val defaultPc = discovered.firstOrNull { it.desktopId == defaultDesktopId }
17+
if (defaultPc != null) return PcMainMenuSelection.Connect(defaultPc)
18+
if (discovered.size == 1) return PcMainMenuSelection.Connect(discovered.single())
19+
return PcMainMenuSelection.ShowChooser(discovered)
20+
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,9 @@
305305
<string name="pc_connection_paired_section">Paired PCs</string>
306306
<string name="pc_connection_connect">Connect</string>
307307
<string name="pc_connection_not_nearby">Not nearby</string>
308+
<string name="pc_connection_default">Default</string>
309+
<string name="pc_connection_make_default">Make default</string>
310+
<string name="pc_connection_default_set_message">Default PC set to %1$s.</string>
308311
<string name="pc_connection_unpair">Unpair</string>
309312
<string name="pc_connection_unpair_title">Unpair PC?</string>
310313
<string name="pc_connection_unpair_message">Switchify will forget %1$s. You will need to request access again before controlling this PC.</string>

0 commit comments

Comments
 (0)