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..eb3f9450f5 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.LockScreenEntryPoint +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.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(), appBindings.lockScreenEntryPoint()) 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,22 @@ class MainActivity : NodeActivity() { } } + private fun setupLockManagement( + lockScreenService: LockScreenService, + lockScreenEntryPoint: LockScreenEntryPoint + ) { + lockScreenService.handleSecureFlag(this) + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.RESUMED) { + lockScreenService.lockState.collect { state -> + if (state == LockScreenLockState.Locked) { + startActivity(lockScreenEntryPoint.pinUnlockIntent(this@MainActivity)) + } + } + } + } + } + /** * Called when: * - the launcher icon is clicked (if the app is already running); diff --git a/app/src/main/kotlin/io/element/android/x/di/AppBindings.kt b/app/src/main/kotlin/io/element/android/x/di/AppBindings.kt index d8be841b97..bad34edfa0 100644 --- a/app/src/main/kotlin/io/element/android/x/di/AppBindings.kt +++ b/app/src/main/kotlin/io/element/android/x/di/AppBindings.kt @@ -18,6 +18,7 @@ package io.element.android.x.di import com.squareup.anvil.annotations.ContributesTo import io.element.android.features.api.MigrationEntryPoint +import io.element.android.features.lockscreen.api.LockScreenEntryPoint import io.element.android.features.lockscreen.api.LockScreenService import io.element.android.features.preferences.api.store.AppPreferencesStore import io.element.android.features.rageshake.api.reporter.BugReporter @@ -38,4 +39,6 @@ interface AppBindings { fun preferencesStore(): AppPreferencesStore fun migrationEntryPoint(): MigrationEntryPoint + + fun lockScreenEntryPoint(): LockScreenEntryPoint } 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/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/FtueFlowNode.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/FtueFlowNode.kt index 772343ea58..6720fa0274 100644 --- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/FtueFlowNode.kt +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/FtueFlowNode.kt @@ -132,9 +132,8 @@ class FtueFlowNode @AssistedInject constructor( lifecycleScope.launch { moveToNextStep() } } } - lockScreenEntryPoint.nodeBuilder(this, buildContext) + lockScreenEntryPoint.nodeBuilder(this, buildContext, LockScreenEntryPoint.Target.Setup) .callback(callback) - .target(LockScreenEntryPoint.Target.Setup) .build() } } diff --git a/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenEntryPoint.kt b/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenEntryPoint.kt index 6a2fb0c72d..f31fc5af4a 100644 --- a/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenEntryPoint.kt +++ b/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenEntryPoint.kt @@ -16,17 +16,19 @@ package io.element.android.features.lockscreen.api +import android.content.Context +import android.content.Intent import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin import io.element.android.libraries.architecture.FeatureEntryPoint interface LockScreenEntryPoint : FeatureEntryPoint { - fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder + fun nodeBuilder(parentNode: Node, buildContext: BuildContext, navTarget: Target): NodeBuilder + fun pinUnlockIntent(context: Context): Intent interface NodeBuilder { fun callback(callback: Callback): NodeBuilder - fun target(target: Target): NodeBuilder fun build(): Node } @@ -37,6 +39,5 @@ interface LockScreenEntryPoint : FeatureEntryPoint { enum class Target { Settings, Setup, - Unlock } } diff --git a/features/lockscreen/impl/src/main/AndroidManifest.xml b/features/lockscreen/impl/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..083647d1c2 --- /dev/null +++ b/features/lockscreen/impl/src/main/AndroidManifest.xml @@ -0,0 +1,25 @@ + + + + + + + + + diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenEntryPoint.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenEntryPoint.kt index 5065cdc7f2..a6889a96e7 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenEntryPoint.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenEntryPoint.kt @@ -16,18 +16,20 @@ package io.element.android.features.lockscreen.impl +import android.content.Context +import android.content.Intent import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node import com.squareup.anvil.annotations.ContributesBinding import io.element.android.features.lockscreen.api.LockScreenEntryPoint +import io.element.android.features.lockscreen.impl.unlock.activity.PinUnlockActivity import io.element.android.libraries.architecture.createNode import io.element.android.libraries.di.AppScope import javax.inject.Inject @ContributesBinding(AppScope::class) class DefaultLockScreenEntryPoint @Inject constructor() : LockScreenEntryPoint { - override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): LockScreenEntryPoint.NodeBuilder { - var innerTarget: LockScreenEntryPoint.Target = LockScreenEntryPoint.Target.Unlock + override fun nodeBuilder(parentNode: Node, buildContext: BuildContext, navTarget: LockScreenEntryPoint.Target): LockScreenEntryPoint.NodeBuilder { val callbacks = mutableListOf() return object : LockScreenEntryPoint.NodeBuilder { @@ -36,15 +38,9 @@ class DefaultLockScreenEntryPoint @Inject constructor() : LockScreenEntryPoint { return this } - override fun target(target: LockScreenEntryPoint.Target): LockScreenEntryPoint.NodeBuilder { - innerTarget = target - return this - } - override fun build(): Node { val inputs = LockScreenFlowNode.Inputs( - when (innerTarget) { - LockScreenEntryPoint.Target.Unlock -> LockScreenFlowNode.NavTarget.Unlock + when (navTarget) { LockScreenEntryPoint.Target.Setup -> LockScreenFlowNode.NavTarget.Setup LockScreenEntryPoint.Target.Settings -> LockScreenFlowNode.NavTarget.Settings } @@ -54,4 +50,8 @@ class DefaultLockScreenEntryPoint @Inject constructor() : LockScreenEntryPoint { } } } + + override fun pinUnlockIntent(context: Context): Intent { + return PinUnlockActivity.newIntent(context) + } } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/LockScreenFlowNode.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/LockScreenFlowNode.kt index 8dd75fc65d..fcbb0336a4 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/LockScreenFlowNode.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/LockScreenFlowNode.kt @@ -30,7 +30,6 @@ import io.element.android.anvilannotations.ContributesNode import io.element.android.features.lockscreen.api.LockScreenEntryPoint import io.element.android.features.lockscreen.impl.settings.LockScreenSettingsFlowNode import io.element.android.features.lockscreen.impl.setup.LockScreenSetupFlowNode -import io.element.android.features.lockscreen.impl.unlock.PinUnlockNode import io.element.android.libraries.architecture.BackstackView import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.NodeInputs @@ -44,20 +43,17 @@ class LockScreenFlowNode @AssistedInject constructor( @Assisted plugins: List, ) : BaseFlowNode( backstack = BackStack( - initialElement = plugins.filterIsInstance(Inputs::class.java).first().initialNavTarget, + initialElement = plugins.filterIsInstance().first().initialNavTarget, savedStateMap = buildContext.savedStateMap, ), buildContext = buildContext, plugins = plugins, ) { data class Inputs( - val initialNavTarget: NavTarget = NavTarget.Unlock, + val initialNavTarget: NavTarget, ) : NodeInputs sealed interface NavTarget : Parcelable { - @Parcelize - data object Unlock : NavTarget - @Parcelize data object Setup : NavTarget @@ -75,10 +71,6 @@ class LockScreenFlowNode @AssistedInject constructor( override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { return when (navTarget) { - NavTarget.Unlock -> { - val inputs = PinUnlockNode.Inputs(isInAppUnlock = false) - createNode(buildContext, plugins = listOf(inputs)) - } NavTarget.Setup -> { val callback = OnSetupDoneCallback(plugins()) createNode(buildContext, plugins = listOf(callback)) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsFlowNode.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsFlowNode.kt index 0890f38d1e..13fe1e62aa 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsFlowNode.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsFlowNode.kt @@ -103,13 +103,12 @@ class LockScreenSettingsFlowNode @AssistedInject constructor( override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { return when (navTarget) { NavTarget.Unlock -> { - val inputs = PinUnlockNode.Inputs(isInAppUnlock = true) val callback = object : PinUnlockNode.Callback { override fun onUnlock() { backstack.newRoot(NavTarget.Settings) } } - createNode(buildContext, plugins = listOf(inputs, callback)) + createNode(buildContext, plugins = listOf(callback)) } NavTarget.SetupPin -> { createNode(buildContext) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockNode.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockNode.kt index da6853a39c..f357869375 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockNode.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockNode.kt @@ -26,8 +26,6 @@ import com.bumble.appyx.core.plugin.plugins import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode -import io.element.android.libraries.architecture.NodeInputs -import io.element.android.libraries.architecture.inputs import io.element.android.libraries.di.SessionScope @ContributesNode(SessionScope::class) @@ -40,12 +38,6 @@ class PinUnlockNode @AssistedInject constructor( fun onUnlock() } - data class Inputs( - val isInAppUnlock: Boolean - ) : NodeInputs - - private val inputs: Inputs = inputs() - private fun onUnlock() { plugins().forEach { it.onUnlock() @@ -62,7 +54,9 @@ class PinUnlockNode @AssistedInject constructor( } PinUnlockView( state = state, - isInAppUnlock = inputs.isInAppUnlock, + // UnlockNode is only used for in-app unlock, so we can safely set isInAppUnlock to true. + // It's set to false in PinUnlockActivity. + isInAppUnlock = true, modifier = modifier ) } 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..9c228b0736 --- /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() { + internal 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/DefaultSignOutTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/DefaultSignOutTest.kt new file mode 100644 index 0000000000..6870a336d3 --- /dev/null +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/DefaultSignOutTest.kt @@ -0,0 +1,61 @@ +/* + * 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 com.google.common.truth.Truth.assertThat +import io.element.android.features.lockscreen.impl.unlock.signout.DefaultSignOut +import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.matrix.test.FakeMatrixClientProvider +import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService +import io.element.android.tests.testutils.lambda.assert +import io.element.android.tests.testutils.lambda.lambdaRecorder +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class DefaultSignOutTest { + private val matrixClient = FakeMatrixClient() + private val authenticationService = FakeAuthenticationService() + private val matrixClientProvider = FakeMatrixClientProvider(getClient = { Result.success(matrixClient) }) + private val sut = DefaultSignOut(authenticationService, matrixClientProvider) + + @Test + fun `when no active session then it throws`() = runTest { + authenticationService.getLatestSessionIdLambda = { null } + val result = runCatching { sut.invoke() } + assertThat(result.isFailure).isTrue() + } + + @Test + fun `with one active session and successful logout on client`() = runTest { + val logoutLambda = lambdaRecorder { _: Boolean -> null } + authenticationService.getLatestSessionIdLambda = { matrixClient.sessionId } + matrixClient.logoutLambda = logoutLambda + val result = runCatching { sut.invoke() } + assertThat(result.isSuccess).isTrue() + assert(logoutLambda).isCalledOnce() + } + + @Test + fun `with one active session and and failed logout on client`() = runTest { + val logoutLambda = lambdaRecorder { _: Boolean -> error("Failed to logout") } + authenticationService.getLatestSessionIdLambda = { matrixClient.sessionId } + matrixClient.logoutLambda = logoutLambda + val result = runCatching { sut.invoke() } + assertThat(result.isFailure).isTrue() + assert(logoutLambda).isCalledOnce() + } +} 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..89d0e92ee2 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 @@ -28,8 +28,10 @@ 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.pin.model.assertText 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.matrix.test.FakeMatrixClient +import io.element.android.tests.testutils.lambda.assert +import io.element.android.tests.testutils.lambda.lambdaRecorder import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.test.runTest import org.junit.Test @@ -104,7 +106,9 @@ class PinUnlockPresenterTest { @Test fun `present - forgot pin flow`() = runTest { - val presenter = createPinUnlockPresenter(this) + val signOutLambda = lambdaRecorder { null } + val signOut = FakeSignOut(signOutLambda) + val presenter = createPinUnlockPresenter(this, signOut = signOut) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -131,6 +135,7 @@ class PinUnlockPresenterTest { awaitItem().also { state -> assertThat(state.signOutAction).isInstanceOf(AsyncData.Success::class.java) } + assert(signOutLambda).isCalledOnce().withNoParameter() } } @@ -142,6 +147,7 @@ class PinUnlockPresenterTest { scope: CoroutineScope, biometricUnlockManager: BiometricUnlockManager = FakeBiometricUnlockManager(), callback: PinCodeManager.Callback = DefaultPinCodeManagerCallback(), + signOut: SignOut = FakeSignOut(), ): PinUnlockPresenter { val pinCodeManager = aPinCodeManager().apply { addCallback(callback) @@ -150,7 +156,7 @@ class PinUnlockPresenterTest { return PinUnlockPresenter( pinCodeManager = pinCodeManager, biometricUnlockManager = biometricUnlockManager, - matrixClient = FakeMatrixClient(), + signOut = signOut, coroutineScope = scope, pinUnlockHelper = PinUnlockHelper(biometricUnlockManager, pinCodeManager), ) diff --git a/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutPresenterTest.kt b/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutPresenterTest.kt index 9531ea8886..70b346ba8d 100644 --- a/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutPresenterTest.kt +++ b/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutPresenterTest.kt @@ -144,7 +144,9 @@ class LogoutPresenterTest { @Test fun `present - logout with error then cancel`() = runTest { val matrixClient = FakeMatrixClient().apply { - givenLogoutError(A_THROWABLE) + logoutLambda = { _ -> + throw A_THROWABLE + } } val presenter = createLogoutPresenter( matrixClient, @@ -170,7 +172,13 @@ class LogoutPresenterTest { @Test fun `present - logout with error then force`() = runTest { val matrixClient = FakeMatrixClient().apply { - givenLogoutError(A_THROWABLE) + logoutLambda = { ignoreSdkError -> + if (!ignoreSdkError) { + throw A_THROWABLE + } else { + null + } + } } val presenter = createLogoutPresenter( matrixClient, diff --git a/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutPresenterTest.kt b/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutPresenterTest.kt index bf3df93731..14d340570c 100644 --- a/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutPresenterTest.kt +++ b/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutPresenterTest.kt @@ -125,7 +125,9 @@ class DefaultDirectLogoutPresenterTest { @Test fun `present - logout with error then cancel`() = runTest { val matrixClient = FakeMatrixClient().apply { - givenLogoutError(A_THROWABLE) + logoutLambda = { _ -> + throw A_THROWABLE + } } val presenter = createDefaultDirectLogoutPresenter( matrixClient, @@ -151,7 +153,13 @@ class DefaultDirectLogoutPresenterTest { @Test fun `present - logout with error then force`() = runTest { val matrixClient = FakeMatrixClient().apply { - givenLogoutError(A_THROWABLE) + logoutLambda = { ignoreSdkError -> + if (!ignoreSdkError) { + throw A_THROWABLE + } else { + null + } + } } val presenter = createDefaultDirectLogoutPresenter( matrixClient, diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/PreferencesFlowNode.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/PreferencesFlowNode.kt index b93e02dd39..ac8a881348 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/PreferencesFlowNode.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/PreferencesFlowNode.kt @@ -212,9 +212,7 @@ class PreferencesFlowNode @AssistedInject constructor( createNode(buildContext, listOf(inputs)) } NavTarget.LockScreenSettings -> { - lockScreenEntryPoint.nodeBuilder(this, buildContext) - .target(LockScreenEntryPoint.Target.Settings) - .build() + lockScreenEntryPoint.nodeBuilder(this, buildContext, LockScreenEntryPoint.Target.Settings).build() } NavTarget.BlockedUsers -> { createNode(buildContext) diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt index 628c99903f..17e9bedfcf 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt @@ -53,7 +53,6 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.flowOf @@ -95,7 +94,6 @@ class FakeMatrixClient( private var createRoomResult: Result = Result.success(A_ROOM_ID) private var createDmResult: Result = Result.success(A_ROOM_ID) private var findDmResult: RoomId? = A_ROOM_ID - private var logoutFailure: Throwable? = null private val getRoomResults = mutableMapOf() private val searchUserResults = mutableMapOf>() private val getProfileResults = mutableMapOf>() @@ -115,6 +113,9 @@ class FakeMatrixClient( var getRoomInfoFlowLambda = { _: RoomId -> flowOf>(Optional.empty()) } + var logoutLambda: (Boolean) -> String? = { + null + } override suspend fun getRoom(roomId: RoomId): MatrixRoom? { return getRoomResults[roomId] @@ -159,12 +160,8 @@ class FakeMatrixClient( override suspend fun clearCache() { } - override suspend fun logout(ignoreSdkError: Boolean): String? { - delay(100) - if (ignoreSdkError.not()) { - logoutFailure?.let { throw it } - } - return null + override suspend fun logout(ignoreSdkError: Boolean): String? = simulateLongTask { + return logoutLambda(ignoreSdkError) } override fun close() = Unit @@ -228,10 +225,6 @@ class FakeMatrixClient( // Mocks - fun givenLogoutError(failure: Throwable?) { - logoutFailure = failure - } - fun givenCreateRoomResult(result: Result) { createRoomResult = result } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeAuthenticationService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeAuthenticationService.kt index 2cf6b77a78..c24df7d717 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeAuthenticationService.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeAuthenticationService.kt @@ -39,13 +39,13 @@ class FakeAuthenticationService : MatrixAuthenticationService { private var changeServerError: Throwable? = null private var matrixClient: MatrixClient? = null + var getLatestSessionIdLambda: (() -> SessionId?) = { null } + override fun loggedInStateFlow(): Flow { return flowOf(LoggedInState.NotLoggedIn) } - override suspend fun getLatestSessionId(): SessionId? { - return null - } + override suspend fun getLatestSessionId(): SessionId? = getLatestSessionIdLambda() override suspend fun restoreSession(sessionId: SessionId): Result { return if (matrixClient != null) {