CodePairViewModel.kt

4.2 KB · raw

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.Inject2324/**25 * The phone half of code pairing: show a code, wait to be dialed.26 *27 * The browser drives the conversation from here on — it is always the dialer28 * (see [CodePairingServer] for the wire contract), so there is nothing for this29 * screen to do but publish the code and then present the SAS for comparison.30 */31sealed 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    /**51     * The battery-exemption dialog to show, or null if it's already granted or52     * already been declined once. Consumed by the screen, which owns an53     * Activity to launch it from.54     */55    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        // The handler blocks inside the live connection while the user compares69        // digits, so the SAS has to reach the UI out of band rather than as the70        // connection's result.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                    // Delivered via confirmationSink; the flow only sees the outcome.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}