6c48f1b25b6e
feat: Trigger foreground sync (v2.3.49)
- time
- 2026-06-26 14:10 UTC · 4 months ago
- machine
- koh2
- parent
- 228056e22836
↓ peach-android-6c48f1b2.tar.gzatlas at this save
files changed5 files
app/build.gradle+2 −2app/src/main/java/com/peachpasswords/PeachApplication.kt+6 −0app/src/main/java/com/peachpasswords/sync/ForegroundSyncCoordinator.kt+75 −0app/src/main/java/com/peachpasswords/ui/lock/LockViewModel.kt+8 −1app/src/test/java/com/peachpasswords/sync/ForegroundSyncCoordinatorTest.kt+29 −0app/build.gradle+2 −2
@@ -16,8 +16,8 @@1616 applicationId "com.peachpasswords"1717 minSdk 261818 targetSdk 3519- versionCode 23480020- versionName "2.3.48"19+ versionCode 23490020+ versionName "2.3.49"2121 2222 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"2323 vectorDrawables {app/src/main/java/com/peachpasswords/PeachApplication.kt+6 −0
@@ -4,6 +4,7 @@44 import androidx.lifecycle.DefaultLifecycleObserver55 import androidx.lifecycle.LifecycleOwner66 import androidx.lifecycle.ProcessLifecycleOwner7+import com.peachpasswords.sync.ForegroundSyncCoordinator78 import com.peachpasswords.vault.VaultLockTimer89 import com.peachpasswords.vault.VaultRepository910 import dagger.hilt.android.HiltAndroidApp@@ -13,6 +14,7 @@1314 class PeachApplication : Application() {1415 @Inject lateinit var vault: VaultRepository1516 @Inject lateinit var lockTimer: VaultLockTimer17+ @Inject lateinit var foregroundSync: ForegroundSyncCoordinator1618 1719 override fun onCreate() {1820 super.onCreate()@@ -20,6 +22,10 @@2022 // ProcessLifecycleOwner is process-wide and debounced, so a rotation or a2123 // brief Activity restart does not trip it — only a genuine backgrounding.2224 ProcessLifecycleOwner.get().lifecycle.addObserver(object : DefaultLifecycleObserver {25+ override fun onStart(owner: LifecycleOwner) {26+ foregroundSync.triggerForegroundSync()27+ }28+2329 override fun onStop(owner: LifecycleOwner) {2430 vault.lock()2531 lockTimer.cancel()app/src/main/java/com/peachpasswords/sync/ForegroundSyncCoordinator.kt+75 −0
@@ -0,0 +1,75 @@1+package com.peachpasswords.sync2+3+import android.content.Context4+import com.peachpasswords.vault.VaultCrypto5+import com.peachpasswords.vault.VaultRepository6+import dagger.hilt.android.qualifiers.ApplicationContext7+import kotlinx.coroutines.CoroutineScope8+import kotlinx.coroutines.Dispatchers9+import kotlinx.coroutines.Job10+import kotlinx.coroutines.SupervisorJob11+import kotlinx.coroutines.launch12+import javax.inject.Inject13+import javax.inject.Singleton14+15+@Singleton16+class ForegroundSyncCoordinator @Inject constructor(17+ @ApplicationContext private val context: Context,18+ private val vault: VaultRepository,19+ private val deviceStore: PairedDeviceStore,20+ private val pendingVouchedStore: PendingVouchedDeviceStore,21+ private val discoveryClient: PairingDiscoveryClient,22+ private val syncAlertStore: SyncAlertStore,23+) {24+ private val scope = CoroutineScope(Dispatchers.Main.immediate + SupervisorJob())25+ private var inFlight: Job? = null26+27+ fun triggerForegroundSync() {28+ if (inFlight?.isActive == true) return29+ if (!vault.isUnlocked.value) return30+ val peers = foregroundSyncPeers(deviceStore.getAll())31+ if (peers.isEmpty()) return32+ val master = vault.syncMasterKey() ?: return33+ inFlight = scope.launch {34+ try {35+ val identity = buildLocalIdentity(context, master)36+ peers.forEach { peer ->37+ runCatching {38+ discoveryClient.syncWithKnownDevice(39+ deviceId = peer.deviceId,40+ localIdentity = identity,41+ vaultFactory = ::buildAdapter,42+ )43+ }44+ }45+ } finally {46+ VaultCrypto.zero(master)47+ }48+ }49+ }50+51+ private fun buildAdapter(): ChirpSyncVaultAdapter? {52+ if (!vault.isUnlocked.value) return null53+ return VaultSyncAdapter(54+ repo = vault,55+ ourDeviceId = getOrCreateDeviceId(context),56+ pairedDevices = deviceStore,57+ pendingVouched = pendingVouchedStore,58+ syncAlerts = syncAlertStore,59+ )60+ }61+}62+63+internal fun foregroundSyncPeers(devices: List<PairedDevice>): List<PairedDevice> =64+ devices.filter { device ->65+ val trusted = device.trustSource == null || device.trustSource == "direct"66+ val syncCapable = when (device.deviceType) {67+ null,68+ "android",69+ "ios",70+ "desktop",71+ "extension" -> true72+ else -> false73+ }74+ trusted && syncCapable75+ }app/src/main/java/com/peachpasswords/ui/lock/LockViewModel.kt+8 −1
@@ -5,6 +5,7 @@55 import androidx.fragment.app.FragmentActivity66 import androidx.lifecycle.ViewModel77 import androidx.lifecycle.viewModelScope8+import com.peachpasswords.sync.ForegroundSyncCoordinator89 import com.peachpasswords.ui.PeachNav910 import com.peachpasswords.ui.vault.entryTapDestination1011 import com.peachpasswords.vault.CreateVaultResult@@ -85,6 +86,7 @@8586 private val duress: DuressConfigStore,8687 private val lockTimer: VaultLockTimer,8788 private val lastViewed: LastViewedEntryStore,89+ private val foregroundSync: ForegroundSyncCoordinator,8890 @ApplicationContext private val appContext: Context,8991 ) : ViewModel() {9092 private val _state = MutableStateFlow(LockUiState(hasVault = vault.hasVault()))@@ -118,6 +120,7 @@118120 slotA != null -> {119121 vault.commitSlotAUnlock(slotA)120122 lockTimer.start()123+ foregroundSync.triggerForegroundSync()121124 _state.update { it.copy(loading = false, unlocked = true) }122125 }123126 else -> _state.update {@@ -195,7 +198,11 @@195198 val r = vault.unlockWithKey(key)196199 VaultCrypto.zero(key)197200 when (r) {198- is UnlockResult.Success -> { lockTimer.start(); _state.update { it.copy(unlocked = true) } }201+ is UnlockResult.Success -> {202+ lockTimer.start()203+ foregroundSync.triggerForegroundSync()204+ _state.update { it.copy(unlocked = true) }205+ }199206 else -> _state.update { it.copy(error = "biometric unlock failed", shakeNonce = it.shakeNonce + 1) }200207 }201208 }app/src/test/java/com/peachpasswords/sync/ForegroundSyncCoordinatorTest.kt+29 −0
@@ -0,0 +1,29 @@1+package com.peachpasswords.sync2+3+import org.junit.Assert.assertEquals4+import org.junit.Test5+6+class ForegroundSyncCoordinatorTest {7+ @Test fun foreground_sync_targets_direct_sync_capable_devices_only() {8+ val devices = listOf(9+ peer("direct-android", type = "android", trust = "direct"),10+ peer("legacy-null-trust", type = "extension", trust = null),11+ peer("vouched", type = "android", trust = "vouched"),12+ peer("unknown-kind", type = "printer", trust = "direct"),13+ )14+15+ val eligible = foregroundSyncPeers(devices).map { it.deviceId }16+17+ assertEquals(listOf("direct-android", "legacy-null-trust"), eligible)18+ }19+20+ private fun peer(id: String, type: String?, trust: String?) = PairedDevice(21+ deviceId = id,22+ deviceName = id,23+ deviceType = type,24+ longTermPublicKeyHex = "aa".repeat(32),25+ signingPublicKeyHex = "bb".repeat(32),26+ trustSource = trust,27+ pairedAt = 1L,28+ )29+}