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 deviceStore.upsert(99 PairedDevice(100 deviceId = pending.deviceId,101 deviceName = pending.deviceName,102 deviceType = pending.deviceType,103 longTermPublicKeyHex = pending.longTermPublicKeyHex,104 signingPublicKeyHex = pending.signingPublicKeyHex,105 trustSource = "vouched",106 wsPort = pending.wsPort,107 pairedAt = pending.pairedAt,108 ),109 )110 pendingVouchedStore.remove(id)111 }112113 fun rejectVouchedDevice(id: String) = viewModelScope.launch {114 pendingVouchedStore.remove(id)115 }116117 118 suspend fun rotateSyncIdentity(): Boolean {119 val ok = vault.rotateSyncIdentity()120 if (ok) {121 deviceStore.clear()122 _status.value = SyncStatus.Idle123 }124 return ok125 }126127 128 fun buildAdapter(): SyncVaultAdapter? {129 if (!vault.isUnlocked.value) return null130 return VaultSyncAdapter(131 repo = vault,132 ourDeviceId = getOrCreateDeviceId(context),133 pairedDevices = deviceStore,134 pendingVouched = pendingVouchedStore,135 syncAlerts = syncAlertStore,136 )137 }138139 140141142143144 fun syncNow() {145 if (!vault.isUnlocked.value) {146 _status.value = SyncStatus.Error("vault_locked")147 return148 }149 if (pairedDevices.value.isEmpty()) {150 _status.value = SyncStatus.Offline151 return152 }153 PeachSyncService.start(context)154 _status.value = SyncStatus.LastSynced(System.currentTimeMillis())155 }156157 158159160161 fun triggerSync() = syncNow()162163}