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 fun buildAdapter(): ChirpSyncVaultAdapter? {65 if (!vault.isUnlocked.value) return null66 return VaultSyncAdapter(vault, getOrCreateDeviceId(context))67 }6869 7071727374 fun syncNow() {75 if (!vault.isUnlocked.value) {76 _status.value = SyncStatus.Error("vault_locked")77 return78 }79 val peers = pairedDevices.value80 if (peers.isEmpty()) {81 _status.value = SyncStatus.Offline82 return83 }84 val master = vault.syncMasterKey() ?: run { _status.value = SyncStatus.Error("vault_locked"); return }85 _status.value = SyncStatus.Syncing86 viewModelScope.launch {87 try {88 val identity = buildLocalIdentity(context, master)89 peers.forEach { dev ->90 runCatching { discoveryClient.syncWithKnownDevice(dev.deviceId, identity, ::buildAdapter) }91 }92 _status.value = SyncStatus.LastSynced(System.currentTimeMillis())93 } catch (e: Exception) {94 _status.value = SyncStatus.Error(e.message ?: "sync_failed")95 } finally {96 VaultCrypto.zero(master)97 }98 }99 }100101 102103104105 fun triggerSync() = syncNow()106107 108 fun syncWith(endpoint: SyncEndpoint, expectedRemotePubkeyHex: String) {109 val master = vault.syncMasterKey() ?: run {110 _status.value = SyncStatus.Error("vault_locked")111 return112 }113 val adapter = buildAdapter() ?: run {114 _status.value = SyncStatus.Error("vault_locked")115 master.fill(0)116 return117 }118 _status.value = SyncStatus.Syncing119 viewModelScope.launch {120 try {121 val deviceId = getOrCreateDeviceId(context)122 val identity = deriveSyncIdentityKeypair(master, deviceId)123 val session = openChirpSyncSession(124 endpoint = endpoint,125 ourLongTermPrivateKey = identity.secretKey,126 ourLongTermPublicKey = identity.publicKey,127 expectedRemotePubkeyHex = expectedRemotePubkeyHex,128 )129 val outcome = try {130 runChirpSyncInitiator(session, adapter)131 } finally {132 session.close()133 }134 _status.value = when (outcome.status) {135 SyncOutcomeStatus.COMPLETED -> SyncStatus.LastSynced(System.currentTimeMillis())136 SyncOutcomeStatus.VAULT_MISMATCH -> SyncStatus.Error("vault_id_mismatch")137 SyncOutcomeStatus.TIMEOUT -> SyncStatus.Error("timeout")138 SyncOutcomeStatus.ERRORED -> SyncStatus.Error(outcome.error ?: "sync_failed")139 }140 } catch (e: Exception) {141 _status.value = SyncStatus.Error(e.message ?: "sync_failed")142 } finally {143 VaultCrypto.zero(master)144 }145 }146 }147}