From 1d36ecafe14dc92884da7ca59a850da8a05d390a Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Thu, 30 Apr 2026 17:37:47 -0300 Subject: [PATCH] Clean up back-pressed behavior which could result in an empty backstack and crash. Co-authored-by: Greyson Parrelli Co-authored-by: jeffrey-signal --- .../QuickTransferOldDeviceNavigation.kt | 8 ++++++++ .../QuickTransferOldDeviceViewModel.kt | 19 +++++++++++++++++-- .../registration/RegistrationNavigation.kt | 8 ++++++++ .../registration/RegistrationViewModel.kt | 16 +++++++++++++++- 4 files changed, 48 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/olddevice/QuickTransferOldDeviceNavigation.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/olddevice/QuickTransferOldDeviceNavigation.kt index 6c841d4591..f9b502c761 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/olddevice/QuickTransferOldDeviceNavigation.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/olddevice/QuickTransferOldDeviceNavigation.kt @@ -6,6 +6,7 @@ package org.thoughtcrime.securesms.registration.olddevice import android.os.Parcelable +import androidx.activity.compose.LocalOnBackPressedDispatcherOwner import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -49,6 +50,13 @@ fun TransferAccountNavHost( ) { val backStack by viewModel.backStack.collectAsStateWithLifecycle() + val backDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher + LaunchedEffect(viewModel, backDispatcher) { + viewModel.finishRequests.collect { + backDispatcher?.onBackPressed() + } + } + val entryProvider = entryProvider { navigationEntries( viewModel = viewModel, diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/olddevice/QuickTransferOldDeviceViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/olddevice/QuickTransferOldDeviceViewModel.kt index add086a5d3..6642d746e2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/olddevice/QuickTransferOldDeviceViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/olddevice/QuickTransferOldDeviceViewModel.kt @@ -8,8 +8,12 @@ package org.thoughtcrime.securesms.registration.olddevice import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.signal.core.util.logging.Log @@ -41,8 +45,11 @@ class QuickTransferOldDeviceViewModel(reRegisterUri: String) : ViewModel() { private val _backStack: MutableStateFlow> = MutableStateFlow(listOf(TransferAccountRoute.Transfer)) val backStack: StateFlow> = _backStack + private val finishChannel = Channel(capacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) + val finishRequests: Flow = finishChannel.receiveAsFlow() + fun goBack() { - _backStack.update { it.dropLast(1) } + popOrFinish() } fun onEvent(event: PrepareDeviceScreenEvents) { @@ -51,7 +58,7 @@ class QuickTransferOldDeviceViewModel(reRegisterUri: String) : ViewModel() { store.update { it.copy(navigateToBackupCreation = true) } } PrepareDeviceScreenEvents.NavigateBack -> { - _backStack.update { it.dropLast(1) } + popOrFinish() } PrepareDeviceScreenEvents.SkipAndContinue -> { _backStack.update { listOf(TransferAccountRoute.Transfer) } @@ -96,6 +103,14 @@ class QuickTransferOldDeviceViewModel(reRegisterUri: String) : ViewModel() { store.update { it.copy(navigateToBackupCreation = false) } } + private fun popOrFinish() { + if (_backStack.value.size > 1) { + _backStack.update { it.dropLast(1) } + } else { + finishChannel.trySend(Unit) + } + } + private fun transferAccount() { viewModelScope.launch(Dispatchers.IO) { val restoreMethodToken = UUID.randomUUID().toString() diff --git a/feature/registration/src/main/java/org/signal/registration/RegistrationNavigation.kt b/feature/registration/src/main/java/org/signal/registration/RegistrationNavigation.kt index 7da631bc01..8c9341f251 100644 --- a/feature/registration/src/main/java/org/signal/registration/RegistrationNavigation.kt +++ b/feature/registration/src/main/java/org/signal/registration/RegistrationNavigation.kt @@ -8,6 +8,7 @@ package org.signal.registration import android.os.Parcelable +import androidx.activity.compose.LocalOnBackPressedDispatcherOwner import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.CircularProgressIndicator @@ -231,6 +232,13 @@ fun RegistrationNavHost( return } + val backDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher + LaunchedEffect(viewModel, backDispatcher) { + viewModel.finishRequests.collect { + backDispatcher?.onBackPressed() + } + } + val entryProvider = entryProvider { navigationEntries( registrationRepository = registrationRepository, diff --git a/feature/registration/src/main/java/org/signal/registration/RegistrationViewModel.kt b/feature/registration/src/main/java/org/signal/registration/RegistrationViewModel.kt index 6ef85a12d3..167fffc243 100644 --- a/feature/registration/src/main/java/org/signal/registration/RegistrationViewModel.kt +++ b/feature/registration/src/main/java/org/signal/registration/RegistrationViewModel.kt @@ -13,9 +13,13 @@ import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.createSavedStateHandle import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.CreationExtras +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch import org.signal.core.ui.navigation.ResultEventBus import org.signal.core.util.logging.Log @@ -35,6 +39,9 @@ class RegistrationViewModel(private val repository: RegistrationRepository, save private var _state: MutableStateFlow = savedStateHandle.getMutableStateFlow("registration_state", initialValue = RegistrationFlowState()) val state: StateFlow = _state.asStateFlow() + private val finishChannel = Channel(capacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) + val finishRequests: Flow = finishChannel.receiveAsFlow() + val resultBus = ResultEventBus() init { @@ -66,7 +73,14 @@ class RegistrationViewModel(private val repository: RegistrationRepository, save is RegistrationFlowEvent.Registered -> state.copy(accountEntropyPool = event.accountEntropyPool) is RegistrationFlowEvent.MasterKeyRestoredFromSvr -> state.copy(temporaryMasterKey = event.masterKey) is RegistrationFlowEvent.NavigateToScreen -> applyNavigationToScreenEvent(state, event) - is RegistrationFlowEvent.NavigateBack -> state.copy(backStack = state.backStack.dropLast(1)) + is RegistrationFlowEvent.NavigateBack -> { + if (state.backStack.size > 1) { + state.copy(backStack = state.backStack.dropLast(1)) + } else { + finishChannel.trySend(Unit) + state + } + } is RegistrationFlowEvent.RecoveryPasswordInvalid -> state.copy(doNotAttemptRecoveryPassword = true) is RegistrationFlowEvent.PendingRestoreOptionSelected -> state.copy(pendingRestoreOption = event.option) is RegistrationFlowEvent.UserSuppliedAepSubmitted -> state.copy(unverifiedRestoredAep = event.aep)