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.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.Inject3233343536373839404142434445464748@HiltViewModel49class SyncViewModel @Inject constructor(50 @ApplicationContext private val context: Context,51 private val vault: VaultRepository,52 private val deviceStore: PairedDeviceStore,53 private val pendingVouchedStore: PendingVouchedDeviceStore,54 private val syncAlertStore: SyncAlertStore,55 private val relaySettings: RelaySettingsStore,56) : ViewModel() {5758 private val _status = MutableStateFlow<SyncStatus>(SyncStatus.Idle)59 val status: StateFlow<SyncStatus> = _status.asStateFlow()6061 private val _relayUrl = MutableStateFlow(relaySettings.relayUrl())62 val relayUrl: StateFlow<String> = _relayUrl.asStateFlow()636465 private val _syncMode = MutableStateFlow(relaySettings.syncMode())66 val syncMode: StateFlow<SyncMode> = _syncMode.asStateFlow()6768 fun setSyncMode(mode: SyncMode) {69 relaySettings.setSyncMode(mode)70 _syncMode.value = relaySettings.syncMode()71 _relayUrl.value = relaySettings.relayUrl()72 }7374 75 fun setPrivateRelayUrl(input: String): Boolean = runCatching {76 _relayUrl.value = relaySettings.setPrivateRelayUrl(input)77 _syncMode.value = relaySettings.syncMode()78 true79 }.getOrDefault(false)808182 83 val pairedDevices: StateFlow<List<PairedDevice>> =84 deviceStore.observeAll()85 .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList())8687 val pendingVouched: StateFlow<List<PendingVouchedDevice>> =88 pendingVouchedStore.observeAll()89 .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList())9091 9293949596 fun unpairDevice(id: String) = viewModelScope.launch {97 deviceStore.revoke(id, getOrCreateDeviceId(context))98 }99100 fun confirmVouchedDevice(id: String) = viewModelScope.launch {101 val pending = pendingVouchedStore.get(id) ?: return@launch102 deviceStore.upsert(103 PairedDevice(104 deviceId = pending.deviceId,105 deviceName = pending.deviceName,106 deviceType = pending.deviceType,107 longTermPublicKeyHex = pending.longTermPublicKeyHex,108 signingPublicKeyHex = pending.signingPublicKeyHex,109 trustSource = "vouched",110 wsPort = pending.wsPort,111 pairedAt = pending.pairedAt,112 ),113 )114 pendingVouchedStore.remove(id)115 }116117 fun rejectVouchedDevice(id: String) = viewModelScope.launch {118 pendingVouchedStore.remove(id)119 }120121 122 suspend fun rotateSyncIdentity(): Boolean {123 val ok = vault.rotateSyncIdentity()124 if (ok) {125 deviceStore.clear()126 _status.value = SyncStatus.Idle127 }128 return ok129 }130131 132 fun buildAdapter(): ChirpSyncVaultAdapter? {133 if (!vault.isUnlocked.value) return null134 return VaultSyncAdapter(135 repo = vault,136 ourDeviceId = getOrCreateDeviceId(context),137 pairedDevices = deviceStore,138 pendingVouched = pendingVouchedStore,139 syncAlerts = syncAlertStore,140 )141 }142143 144145146147148 fun syncNow() {149 if (!vault.isUnlocked.value) {150 _status.value = SyncStatus.Error("vault_locked")151 return152 }153 if (pairedDevices.value.isEmpty()) {154 _status.value = SyncStatus.Offline155 return156 }157 158 159 PeachSyncService.start(context)160 _status.value = SyncStatus.LastSynced(System.currentTimeMillis())161 }162163 164165166167 fun triggerSync() = syncNow()168169}