1package com.peachpasswords.ui.sync23import android.content.Context4import androidx.lifecycle.ViewModel5import androidx.lifecycle.viewModelScope6import com.peachpasswords.sync.SyncVaultAdapter7import com.peachpasswords.sync.PairedDevice8import com.peachpasswords.sync.PairedDeviceStore9import com.peachpasswords.sync.PendingVouchedDevice10import com.peachpasswords.sync.PendingVouchedDeviceStore11import com.peachpasswords.sync.hexToPubkey12import com.peachpasswords.sync.PeachSyncService13import com.peachpasswords.sync.RelaySettingsStore14import com.peachpasswords.sync.SyncMode15import com.peachpasswords.sync.SyncAlertStore16import com.peachpasswords.sync.SyncStatus17import com.peachpasswords.sync.VaultSyncAdapter18import com.peachpasswords.sync.buildLocalIdentity19import com.peachpasswords.sync.deriveLocalSyncKeypair20import 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.Inject32333435363738394041424344@HiltViewModel45class SyncViewModel @Inject constructor(46 @ApplicationContext private val context: Context,47 private val vault: VaultRepository,48 private val deviceStore: PairedDeviceStore,49 private val pendingVouchedStore: PendingVouchedDeviceStore,50 private val syncAlertStore: SyncAlertStore,51 private val relaySettings: RelaySettingsStore,52) : ViewModel() {5354 private val _status = MutableStateFlow<SyncStatus>(SyncStatus.Idle)55 val status: StateFlow<SyncStatus> = _status.asStateFlow()5657 private val _relayUrl = MutableStateFlow(relaySettings.relayUrl())58 val relayUrl: StateFlow<String> = _relayUrl.asStateFlow()596061 private val _syncMode = MutableStateFlow(relaySettings.syncMode())62 val syncMode: StateFlow<SyncMode> = _syncMode.asStateFlow()6364 fun setSyncMode(mode: SyncMode) {65 relaySettings.setSyncMode(mode)66 _syncMode.value = relaySettings.syncMode()67 _relayUrl.value = relaySettings.relayUrl()68 }6970 71 fun setPrivateRelayUrl(input: String): Boolean = runCatching {72 _relayUrl.value = relaySettings.setPrivateRelayUrl(input)73 _syncMode.value = relaySettings.syncMode()74 true75 }.getOrDefault(false)767778 79 val pairedDevices: StateFlow<List<PairedDevice>> =80 deviceStore.observeAll()81 .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList())8283 val pendingVouched: StateFlow<List<PendingVouchedDevice>> =84 pendingVouchedStore.observeAll()85 .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList())8687 8889909192 fun unpairDevice(id: String) = viewModelScope.launch {93 deviceStore.revoke(id, getOrCreateDeviceId(context))94 }9596 fun confirmVouchedDevice(id: String) = viewModelScope.launch {97 val pending = pendingVouchedStore.get(id) ?: return@launch98 99 100 101 if (deviceStore.isRevoked(pending.deviceId, pending.longTermPublicKeyHex)) {102 pendingVouchedStore.remove(id)103 return@launch104 }105 deviceStore.upsert(106 PairedDevice(107 deviceId = pending.deviceId,108 deviceName = pending.deviceName,109 deviceType = pending.deviceType,110 longTermPublicKeyHex = pending.longTermPublicKeyHex,111 signingPublicKeyHex = pending.signingPublicKeyHex,112 trustSource = "vouched",113 wsPort = pending.wsPort,114 pairedAt = pending.pairedAt,115 ),116 )117 pendingVouchedStore.remove(id)118 }119120 fun rejectVouchedDevice(id: String) = viewModelScope.launch {121 pendingVouchedStore.remove(id)122 }123124 125 suspend fun rotateSyncIdentity(): Boolean {126 val ok = vault.rotateSyncIdentity()127 if (ok) {128 deviceStore.clear()129 _status.value = SyncStatus.Idle130 }131 return ok132 }133134 135 fun buildAdapter(): SyncVaultAdapter? {136 if (!vault.isUnlocked.value) return null137 return VaultSyncAdapter(138 repo = vault,139 ourDeviceId = getOrCreateDeviceId(context),140 pairedDevices = deviceStore,141 pendingVouched = pendingVouchedStore,142 syncAlerts = syncAlertStore,143 )144 }145146 147148149150151 fun syncNow() {152 if (!vault.isUnlocked.value) {153 _status.value = SyncStatus.Error("vault_locked")154 return155 }156 if (pairedDevices.value.isEmpty()) {157 _status.value = SyncStatus.Offline158 return159 }160 PeachSyncService.start(context)161 _status.value = SyncStatus.LastSynced(System.currentTimeMillis())162 }163164 165166167168 fun triggerSync() = syncNow()169170}