1package com.peachpasswords.ui.pairing23import android.content.Context4import android.util.Log5import com.peachpasswords.BuildConfig6import androidx.lifecycle.ViewModel7import androidx.lifecycle.viewModelScope8import com.peachpasswords.sync.BatteryExemption9import com.peachpasswords.sync.CodePairingServer10import com.peachpasswords.sync.PairingEvent11import com.peachpasswords.sync.PairingSession12import com.peachpasswords.sync.PeachSyncService13import com.peachpasswords.sync.buildLocalIdentity14import com.peachpasswords.sync.deriveLocalSyncKeypair15import com.peachpasswords.sync.getOrCreateDeviceId16import com.peachpasswords.vault.VaultCrypto17import com.peachpasswords.vault.VaultRepository18import dagger.hilt.android.lifecycle.HiltViewModel19import dagger.hilt.android.qualifiers.ApplicationContext20import kotlinx.coroutines.flow.MutableStateFlow21import kotlinx.coroutines.flow.StateFlow22import kotlinx.coroutines.flow.asStateFlow23import kotlinx.coroutines.launch24import javax.inject.Inject252627282930313233sealed interface CodePairUiState {34 data object Starting : CodePairUiState35 data class ShowingCode(val code: String, val ip: String, val port: Int) : CodePairUiState36 data class ConfirmSas(val sas: String, val deviceName: String, val session: PairingSession) : CodePairUiState37 data class Paired(val deviceName: String) : CodePairUiState38 data object Expired : CodePairUiState39 data class Failed(val reason: String) : CodePairUiState40}4142@HiltViewModel43class CodePairViewModel @Inject constructor(44 private val vault: VaultRepository,45 private val server: CodePairingServer,46 @ApplicationContext private val appContext: Context,47) : ViewModel() {4849 private val _state = MutableStateFlow<CodePairUiState>(CodePairUiState.Starting)50 val state: StateFlow<CodePairUiState> = _state.asStateFlow()5152 5354555657 fun batteryExemptionIntent(): android.content.Intent? = BatteryExemption.requestIntent(appContext)5859 fun start() {60 val master = vault.syncMasterKey() ?: run {61 _state.value = CodePairUiState.Failed("vault_locked")62 return63 }64 PeachSyncService.start(appContext)6566 val keys = deriveLocalSyncKeypair(master, getOrCreateDeviceId(appContext))67 val identity = buildLocalIdentity(appContext, master)68 VaultCrypto.zero(master)6970 71 72 73 server.confirmationSink = { pending ->74 _state.value = CodePairUiState.ConfirmSas(pending.sas, pending.deviceName, pending.session)75 }7677 viewModelScope.launch {78 server.start(viewModelScope, keys.secretKey, keys.publicKey, identity).collect { event ->79 _state.value = when (event) {80 is PairingEvent.Listening -> CodePairUiState.ShowingCode(event.code, event.ip, event.port)81 is PairingEvent.Paired -> CodePairUiState.Paired(event.deviceName)82 PairingEvent.Expired -> CodePairUiState.Expired83 is PairingEvent.Rejected -> {84 85 86 87 if (BuildConfig.DEBUG) Log.w("PeachPairing", "pairing rejected: ${event.reason}")88 CodePairUiState.Failed(event.reason)89 }90 91 is PairingEvent.AwaitingConfirmation -> _state.value92 }93 }94 }95 }9697 fun confirmSas(matches: Boolean) {98 (_state.value as? CodePairUiState.ConfirmSas)?.session?.confirm(matches)99 }100101 fun restart() {102 server.stop()103 _state.value = CodePairUiState.Starting104 start()105 }106107 fun stop() {108 server.confirmationSink = null109 server.stop()110 }111112 override fun onCleared() {113 stop()114 super.onCleared()115 }116}