diff --git a/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt b/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt index 33fa3aef8e..783b4a1bb4 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt @@ -10,7 +10,6 @@ import android.view.WindowManager import androidx.activity.compose.setContent import androidx.activity.viewModels import androidx.compose.animation.core.* -import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.runtime.* @@ -60,6 +59,7 @@ class MainActivity: FragmentActivity() { } } private val vm by viewModels() + private val destroyedAfterBackPress = mutableStateOf(false) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -87,6 +87,7 @@ class MainActivity: FragmentActivity() { m, userAuthorized, laFailed, + destroyedAfterBackPress, ::runAuthenticate, ::setPerformLA, showLANotice = { showLANotice(m.controller.appPrefs.laNoticeShown, this) } @@ -109,7 +110,12 @@ class MainActivity: FragmentActivity() { val enteredBackgroundVal = enteredBackground.value val delay = vm.chatModel.controller.appPrefs.laLockDelay.get() if (enteredBackgroundVal == null || elapsedRealtime() - enteredBackgroundVal >= delay * 1000) { - runAuthenticate() + if (userAuthorized.value != false) { + /** [runAuthenticate] will be called in [MainPage] if needed. Making like this prevents double showing of passcode on start */ + setAuthState() + } else if (!vm.chatModel.activeCallViewIsVisible.value) { + runAuthenticate() + } } } @@ -143,6 +149,7 @@ class MainActivity: FragmentActivity() { // When pressed Back and there is no one wants to process the back event, clear auth state to force re-auth on launch clearAuthState() laFailed.value = true + destroyedAfterBackPress.value = true } if (!onBackPressedDispatcher.hasEnabledCallbacks()) { // Drop shared content @@ -150,13 +157,14 @@ class MainActivity: FragmentActivity() { } } + private fun setAuthState() { + userAuthorized.value = !vm.chatModel.controller.appPrefs.performLA.get() + } + private fun runAuthenticate() { val m = vm.chatModel - if (!m.controller.appPrefs.performLA.get()) { - userAuthorized.value = true - } else { - userAuthorized.value = false - ModalManager.shared.closeModals() + setAuthState() + if (userAuthorized.value == false) { // To make Main thread free in order to allow to Compose to show blank view that hiding content underneath of it faster on slow devices CoroutineScope(Dispatchers.Default).launch { delay(50) @@ -377,6 +385,7 @@ fun MainPage( chatModel: ChatModel, userAuthorized: MutableState, laFailed: MutableState, + destroyedAfterBackPress: MutableState, runAuthenticate: () -> Unit, setPerformLA: (Boolean, FragmentActivity) -> Unit, showLANotice: () -> Unit @@ -413,19 +422,21 @@ fun MainPage( } @Composable - fun authView() { - Box( - Modifier.fillMaxSize().background(MaterialTheme.colors.background), - contentAlignment = Alignment.Center - ) { - SimpleButton( - stringResource(R.string.auth_unlock), - icon = painterResource(R.drawable.ic_lock), - click = { - laFailed.value = false - runAuthenticate() - } - ) + fun AuthView() { + Surface(color = MaterialTheme.colors.background) { + Box( + Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + SimpleButton( + stringResource(R.string.auth_unlock), + icon = painterResource(R.drawable.ic_lock), + click = { + laFailed.value = false + runAuthenticate() + } + ) + } } } @@ -439,17 +450,8 @@ fun MainPage( } } onboarding == null || userCreated == null -> SplashView() - userAuthorized.value != true -> { - if (chatModel.controller.appPrefs.performLA.get() && laFailed.value) { - authView() - } else { - SplashView() - } - } onboarding == OnboardingStage.OnboardingComplete && userCreated -> { Box { - if (chatModel.showCallView.value) ActiveCallView(chatModel) - else { showAdvertiseLAAlert = true BoxWithConstraints { var currentChatId by rememberSaveable { mutableStateOf(chatModel.chatId.value) } @@ -495,7 +497,6 @@ fun MainPage( } } } - } } onboarding == OnboardingStage.Step1_SimpleXInfo -> SimpleXInfo(chatModel, onboarding = true) onboarding == OnboardingStage.Step2_CreateProfile -> CreateProfile(chatModel) {} @@ -503,6 +504,24 @@ fun MainPage( onboarding == OnboardingStage.Step4_SetNotificationsMode -> SetNotificationsMode(chatModel) } ModalManager.shared.showInView() + val unauthorized = remember { derivedStateOf { userAuthorized.value != true } } + if (unauthorized.value && !(chatModel.activeCallViewIsVisible.value && chatModel.showCallView.value)) { + LaunchedEffect(Unit) { + // With these constrains when user presses back button while on ChatList, activity destroys and shows auth request + // while the screen moves to a launcher. Detect it and prevent showing the auth + if (!(destroyedAfterBackPress.value && chatModel.controller.appPrefs.laMode.get() == LAMode.SYSTEM)) { + runAuthenticate() + } + } + if (chatModel.controller.appPrefs.performLA.get() && laFailed.value) { + AuthView() + } else { + SplashView() + } + } else if (chatModel.showCallView.value) { + ActiveCallView(chatModel) + } + ModalManager.shared.showPasscodeInView() val invitation = chatModel.activeCallInvitation.value if (invitation != null) IncomingCallAlertView(invitation, chatModel) AlertManager.shared.showInView() diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt index c6fb1ae2c2..99930c4df7 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt @@ -75,6 +75,7 @@ class ChatModel(val controller: ChatController) { val callInvitations = mutableStateMapOf() val activeCallInvitation = mutableStateOf(null) val activeCall = mutableStateOf(null) + val activeCallViewIsVisible = mutableStateOf(false) val callCommand = mutableStateOf(null) val showCallView = mutableStateOf(false) val switchingCall = mutableStateOf(false) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/call/CallManager.kt b/apps/android/app/src/main/java/chat/simplex/app/views/call/CallManager.kt index a88f07d3c5..b87f996f8e 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/call/CallManager.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/call/CallManager.kt @@ -26,7 +26,6 @@ class CallManager(val chatModel: ChatModel) { } fun acceptIncomingCall(invitation: RcvCallInvitation) { - ModalManager.shared.closeModals() val call = chatModel.activeCall.value if (call == null) { justAcceptIncomingCall(invitation = invitation) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/call/CallView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/call/CallView.kt index e7e235c606..62f4cc7bd1 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/call/CallView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/call/CallView.kt @@ -185,10 +185,12 @@ fun ActiveCallView(chatModel: ChatModel) { activity.volumeControlStream = AudioManager.STREAM_VOICE_CALL // Lock orientation to portrait in order to have good experience with calls activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + chatModel.activeCallViewIsVisible.value = true onDispose { activity.volumeControlStream = prevVolumeControlStream // Unlock orientation activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED + chatModel.activeCallViewIsVisible.value = false } } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/LocalAuthentication.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/LocalAuthentication.kt index 403b21899c..97ef5e1082 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/LocalAuthentication.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/LocalAuthentication.kt @@ -53,7 +53,7 @@ fun authenticate( } LAMode.PASSCODE -> { val password = ksAppPassword.get() ?: return completed(LAResult.Unavailable(generalGetString(R.string.la_no_app_password))) - ModalManager.shared.showCustomModal(animated = false) { close -> + ModalManager.shared.showPasscodeCustomModal { close -> BackHandler { close() completed(LAResult.Error(generalGetString(R.string.authentication_cancelled))) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/ModalView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/ModalView.kt index 3ba958a540..ab1e123960 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/ModalView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/ModalView.kt @@ -37,6 +37,7 @@ class ModalManager { private val modalCount = mutableStateOf(0) private val toRemove = mutableSetOf() private var oldViewChanging = AtomicBoolean(false) + private var passcodeView: MutableState<(@Composable (close: () -> Unit) -> Unit)?> = mutableStateOf(null) fun showModal(settings: Boolean = false, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable () -> Unit) { showCustomModal { close -> @@ -51,7 +52,7 @@ class ModalManager { } fun showCustomModal(animated: Boolean = true, modal: @Composable (close: () -> Unit) -> Unit) { - Log.d(TAG, "ModalManager.showModal") + Log.d(TAG, "ModalManager.showCustomModal") // Means, animation is in progress or not started yet. Do not wait until animation finishes, just remove all from screen. // This is useful when invoking close() and ShowCustomModal one after another without delay. Otherwise, screen will hold prev view if (toRemove.isNotEmpty()) { @@ -61,6 +62,11 @@ class ModalManager { modalCount.value = modalViews.size - toRemove.size } + fun showPasscodeCustomModal(modal: @Composable (close: () -> Unit) -> Unit) { + Log.d(TAG, "ModalManager.showPasscodeCustomModal") + passcodeView.value = modal + } + fun hasModalsOpen() = modalCount.value > 0 fun closeModal() { @@ -73,6 +79,7 @@ class ModalManager { fun closeModals() { while (modalCount.value > 0) closeModal() + passcodeView.value = null } @OptIn(ExperimentalAnimationApi::class) @@ -100,6 +107,11 @@ class ModalManager { } } + @Composable + fun showPasscodeInView() { + remember { passcodeView }.value?.invoke { passcodeView.value = null } + } + /** * Allows to modify a list without getting [ConcurrentModificationException] * */