1package com.peachpasswords.ui.sync23import android.content.Context4import androidx.lifecycle.ViewModel5import androidx.lifecycle.viewModelScope6import com.peachpasswords.sync.ChirpSyncVaultAdapter7import com.peachpasswords.sync.PairedDevice8import com.peachpasswords.sync.PairedDeviceStore9import com.peachpasswords.sync.PairingDiscoveryClient10import com.peachpasswords.sync.SyncEndpoint11import com.peachpasswords.sync.SyncOutcomeStatus12import com.peachpasswords.sync.SyncStatus13import com.peachpasswords.sync.VaultSyncAdapter14import com.peachpasswords.sync.buildLocalIdentity15import com.peachpasswords.sync.deriveSyncIdentityKeypair16import com.peachpasswords.sync.getOrCreateDeviceId17import com.peachpasswords.sync.openChirpSyncSession18import com.peachpasswords.sync.runChirpSyncInitiator19import com.peachpasswords.vault.VaultCrypto20import com.peachpasswords.vault.VaultRepository21import dagger.hilt.android.lifecycle.HiltViewModel22import dagger.hilt.android.qualifiers.ApplicationContext23import kotlinx.coroutines.flow.MutableStateFlow24import kotlinx.coroutines.flow.SharingStarted25import kotlinx.coroutines.flow.StateFlow26import kotlinx.coroutines.flow.asStateFlow27import kotlinx.coroutines.flow.stateIn28import kotlinx.coroutines.launch29import javax.inject.Inject303132333435363738394041424344@HiltViewModel45class SyncViewModel @Inject constructor(46 @ApplicationContext private val context: Context,47 private val vault: VaultRepository,48 private val deviceStore: PairedDeviceStore,49 private val discoveryClient: PairingDiscoveryClient,50) : ViewModel() {5152 private val _status = MutableStateFlow<SyncStatus>(SyncStatus.Idle)53 val status: StateFlow<SyncStatus> = _status.asStateFlow()5455 56 val pairedDevices: StateFlow<List<PairedDevice>> =57 deviceStore.observeAll()58 .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList())5960 61 fun unpairDevice(id: String) = viewModelScope.launch { deviceStore.remove(id) }6263 64 suspend fun rotateSyncIdentity(): Boolean {65 val ok = vault.rotateSyncIdentity()66 if (ok) {67 deviceStore.clear()68 _status.value = SyncStatus.Idle69 }70 return ok71 }7273 74 fun buildAdapter(): ChirpSyncVaultAdapter? {75 if (!vault.isUnlocked.value) return null76 return VaultSyncAdapter(vault, getOrCreateDeviceId(context))77 }7879 8081828384 fun syncNow() {85 if (!vault.isUnlocked.value) {86 _status.value = SyncStatus.Error("vault_locked")87 return88 }89 val peers = pairedDevices.value90 if (peers.isEmpty()) {91 _status.value = SyncStatus.Offline92 return93 }94 val master = vault.syncMasterKey() ?: run { _status.value = SyncStatus.Error("vault_locked"); return }95 _status.value = SyncStatus.Syncing96 viewModelScope.launch {97 try {98 val identity = buildLocalIdentity(context, master)99 peers.forEach { dev ->100 runCatching { discoveryClient.syncWithKnownDevice(dev.deviceId, identity, ::buildAdapter) }101 }102 _status.value = SyncStatus.LastSynced(System.currentTimeMillis())103 } catch (e: Exception) {104 _status.value = SyncStatus.Error(e.message ?: "sync_failed")105 } finally {106 VaultCrypto.zero(master)107 }108 }109 }110111 112113114115 fun triggerSync() = syncNow()116117 118 fun syncWith(endpoint: SyncEndpoint, expectedRemotePubkeyHex: String) {119 val master = vault.syncMasterKey() ?: run {120 _status.value = SyncStatus.Error("vault_locked")121 return122 }123 val adapter = buildAdapter() ?: run {124 _status.value = SyncStatus.Error("vault_locked")125 master.fill(0)126 return127 }128 _status.value = SyncStatus.Syncing129 viewModelScope.launch {130 try {131 val deviceId = getOrCreateDeviceId(context)132 val identity = deriveSyncIdentityKeypair(master, deviceId)133 val session = openChirpSyncSession(134 endpoint = endpoint,135 ourLongTermPrivateKey = identity.secretKey,136 ourLongTermPublicKey = identity.publicKey,137 expectedRemotePubkeyHex = expectedRemotePubkeyHex,138 )139 val outcome = try {140 runChirpSyncInitiator(session, adapter)141 } finally {142 session.close()143 }144 _status.value = when (outcome.status) {145 SyncOutcomeStatus.COMPLETED -> SyncStatus.LastSynced(System.currentTimeMillis())146 SyncOutcomeStatus.VAULT_MISMATCH -> SyncStatus.Error("vault_id_mismatch")147 SyncOutcomeStatus.TIMEOUT -> SyncStatus.Error("timeout")148 SyncOutcomeStatus.ERRORED -> SyncStatus.Error(outcome.error ?: "sync_failed")149 }150 } catch (e: Exception) {151 _status.value = SyncStatus.Error(e.message ?: "sync_failed")152 } finally {153 VaultCrypto.zero(master)154 }155 }156 }157}