99985d508ba1

trigger Sync after local vault commits and keep Android listener alive (v2.17.43)

time
2026-09-21 12:53 UTC · 1 month ago
machine
koh2
agent
gpt-5
session
restore automatic sync freshness
parent
dcc055003dbc
6 files VERSION+1 −1app/src/main/AndroidManifest.xml+8 −4app/src/main/java/com/peachpasswords/PeachApplication.kt+7 −0app/src/main/java/com/peachpasswords/sync/PeachSyncService.kt+2 −2app/src/main/java/com/peachpasswords/vault/VaultRepository.kt+5 −0app/src/test/java/com/peachpasswords/vault/VaultRepositoryTest.kt+25 −0
VERSION+1 −1
@@ -1 +1 @@1-2.17.421+2.17.43

view file

app/src/main/AndroidManifest.xml+8 −4
@@ -20,7 +20,7 @@2020     <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />2121     <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />2222     <!-- Android 14+ requires the specific type alongside the generic permission. -->23-    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />23+    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />2424     <!-- Doze defers network work even for a foreground service, which would make2525          an already-paired browser's reconnect time out with no visible cause.2626          Play-policy-restricted: only ever requested from an explicit user tap, and@@ -109,12 +109,16 @@109109                 android:resource="@xml/file_paths" />110110         </provider>111111 112-        <!-- Keeps the phone dialable for sync; a browser extension can never113-             listen, so this side has to stay reachable. -->112+        <!-- Keeps the phone dialable for sync. This is continuous encrypted113+             device reachability, not Android's time-limited dataSync work. -->114114         <service115115             android:name=".sync.PeachSyncService"116116             android:exported="false"117-            android:foregroundServiceType="dataSync" />117+            android:foregroundServiceType="specialUse">118+            <property119+                android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"120+                android:value="continuous encrypted sync reachability for paired Peach devices" />121+        </service>118122 119123         <receiver120124             android:name=".sync.SyncNotificationDismissedReceiver"

view file

app/src/main/java/com/peachpasswords/PeachApplication.kt+7 −0
@@ -10,12 +10,14 @@1010 import com.peachpasswords.sync.ForegroundSyncCoordinator1111 import com.peachpasswords.sync.LockedSyncSpoolHooks1212 import com.peachpasswords.sync.LockedSyncSpoolStore13+import com.peachpasswords.sync.PeachSyncService1314 import com.peachpasswords.shared.PeachClipboard1415 import com.peachpasswords.ui.lock.BiometricKeyStore1516 import com.peachpasswords.updates.UpdateCheckWorker1617 import com.peachpasswords.vault.PinUnlockStore1718 import com.peachpasswords.vault.VaultKeyRotationHooks1819 import com.peachpasswords.vault.VaultLockTimer20+import com.peachpasswords.vault.VaultMutationHooks1921 import com.peachpasswords.vault.VaultRepository2022 import dagger.hilt.android.HiltAndroidApp2123 import javax.inject.Inject@@ -53,6 +55,11 @@5355         VaultKeyRotationHooks.onRotated = {5456             pinUnlock.clear()5557             BiometricKeyStore.clear(this)58+        }59+        VaultMutationHooks.onCommitted = {60+            if (backgroundSync.enabled.value && vault.syncVaultId() != null) {61+                PeachSyncService.start(this)62+            }5663         }5764 5865         // Update checks: once a day in the background, plus one now — process

view file

app/src/main/java/com/peachpasswords/sync/PeachSyncService.kt+2 −2
@@ -227,8 +227,8 @@227227         PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,228228     )229229     return NotificationCompat.Builder(context, SYNC_NOTIFICATION_CHANNEL_ID)230-        .setContentTitle("Peach sync is active")231-        .setContentText("Your other devices can reach this phone for encrypted sync.")230+        .setContentTitle("Peach sync is on")231+        .setContentText("Checking for encrypted changes from your paired devices.")232232         .setSmallIcon(R.drawable.ic_launcher_monochrome)233233         .setOngoing(true)234234         .setPriority(NotificationCompat.PRIORITY_LOW)

view file

app/src/main/java/com/peachpasswords/vault/VaultRepository.kt+5 −0
@@ -71,6 +71,10 @@7171     @Volatile var onRotated: () -> Unit = {}7272 }7373 74+internal object VaultMutationHooks {75+    @Volatile var onCommitted: () -> Unit = {}76+}77+7478 /**7579  * In-memory vault session backed by [VaultStore]. Faithful port of the browser7680  * `vault.ts` semantics that matter for correctness:@@ -3134,5 +3138,6 @@31343138         // being republished into a newly locked (or newly unlocked) session.31353139         mutateIfCurrentEpoch(expectedEpoch) { publish() }31363140         runCatching { LockedSyncSpoolHooks.refresh(this) }3141+        runCatching { VaultMutationHooks.onCommitted() }31373142     }31383143 }

view file

app/src/test/java/com/peachpasswords/vault/VaultRepositoryTest.kt+25 −0
@@ -43,6 +43,31 @@4343     private fun note(title: String) =4444         VaultEntry(id = "", title = title, type = "note", createdAt = 0, modifiedAt = 0)4545 46+    @Test fun local_entry_commit_requests_sync_but_remote_merge_does_not() = runBlocking {47+        val repo = VaultRepository(FakeStorage())48+        repo.createVault("pw")49+        var requests = 050+        VaultMutationHooks.onCommitted = { requests += 1 }51+        try {52+            repo.addEntry(login("Local"))53+            assertEquals(1, requests)54+55+            repo.applyMergedRemoteEntries(56+                listOf(57+                    login("Remote").copy(58+                        id = "remote-1",59+                        createdAt = 1,60+                        modifiedAt = System.currentTimeMillis() + 10_000,61+                    ),62+                ),63+                emptyList(),64+            )65+            assertEquals(1, requests)66+        } finally {67+            VaultMutationHooks.onCommitted = {}68+        }69+    }70+4671     @Test fun full_lock_offers_committed_state_to_opaque_sync_spool_before_zeroing() = runBlocking {4772         val repo = VaultRepository(FakeStorage())4873         repo.createVault("pw")

view file