CodePairViewModel.kt

3.6 KB · raw

1package com.peachpasswords.ui.pairing23import android.content.Context4import androidx.lifecycle.ViewModel5import androidx.lifecycle.viewModelScope6import com.peachpasswords.sync.CodePairingServer7import com.peachpasswords.sync.PairingEvent8import com.peachpasswords.sync.PairingSession9import com.peachpasswords.sync.PeachSyncService10import com.peachpasswords.sync.buildLocalIdentity11import com.peachpasswords.sync.deriveLocalSyncKeypair12import com.peachpasswords.sync.getOrCreateDeviceId13import com.peachpasswords.vault.VaultCrypto14import com.peachpasswords.vault.VaultRepository15import dagger.hilt.android.lifecycle.HiltViewModel16import dagger.hilt.android.qualifiers.ApplicationContext17import kotlinx.coroutines.flow.MutableStateFlow18import kotlinx.coroutines.flow.StateFlow19import kotlinx.coroutines.flow.asStateFlow20import kotlinx.coroutines.launch21import javax.inject.Inject2223/**24 * The phone half of code pairing: show a code, wait to be dialed.25 *26 * The browser drives the conversation from here on — it is always the dialer27 * (see [CodePairingServer] for the wire contract), so there is nothing for this28 * screen to do but publish the code and then present the SAS for comparison.29 */30sealed interface CodePairUiState {31    data object Starting : CodePairUiState32    data class ShowingCode(val code: String, val ip: String, val port: Int) : CodePairUiState33    data class ConfirmSas(val sas: String, val deviceName: String, val session: PairingSession) : CodePairUiState34    data class Paired(val deviceName: String) : CodePairUiState35    data class Failed(val reason: String) : CodePairUiState36}3738@HiltViewModel39class CodePairViewModel @Inject constructor(40    private val vault: VaultRepository,41    private val server: CodePairingServer,42    @ApplicationContext private val appContext: Context,43) : ViewModel() {4445    private val _state = MutableStateFlow<CodePairUiState>(CodePairUiState.Starting)46    val state: StateFlow<CodePairUiState> = _state.asStateFlow()4748    fun start() {49        val master = vault.syncMasterKey() ?: run {50            _state.value = CodePairUiState.Failed("vault_locked")51            return52        }53        PeachSyncService.start(appContext)5455        val keys = deriveLocalSyncKeypair(master, getOrCreateDeviceId(appContext))56        val identity = buildLocalIdentity(appContext, master)57        VaultCrypto.zero(master)5859        // The handler blocks inside the live connection while the user compares60        // digits, so the SAS has to reach the UI out of band rather than as the61        // connection's result.62        server.confirmationSink = { pending ->63            _state.value = CodePairUiState.ConfirmSas(pending.sas, pending.deviceName, pending.session)64        }6566        viewModelScope.launch {67            server.start(viewModelScope, keys.secretKey, keys.publicKey, identity).collect { event ->68                _state.value = when (event) {69                    is PairingEvent.Listening -> CodePairUiState.ShowingCode(event.code, event.ip, event.port)70                    is PairingEvent.Paired -> CodePairUiState.Paired(event.deviceName)71                    is PairingEvent.Rejected -> CodePairUiState.Failed(event.reason)72                    // Delivered via confirmationSink; the flow only sees the outcome.73                    is PairingEvent.AwaitingConfirmation -> _state.value74                }75            }76        }77    }7879    fun confirmSas(matches: Boolean) {80        (_state.value as? CodePairUiState.ConfirmSas)?.session?.confirm(matches)81    }8283    fun stop() {84        server.confirmationSink = null85        server.stop()86    }8788    override fun onCleared() {89        stop()90        super.onCleared()91    }92}