From b0c369ee1a35a43df719535dba44b08133ddecd9 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 17 May 2024 18:40:16 +0200 Subject: [PATCH] Fix modal contents overlapping screen lock pin #2692 --- app/src/main/AndroidManifest.xml | 4 ++ .../io/element/android/x/MainActivity.kt | 41 ++++++----- .../android/appnav/LoggedInFlowNode.kt | 19 +---- changelog.d/2692.bugfix | 1 + .../impl/unlock/PinUnlockPresenter.kt | 6 +- .../impl/unlock/activity/PinUnlockActivity.kt | 71 +++++++++++++++++++ .../impl/unlock/di/PinUnlockBindings.kt | 26 +++++++ .../impl/unlock/signout/DefaultSignOut.kt | 40 +++++++++++ .../lockscreen/impl/unlock/signout/SignOut.kt | 21 ++++++ .../lockscreen/impl/unlock/FakeSignOut.kt | 28 ++++++++ .../impl/unlock/PinUnlockPresenterTest.kt | 3 +- 11 files changed, 219 insertions(+), 41 deletions(-) create mode 100644 changelog.d/2692.bugfix create mode 100644 features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/activity/PinUnlockActivity.kt create mode 100644 features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/di/PinUnlockBindings.kt create mode 100644 features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/signout/DefaultSignOut.kt create mode 100644 features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/signout/SignOut.kt create mode 100644 features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/FakeSignOut.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 931c995d9a..660980d538 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -138,6 +138,10 @@ android:resource="@xml/file_providers" /> + + diff --git a/app/src/main/kotlin/io/element/android/x/MainActivity.kt b/app/src/main/kotlin/io/element/android/x/MainActivity.kt index c11e63a164..7d2f404560 100644 --- a/app/src/main/kotlin/io/element/android/x/MainActivity.kt +++ b/app/src/main/kotlin/io/element/android/x/MainActivity.kt @@ -32,6 +32,9 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalUriHandler import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import com.bumble.appyx.core.integration.NodeHost import com.bumble.appyx.core.integrationpoint.NodeActivity import com.bumble.appyx.core.plugin.NodeReadyObserver @@ -39,13 +42,16 @@ import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.Theme import io.element.android.compound.theme.isDark import io.element.android.compound.theme.mapToTheme +import io.element.android.features.lockscreen.api.LockScreenLockState +import io.element.android.features.lockscreen.api.LockScreenService import io.element.android.features.lockscreen.api.handleSecureFlag -import io.element.android.features.lockscreen.api.isLocked +import io.element.android.features.lockscreen.impl.unlock.activity.PinUnlockActivity import io.element.android.libraries.architecture.bindings import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.designsystem.utils.snackbar.LocalSnackbarDispatcher import io.element.android.x.di.AppBindings import io.element.android.x.intent.SafeUriHandler +import kotlinx.coroutines.launch import timber.log.Timber private val loggerTag = LoggerTag("MainActivity") @@ -59,27 +65,13 @@ class MainActivity : NodeActivity() { installSplashScreen() super.onCreate(savedInstanceState) appBindings = bindings() - appBindings.lockScreenService().handleSecureFlag(this) + setupLockManagement(appBindings.lockScreenService()) enableEdgeToEdge() setContent { MainContent(appBindings) } } - @Deprecated("") - override fun onBackPressed() { - // If the app is locked, we need to intercept onBackPressed before it goes to OnBackPressedDispatcher. - // Indeed, otherwise we would need to trick Appyx backstack management everywhere. - // Without this trick, we would get pop operations on the hidden backstack. - if (appBindings.lockScreenService().isLocked) { - // Do not kill the app in this case, just go to background. - moveTaskToBack(false) - } else { - @Suppress("DEPRECATION") - super.onBackPressed() - } - } - @Composable private fun MainContent(appBindings: AppBindings) { val theme by remember { @@ -96,8 +88,8 @@ class MainActivity : NodeActivity() { ) { Box( modifier = Modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.background), + .fillMaxSize() + .background(MaterialTheme.colorScheme.background), ) { if (migrationState.migrationAction.isSuccess()) { MainNodeHost() @@ -131,6 +123,19 @@ class MainActivity : NodeActivity() { } } + private fun setupLockManagement(lockScreenService: LockScreenService) { + lockScreenService.handleSecureFlag(this) + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.RESUMED) { + lockScreenService.lockState.collect { state -> + if (state == LockScreenLockState.Locked) { + startActivity(PinUnlockActivity.newIntent(this@MainActivity)) + } + } + } + } + } + /** * Called when: * - the launcher icon is clicked (if the app is already running); diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt index df86219cd1..bd68a9151e 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -47,9 +47,6 @@ import io.element.android.features.createroom.api.CreateRoomEntryPoint import io.element.android.features.ftue.api.FtueEntryPoint import io.element.android.features.ftue.api.state.FtueService import io.element.android.features.ftue.api.state.FtueState -import io.element.android.features.lockscreen.api.LockScreenEntryPoint -import io.element.android.features.lockscreen.api.LockScreenLockState -import io.element.android.features.lockscreen.api.LockScreenService import io.element.android.features.networkmonitor.api.NetworkMonitor import io.element.android.features.networkmonitor.api.NetworkStatus import io.element.android.features.preferences.api.PreferencesEntryPoint @@ -100,8 +97,6 @@ class LoggedInFlowNode @AssistedInject constructor( private val coroutineScope: CoroutineScope, private val networkMonitor: NetworkMonitor, private val ftueService: FtueService, - private val lockScreenEntryPoint: LockScreenEntryPoint, - private val lockScreenStateService: LockScreenService, private val roomDirectoryEntryPoint: RoomDirectoryEntryPoint, private val matrixClient: MatrixClient, snackbarDispatcher: SnackbarDispatcher, @@ -111,7 +106,7 @@ class LoggedInFlowNode @AssistedInject constructor( savedStateMap = buildContext.savedStateMap, ), permanentNavModel = PermanentNavModel( - navTargets = setOf(NavTarget.LoggedInPermanent, NavTarget.LockPermanent), + navTargets = setOf(NavTarget.LoggedInPermanent), savedStateMap = buildContext.savedStateMap, ), buildContext = buildContext, @@ -189,9 +184,6 @@ class LoggedInFlowNode @AssistedInject constructor( @Parcelize data object LoggedInPermanent : NavTarget - @Parcelize - data object LockPermanent : NavTarget - @Parcelize data object RoomList : NavTarget @@ -235,11 +227,6 @@ class LoggedInFlowNode @AssistedInject constructor( NavTarget.LoggedInPermanent -> { createNode(buildContext) } - NavTarget.LockPermanent -> { - lockScreenEntryPoint.nodeBuilder(this, buildContext) - .target(LockScreenEntryPoint.Target.Unlock) - .build() - } NavTarget.RoomList -> { val callback = object : RoomListEntryPoint.Callback { override fun onRoomClicked(roomId: RoomId) { @@ -430,15 +417,11 @@ class LoggedInFlowNode @AssistedInject constructor( @Composable override fun View(modifier: Modifier) { Box(modifier = modifier) { - val lockScreenState by lockScreenStateService.lockState.collectAsState() val ftueState by ftueService.state.collectAsState() BackstackView() if (ftueState is FtueState.Complete) { PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.LoggedInPermanent) } - if (lockScreenState == LockScreenLockState.Locked) { - PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.LockPermanent) - } } } diff --git a/changelog.d/2692.bugfix b/changelog.d/2692.bugfix new file mode 100644 index 0000000000..3fefb20a07 --- /dev/null +++ b/changelog.d/2692.bugfix @@ -0,0 +1 @@ +Fix modal contents overlapping screen lock pin. diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt index aebf95aa37..db56b8c17b 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt @@ -29,11 +29,11 @@ import io.element.android.features.lockscreen.impl.biometric.BiometricUnlockMana import io.element.android.features.lockscreen.impl.pin.PinCodeManager import io.element.android.features.lockscreen.impl.pin.model.PinEntry import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel +import io.element.android.features.lockscreen.impl.unlock.signout.SignOut import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.runCatchingUpdatingState import io.element.android.libraries.core.bool.orFalse -import io.element.android.libraries.matrix.api.MatrixClient import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import javax.inject.Inject @@ -41,7 +41,7 @@ import javax.inject.Inject class PinUnlockPresenter @Inject constructor( private val pinCodeManager: PinCodeManager, private val biometricUnlockManager: BiometricUnlockManager, - private val matrixClient: MatrixClient, + private val signOut: SignOut, private val coroutineScope: CoroutineScope, private val pinUnlockHelper: PinUnlockHelper, ) : Presenter { @@ -179,7 +179,7 @@ class PinUnlockPresenter @Inject constructor( private fun CoroutineScope.signOut(signOutAction: MutableState>) = launch { suspend { - matrixClient.logout(ignoreSdkError = true) + signOut() }.runCatchingUpdatingState(signOutAction) } } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/activity/PinUnlockActivity.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/activity/PinUnlockActivity.kt new file mode 100644 index 0000000000..0c2de97bb2 --- /dev/null +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/activity/PinUnlockActivity.kt @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.lockscreen.impl.unlock.activity + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.activity.OnBackPressedCallback +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.lifecycleScope +import io.element.android.compound.theme.ElementTheme +import io.element.android.features.lockscreen.api.LockScreenLockState +import io.element.android.features.lockscreen.api.LockScreenService +import io.element.android.features.lockscreen.impl.unlock.PinUnlockPresenter +import io.element.android.features.lockscreen.impl.unlock.PinUnlockView +import io.element.android.features.lockscreen.impl.unlock.di.PinUnlockBindings +import io.element.android.libraries.architecture.bindings +import kotlinx.coroutines.launch +import javax.inject.Inject + +class PinUnlockActivity : AppCompatActivity() { + companion object { + fun newIntent(context: Context): Intent { + return Intent(context, PinUnlockActivity::class.java) + } + } + + @Inject lateinit var presenter: PinUnlockPresenter + @Inject lateinit var lockScreenService: LockScreenService + + override fun onCreate(savedInstanceState: Bundle?) { + enableEdgeToEdge() + super.onCreate(savedInstanceState) + bindings().inject(this) + setContent { + ElementTheme { + val state = presenter.present() + PinUnlockView(state = state, isInAppUnlock = false) + } + } + lifecycleScope.launch { + lockScreenService.lockState.collect { state -> + if (state == LockScreenLockState.Unlocked) { + finish() + } + } + } + val onBackPressedCallback = object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + moveTaskToBack(true) + } + } + onBackPressedDispatcher.addCallback(this, onBackPressedCallback) + } +} diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/di/PinUnlockBindings.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/di/PinUnlockBindings.kt new file mode 100644 index 0000000000..ddd62d2fb6 --- /dev/null +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/di/PinUnlockBindings.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.lockscreen.impl.unlock.di + +import com.squareup.anvil.annotations.ContributesTo +import io.element.android.features.lockscreen.impl.unlock.activity.PinUnlockActivity +import io.element.android.libraries.di.AppScope + +@ContributesTo(AppScope::class) +interface PinUnlockBindings { + fun inject(activity: PinUnlockActivity) +} diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/signout/DefaultSignOut.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/signout/DefaultSignOut.kt new file mode 100644 index 0000000000..2c541911e6 --- /dev/null +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/signout/DefaultSignOut.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.lockscreen.impl.unlock.signout + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.matrix.api.MatrixClientProvider +import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +class DefaultSignOut @Inject constructor( + private val authenticationService: MatrixAuthenticationService, + private val matrixClientProvider: MatrixClientProvider, +) : SignOut { + override suspend fun invoke(): String? { + val currentSession = authenticationService.getLatestSessionId() + return if (currentSession != null) { + matrixClientProvider.getOrRestore(currentSession) + .getOrThrow() + .logout(ignoreSdkError = true) + } else { + error("No session to sign out") + } + } +} diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/signout/SignOut.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/signout/SignOut.kt new file mode 100644 index 0000000000..f4c91aece6 --- /dev/null +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/signout/SignOut.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.lockscreen.impl.unlock.signout + +interface SignOut { + suspend operator fun invoke(): String? +} diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/FakeSignOut.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/FakeSignOut.kt new file mode 100644 index 0000000000..883a5bf97b --- /dev/null +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/FakeSignOut.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.lockscreen.impl.unlock + +import io.element.android.features.lockscreen.impl.unlock.signout.SignOut +import io.element.android.tests.testutils.simulateLongTask + +class FakeSignOut( + var lambda: () -> String? = { null } +) : SignOut { + override suspend fun invoke(): String? = simulateLongTask { + lambda() + } +} diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenterTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenterTest.kt index 6827055b2c..60cc3fff9d 100644 --- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenterTest.kt +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenterTest.kt @@ -29,7 +29,6 @@ import io.element.android.features.lockscreen.impl.pin.model.PinEntry import io.element.android.features.lockscreen.impl.pin.model.assertText import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel import io.element.android.libraries.architecture.AsyncData -import io.element.android.libraries.matrix.test.FakeMatrixClient import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.test.runTest import org.junit.Test @@ -150,7 +149,7 @@ class PinUnlockPresenterTest { return PinUnlockPresenter( pinCodeManager = pinCodeManager, biometricUnlockManager = biometricUnlockManager, - matrixClient = FakeMatrixClient(), + signOut = FakeSignOut(), coroutineScope = scope, pinUnlockHelper = PinUnlockHelper(biometricUnlockManager, pinCodeManager), )