4f8a9094ec9b
stop reporting coroutine cancellation as a pairing rejection, and cancel the previous transport collector on restart, so switching to the local-network code cannot be overwritten by the relay session it replaced (v2.4.11)
- time
- 2026-07-26 13:16 UTC · 3 months ago
- machine
- koh2
- agent
- claude-opus-5
- session
- pairing UX pass
- parent
- 1f887b74dd3a
↓ peach-android-4f8a9094.tar.gzatlas at this save
files changed4 files
VERSION+1 −1app/build.gradle+2 −2app/src/main/java/com/peachpasswords/sync/RelayAdoptionHost.kt+7 −0app/src/main/java/com/peachpasswords/ui/pairing/CodePairViewModel.kt+14 −2app/build.gradle+2 −2
@@ -16,8 +16,8 @@1616 applicationId "com.peachpasswords"1717 minSdk 261818 targetSdk 3519- versionCode 24001020- versionName "2.4.10"19+ versionCode 24001120+ versionName "2.4.11"2121 2222 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"2323 vectorDrawables {app/src/main/java/com/peachpasswords/sync/RelayAdoptionHost.kt+7 −0
@@ -88,6 +88,13 @@8888 }8989 } catch (error: kotlinx.coroutines.TimeoutCancellationException) {9090 trySend(RelayAdoptionEvent.Expired)91+ } catch (error: kotlinx.coroutines.CancellationException) {92+ // stop() cancels this job when the user switches to LAN-only or93+ // leaves the screen. That is not a pairing failure — reporting it94+ // as one raced the LAN listener and replaced the freshly shown95+ // pairing code with "Couldn't pair: StandaloneCoroutine was96+ // cancelled". Cancellation must propagate, never be swallowed.97+ throw error9198 } catch (error: Throwable) {9299 trySend(RelayAdoptionEvent.Rejected(error.message ?: "relay_adoption_failed"))93100 } finally {app/src/main/java/com/peachpasswords/ui/pairing/CodePairViewModel.kt+14 −2
@@ -26,6 +26,7 @@2626 import com.peachpasswords.license.LicenseRepository2727 import dagger.hilt.android.lifecycle.HiltViewModel2828 import dagger.hilt.android.qualifiers.ApplicationContext29+import kotlinx.coroutines.Job2930 import kotlinx.coroutines.flow.MutableStateFlow3031 import kotlinx.coroutines.flow.StateFlow3132 import kotlinx.coroutines.flow.asStateFlow@@ -64,6 +65,13 @@6465 val state: StateFlow<CodePairUiState> = _state.asStateFlow()6566 6667 /**68+ * The single live transport collector. Switching between relay and LAN69+ * restarts the screen, and without cancelling the previous collector both70+ * would keep writing [_state] — the older one landing last and winning.71+ */72+ private var collectJob: Job? = null73+74+ /**6775 * The battery-exemption dialog to show, or null if it's already granted or6876 * already been declined once. Consumed by the screen, which owns an6977 * Activity to launch it from.@@ -94,7 +102,7 @@94102 _state.value = CodePairUiState.ConfirmSas(pending.sas, pending.deviceName, pending.session)95103 }96104 97- viewModelScope.launch {105+ collectJob = viewModelScope.launch {98106 try {99107 server.start(100108 viewModelScope,@@ -131,7 +139,7 @@131139 identity: LocalChirpIdentity,132140 sourceDeps: AdoptionSourceDeps,133141 ) {134- viewModelScope.launch {142+ collectJob = viewModelScope.launch {135143 try {136144 relayHost.start(137145 viewModelScope,@@ -207,6 +215,8 @@207215 }208216 209217 fun restart() {218+ collectJob?.cancel()219+ collectJob = null210220 relayHost.stop()211221 server.stop()212222 _state.value = CodePairUiState.Starting@@ -219,6 +229,8 @@219229 }220230 221231 fun stop() {232+ collectJob?.cancel()233+ collectJob = null222234 relayHost.stop()223235 server.confirmationSink = null224236 server.stop()