1package com.peachpasswords.ui.pairing23import android.content.Context4import androidx.lifecycle.ViewModel5import androidx.lifecycle.viewModelScope6import com.peachpasswords.sync.BatteryExemption7import com.peachpasswords.sync.CodePairingServer8import com.peachpasswords.sync.PairingEvent9import com.peachpasswords.sync.PairingSession10import com.peachpasswords.sync.PeachSyncService11import com.peachpasswords.sync.buildLocalIdentity12import com.peachpasswords.sync.deriveLocalSyncKeypair13import com.peachpasswords.sync.getOrCreateDeviceId14import com.peachpasswords.vault.VaultCrypto15import com.peachpasswords.vault.VaultRepository16import dagger.hilt.android.lifecycle.HiltViewModel17import dagger.hilt.android.qualifiers.ApplicationContext18import kotlinx.coroutines.flow.MutableStateFlow19import kotlinx.coroutines.flow.StateFlow20import kotlinx.coroutines.flow.asStateFlow21import kotlinx.coroutines.launch22import javax.inject.Inject232425262728293031sealed interface CodePairUiState {32 data object Starting : CodePairUiState33 data class ShowingCode(val code: String, val ip: String, val port: Int) : CodePairUiState34 data class ConfirmSas(val sas: String, val deviceName: String, val session: PairingSession) : CodePairUiState35 data class Paired(val deviceName: String) : CodePairUiState36 data object Expired : CodePairUiState37 data class Failed(val reason: String) : CodePairUiState38}3940@HiltViewModel41class CodePairViewModel @Inject constructor(42 private val vault: VaultRepository,43 private val server: CodePairingServer,44 @ApplicationContext private val appContext: Context,45) : ViewModel() {4647 private val _state = MutableStateFlow<CodePairUiState>(CodePairUiState.Starting)48 val state: StateFlow<CodePairUiState> = _state.asStateFlow()4950 5152535455 fun batteryExemptionIntent(): android.content.Intent? = BatteryExemption.requestIntent(appContext)5657 fun start() {58 val master = vault.syncMasterKey() ?: run {59 _state.value = CodePairUiState.Failed("vault_locked")60 return61 }62 PeachSyncService.start(appContext)6364 val keys = deriveLocalSyncKeypair(master, getOrCreateDeviceId(appContext))65 val identity = buildLocalIdentity(appContext, master)66 VaultCrypto.zero(master)6768 69 70 71 server.confirmationSink = { pending ->72 _state.value = CodePairUiState.ConfirmSas(pending.sas, pending.deviceName, pending.session)73 }7475 viewModelScope.launch {76 server.start(viewModelScope, keys.secretKey, keys.publicKey, identity).collect { event ->77 _state.value = when (event) {78 is PairingEvent.Listening -> CodePairUiState.ShowingCode(event.code, event.ip, event.port)79 is PairingEvent.Paired -> CodePairUiState.Paired(event.deviceName)80 PairingEvent.Expired -> CodePairUiState.Expired81 is PairingEvent.Rejected -> CodePairUiState.Failed(event.reason)82 83 is PairingEvent.AwaitingConfirmation -> _state.value84 }85 }86 }87 }8889 fun confirmSas(matches: Boolean) {90 (_state.value as? CodePairUiState.ConfirmSas)?.session?.confirm(matches)91 }9293 fun restart() {94 server.stop()95 _state.value = CodePairUiState.Starting96 start()97 }9899 fun stop() {100 server.confirmationSink = null101 server.stop()102 }103104 override fun onCleared() {105 stop()106 super.onCleared()107 }108}