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.PendingVouchedDevice10import com.peachpasswords.sync.PendingVouchedDeviceStore11import com.peachpasswords.sync.DiscoveredPeer12import com.peachpasswords.sync.SyncReconnectListener13import com.peachpasswords.sync.hexToPubkey14import com.peachpasswords.sync.PairingDiscoveryClient15import com.peachpasswords.sync.RelaySettingsStore16import com.peachpasswords.sync.SyncAlertStore17import com.peachpasswords.sync.SyncStatus18import com.peachpasswords.sync.VaultSyncAdapter19import com.peachpasswords.sync.buildLocalIdentity20import com.peachpasswords.sync.getOrCreateDeviceId21import com.peachpasswords.vault.VaultCrypto22import com.peachpasswords.vault.VaultRepository23import dagger.hilt.android.lifecycle.HiltViewModel24import dagger.hilt.android.qualifiers.ApplicationContext25import kotlinx.coroutines.flow.MutableStateFlow26import kotlinx.coroutines.flow.SharingStarted27import kotlinx.coroutines.flow.StateFlow28import kotlinx.coroutines.flow.asStateFlow29import kotlinx.coroutines.flow.stateIn30import kotlinx.coroutines.launch31import javax.inject.Inject3233343536373839404142434445@HiltViewModel46class SyncViewModel @Inject constructor(47 @ApplicationContext private val context: Context,48 private val vault: VaultRepository,49 private val deviceStore: PairedDeviceStore,50 private val pendingVouchedStore: PendingVouchedDeviceStore,51 private val discoveryClient: PairingDiscoveryClient,52 private val syncAlertStore: SyncAlertStore,53 private val relaySettings: RelaySettingsStore,54) : ViewModel() {5556 private val _status = MutableStateFlow<SyncStatus>(SyncStatus.Idle)57 val status: StateFlow<SyncStatus> = _status.asStateFlow()5859 60 private val _relayEnabled = MutableStateFlow(relaySettings.isRelayEnabled())61 val relayEnabled: StateFlow<Boolean> = _relayEnabled.asStateFlow()6263 fun setRelayEnabled(on: Boolean) {64 relaySettings.setRelayEnabled(on)65 _relayEnabled.value = on66 }6768 69 val pairedDevices: StateFlow<List<PairedDevice>> =70 deviceStore.observeAll()71 .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList())7273 val pendingVouched: StateFlow<List<PendingVouchedDevice>> =74 pendingVouchedStore.observeAll()75 .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList())7677 78 fun unpairDevice(id: String) = viewModelScope.launch { deviceStore.remove(id) }7980 fun confirmVouchedDevice(id: String) = viewModelScope.launch {81 val pending = pendingVouchedStore.get(id) ?: return@launch82 deviceStore.upsert(83 PairedDevice(84 deviceId = pending.deviceId,85 deviceName = pending.deviceName,86 deviceType = pending.deviceType,87 longTermPublicKeyHex = pending.longTermPublicKeyHex,88 signingPublicKeyHex = pending.signingPublicKeyHex,89 trustSource = "vouched",90 wsPort = pending.wsPort,91 pairedAt = pending.pairedAt,92 ),93 )94 pendingVouchedStore.remove(id)95 }9697 fun rejectVouchedDevice(id: String) = viewModelScope.launch {98 pendingVouchedStore.remove(id)99 }100101 102 suspend fun rotateSyncIdentity(): Boolean {103 val ok = vault.rotateSyncIdentity()104 if (ok) {105 deviceStore.clear()106 _status.value = SyncStatus.Idle107 }108 return ok109 }110111 112 fun buildAdapter(): ChirpSyncVaultAdapter? {113 if (!vault.isUnlocked.value) return null114 return VaultSyncAdapter(115 repo = vault,116 ourDeviceId = getOrCreateDeviceId(context),117 pairedDevices = deviceStore,118 pendingVouched = pendingVouchedStore,119 syncAlerts = syncAlertStore,120 )121 }122123 124125126127128 fun syncNow() {129 if (!vault.isUnlocked.value) {130 _status.value = SyncStatus.Error("vault_locked")131 return132 }133 val peers = pairedDevices.value134 if (peers.isEmpty()) {135 _status.value = SyncStatus.Offline136 return137 }138 val master = vault.syncMasterKey() ?: run { _status.value = SyncStatus.Error("vault_locked"); return }139 _status.value = SyncStatus.Syncing140 viewModelScope.launch {141 try {142 val identity = buildLocalIdentity(context, master)143 peers.forEach { dev ->144 145 146 147 val cached = dev.lastKnownIp?.let { ip ->148 DiscoveredPeer(149 deviceId = dev.deviceId,150 deviceName = dev.deviceName ?: dev.deviceId,151 host = ip,152 port = dev.wsPort ?: SyncReconnectListener.SYNC_PORT,153 remoteStaticPublicKey = dev.longTermPublicKeyHex154 ?.takeIf { it.length == 64 }155 ?.let(::hexToPubkey)156 ?: return@forEach,157 )158 }159 runCatching {160 discoveryClient.syncWithKnownDevice(dev.deviceId, identity, ::buildAdapter, cached = cached)161 }162 }163 _status.value = SyncStatus.LastSynced(System.currentTimeMillis())164 } catch (e: Exception) {165 _status.value = SyncStatus.Error(e.message ?: "sync_failed")166 } finally {167 VaultCrypto.zero(master)168 }169 }170 }171172 173174175176 fun triggerSync() = syncNow()177178}