e7a64bcf8792
feat: Rename paired devices locally (v2.14.16)
- time
- 2026-08-11 20:56 UTC · 2 months ago
- machine
- koh2
- agent
- gpt-5.6-sol
- session
- paired device aliases
- parent
- 90fb2daaf151
↓ peach-android-e7a64bcf.tar.gzatlas at this save
files changed6 files
VERSION+1 −1app/src/main/java/com/peachpasswords/sync/PairedDevice.kt+1 −0app/src/main/java/com/peachpasswords/sync/PairedDeviceStore.kt+25 −2app/src/main/java/com/peachpasswords/ui/settings/SettingsCardBodies.kt+36 −1app/src/main/java/com/peachpasswords/ui/sync/SyncViewModel.kt+4 −0app/src/test/java/com/peachpasswords/sync/PairedDeviceAliasTest.kt+36 −0app/src/main/java/com/peachpasswords/sync/PairedDevice.kt+1 −0
@@ -6,6 +6,7 @@66 data class PairedDevice(77 val deviceId: String,88 val deviceName: String? = null,9+ val localAlias: String? = null,910 val deviceType: String? = null,1011 val longTermPublicKeyHex: String? = null,1112 val signingPublicKeyHex: String? = null,app/src/main/java/com/peachpasswords/sync/PairedDeviceStore.kt+25 −2
@@ -69,11 +69,18 @@6969 override fun upsert(device: PairedDevice) {7070 // Peer-supplied names arrive capped so a hostile peer cannot grow the7171 // registry without limit (adoption + direct-pair seams land here).72- val device = device.copy(deviceName = device.deviceName?.let(::capDeviceName))72+ val device = device.copy(73+ deviceName = device.deviceName?.let(::capDeviceName),74+ localAlias = get(device.deviceId)?.localAlias,75+ )7376 val next = getAll().filterNot { it.deviceId == device.deviceId } + device7477 persist(next)7578 }7679 80+ fun rename(deviceId: String, name: String) {81+ persist(withLocalAlias(getAll(), deviceId, name))82+ }83+7784 override fun remove(deviceId: String) {7885 persist(getAll().filterNot { it.deviceId == deviceId })7986 }@@ -97,7 +104,11 @@97104 .filterNot { removed.contains(it.deviceId) }98105 .toMutableList()99106 batch.upserts.forEach { device ->100- val device = device.copy(deviceName = device.deviceName?.let(::capDeviceName))107+ val alias = nextDevices.firstOrNull { it.deviceId == device.deviceId }?.localAlias108+ val device = device.copy(109+ deviceName = device.deviceName?.let(::capDeviceName),110+ localAlias = alias,111+ )101112 nextDevices.removeAll { it.deviceId == device.deviceId }102113 nextDevices += device103114 }@@ -258,6 +269,18 @@258269 const val MAX_REVOCATIONS = 500259270 val json = Json { encodeDefaults = true; ignoreUnknownKeys = true }260271 }272+}273+274+internal fun withLocalAlias(275+ devices: List<PairedDevice>,276+ deviceId: String,277+ name: String,278+): List<PairedDevice> {279+ val alias = name.trim()280+ require(alias.isNotEmpty()) { "device name must not be empty" }281+ require(alias.length <= MAX_DEVICE_NAME_CHARS) { "device name must be 64 chars or fewer" }282+ require(devices.any { it.deviceId == deviceId }) { "device_not_found" }283+ return devices.map { if (it.deviceId == deviceId) it.copy(localAlias = alias) else it }261284 }262285 263286 internal data class DeviceRevocationMutation(app/src/main/java/com/peachpasswords/ui/settings/SettingsCardBodies.kt+36 −1
@@ -827,6 +827,8 @@827827 var showBackgroundSyncPrompt by remember { mutableStateOf(false) }828828 val scope = rememberCoroutineScope()829829 var confirmUnpair by remember { mutableStateOf<String?>(null) }830+ var renamingDeviceId by remember { mutableStateOf<String?>(null) }831+ var renameDraft by remember { mutableStateOf("") }830832 var rotateConfirmOpen by remember { mutableStateOf(false) }831833 var rotating by remember { mutableStateOf(false) }832834 var rotateError by remember { mutableStateOf<String?>(null) }@@ -889,7 +891,24 @@889891 pairedDevices.forEach { d ->890892 RowLine(fg) {891893 Column(Modifier.weight(1f)) {892- Text(d.deviceName ?: d.deviceId, color = fg, fontFamily = PeachType.Body, fontSize = 13.sp)894+ if (renamingDeviceId == d.deviceId) {895+ Box(896+ Modifier.fillMaxWidth()897+ .border(1.dp, fg.copy(alpha = 0.3f), RoundedCornerShape(8.dp))898+ .padding(horizontal = 10.dp, vertical = 8.dp),899+ ) {900+ BasicTextField(901+ value = renameDraft,902+ onValueChange = { renameDraft = it.take(64) },903+ singleLine = true,904+ textStyle = TextStyle(fontFamily = PeachType.Body, fontSize = 13.sp, color = fg),905+ cursorBrush = SolidColor(fg),906+ modifier = Modifier.fillMaxWidth(),907+ )908+ }909+ } else {910+ Text(d.localAlias ?: d.deviceName ?: d.deviceId, color = fg, fontFamily = PeachType.Body, fontSize = 13.sp)911+ }893912 Text(894913 "${deviceTypeLabel(d.deviceType)} · ${syncTimeLabel(d.lastSyncedAt)}${if (isSyncStale(d.lastSyncedAt)) " · not seen recently" else ""}",895914 color = fg.copy(alpha = 0.7f), fontFamily = PeachType.Body, fontSize = 11.sp, lineHeight = 16.sp,@@ -897,6 +916,22 @@897916 )898917 }899918 if (confirmUnpair != d.deviceId) {919+ if (renamingDeviceId == d.deviceId) {920+ SubtleButton("cancel", fg) { renamingDeviceId = null }921+ Spacer(Modifier.width(PeachSpacing.xs))922+ SubtleButton("save", fg) {923+ if (renameDraft.isNotBlank()) {924+ syncVm.renameDevice(d.deviceId, renameDraft)925+ renamingDeviceId = null926+ }927+ }928+ } else {929+ SubtleButton("rename", fg) {930+ renameDraft = d.localAlias ?: d.deviceName ?: ""931+ renamingDeviceId = d.deviceId932+ }933+ }934+ Spacer(Modifier.width(PeachSpacing.xs))900935 SubtleButton("remove", fg) { confirmUnpair = d.deviceId }901936 }902937 }app/src/main/java/com/peachpasswords/ui/sync/SyncViewModel.kt+4 −0
@@ -136,6 +136,10 @@136136 PeachSyncService.start(context)137137 }138138 139+ fun renameDevice(id: String, name: String) = viewModelScope.launch {140+ deviceStore.rename(id, name)141+ }142+139143 fun confirmVouchedDevice(id: String) = viewModelScope.launch {140144 val pending = pendingVouchedStore.get(id) ?: return@launch141145 // Last gate before a vouched device becomes a sync peer. A queue entryapp/src/test/java/com/peachpasswords/sync/PairedDeviceAliasTest.kt+36 −0
@@ -0,0 +1,36 @@1+package com.peachpasswords.sync2+3+import org.junit.Assert.assertEquals4+import org.junit.Assert.assertThrows5+import org.junit.Test6+7+class PairedDeviceAliasTest {8+ private val device = PairedDevice(9+ deviceId = "phone-1",10+ deviceName = "Pixel",11+ longTermPublicKeyHex = "a".repeat(64),12+ pairedAt = 1,13+ )14+15+ @Test16+ fun `rename stores a trimmed local alias without changing peer identity`() {17+ val renamed = withLocalAlias(listOf(device), device.deviceId, " Travel phone ").single()18+19+ assertEquals("Travel phone", renamed.localAlias)20+ assertEquals(device.deviceName, renamed.deviceName)21+ assertEquals(device.longTermPublicKeyHex, renamed.longTermPublicKeyHex)22+ }23+24+ @Test25+ fun `rename rejects blank oversized and unknown aliases`() {26+ assertThrows(IllegalArgumentException::class.java) {27+ withLocalAlias(listOf(device), device.deviceId, " ")28+ }29+ assertThrows(IllegalArgumentException::class.java) {30+ withLocalAlias(listOf(device), device.deviceId, "x".repeat(65))31+ }32+ assertThrows(IllegalArgumentException::class.java) {33+ withLocalAlias(listOf(device), "missing", "Phone")34+ }35+ }36+}