Merge pull request #2874 from element-hq/feature/fga/fix_2692
Fix modal contents overlapping screen lock pin #2692
This commit is contained in:
commit
509e080fc2
24 changed files with 364 additions and 99 deletions
|
|
@ -32,6 +32,9 @@ import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalUriHandler
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
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.integration.NodeHost
|
||||||
import com.bumble.appyx.core.integrationpoint.NodeActivity
|
import com.bumble.appyx.core.integrationpoint.NodeActivity
|
||||||
import com.bumble.appyx.core.plugin.NodeReadyObserver
|
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.Theme
|
||||||
import io.element.android.compound.theme.isDark
|
import io.element.android.compound.theme.isDark
|
||||||
import io.element.android.compound.theme.mapToTheme
|
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.handleSecureFlag
|
||||||
import io.element.android.features.lockscreen.api.isLocked
|
|
||||||
import io.element.android.libraries.architecture.bindings
|
import io.element.android.libraries.architecture.bindings
|
||||||
import io.element.android.libraries.core.log.logger.LoggerTag
|
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.LocalSnackbarDispatcher
|
import io.element.android.libraries.designsystem.utils.snackbar.LocalSnackbarDispatcher
|
||||||
import io.element.android.x.di.AppBindings
|
import io.element.android.x.di.AppBindings
|
||||||
import io.element.android.x.intent.SafeUriHandler
|
import io.element.android.x.intent.SafeUriHandler
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
private val loggerTag = LoggerTag("MainActivity")
|
private val loggerTag = LoggerTag("MainActivity")
|
||||||
|
|
@ -59,27 +65,13 @@ class MainActivity : NodeActivity() {
|
||||||
installSplashScreen()
|
installSplashScreen()
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
appBindings = bindings()
|
appBindings = bindings()
|
||||||
appBindings.lockScreenService().handleSecureFlag(this)
|
setupLockManagement(appBindings.lockScreenService(), appBindings.lockScreenEntryPoint())
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
setContent {
|
setContent {
|
||||||
MainContent(appBindings)
|
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
|
@Composable
|
||||||
private fun MainContent(appBindings: AppBindings) {
|
private fun MainContent(appBindings: AppBindings) {
|
||||||
val theme by remember {
|
val theme by remember {
|
||||||
|
|
@ -96,8 +88,8 @@ class MainActivity : NodeActivity() {
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(MaterialTheme.colorScheme.background),
|
.background(MaterialTheme.colorScheme.background),
|
||||||
) {
|
) {
|
||||||
if (migrationState.migrationAction.isSuccess()) {
|
if (migrationState.migrationAction.isSuccess()) {
|
||||||
MainNodeHost()
|
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:
|
* Called when:
|
||||||
* - the launcher icon is clicked (if the app is already running);
|
* - the launcher icon is clicked (if the app is already running);
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ package io.element.android.x.di
|
||||||
|
|
||||||
import com.squareup.anvil.annotations.ContributesTo
|
import com.squareup.anvil.annotations.ContributesTo
|
||||||
import io.element.android.features.api.MigrationEntryPoint
|
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.lockscreen.api.LockScreenService
|
||||||
import io.element.android.features.preferences.api.store.AppPreferencesStore
|
import io.element.android.features.preferences.api.store.AppPreferencesStore
|
||||||
import io.element.android.features.rageshake.api.reporter.BugReporter
|
import io.element.android.features.rageshake.api.reporter.BugReporter
|
||||||
|
|
@ -38,4 +39,6 @@ interface AppBindings {
|
||||||
fun preferencesStore(): AppPreferencesStore
|
fun preferencesStore(): AppPreferencesStore
|
||||||
|
|
||||||
fun migrationEntryPoint(): MigrationEntryPoint
|
fun migrationEntryPoint(): MigrationEntryPoint
|
||||||
|
|
||||||
|
fun lockScreenEntryPoint(): LockScreenEntryPoint
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.FtueEntryPoint
|
||||||
import io.element.android.features.ftue.api.state.FtueService
|
import io.element.android.features.ftue.api.state.FtueService
|
||||||
import io.element.android.features.ftue.api.state.FtueState
|
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.NetworkMonitor
|
||||||
import io.element.android.features.networkmonitor.api.NetworkStatus
|
import io.element.android.features.networkmonitor.api.NetworkStatus
|
||||||
import io.element.android.features.preferences.api.PreferencesEntryPoint
|
import io.element.android.features.preferences.api.PreferencesEntryPoint
|
||||||
|
|
@ -100,8 +97,6 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
private val coroutineScope: CoroutineScope,
|
private val coroutineScope: CoroutineScope,
|
||||||
private val networkMonitor: NetworkMonitor,
|
private val networkMonitor: NetworkMonitor,
|
||||||
private val ftueService: FtueService,
|
private val ftueService: FtueService,
|
||||||
private val lockScreenEntryPoint: LockScreenEntryPoint,
|
|
||||||
private val lockScreenStateService: LockScreenService,
|
|
||||||
private val roomDirectoryEntryPoint: RoomDirectoryEntryPoint,
|
private val roomDirectoryEntryPoint: RoomDirectoryEntryPoint,
|
||||||
private val matrixClient: MatrixClient,
|
private val matrixClient: MatrixClient,
|
||||||
snackbarDispatcher: SnackbarDispatcher,
|
snackbarDispatcher: SnackbarDispatcher,
|
||||||
|
|
@ -111,7 +106,7 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
savedStateMap = buildContext.savedStateMap,
|
savedStateMap = buildContext.savedStateMap,
|
||||||
),
|
),
|
||||||
permanentNavModel = PermanentNavModel(
|
permanentNavModel = PermanentNavModel(
|
||||||
navTargets = setOf(NavTarget.LoggedInPermanent, NavTarget.LockPermanent),
|
navTargets = setOf(NavTarget.LoggedInPermanent),
|
||||||
savedStateMap = buildContext.savedStateMap,
|
savedStateMap = buildContext.savedStateMap,
|
||||||
),
|
),
|
||||||
buildContext = buildContext,
|
buildContext = buildContext,
|
||||||
|
|
@ -189,9 +184,6 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object LoggedInPermanent : NavTarget
|
data object LoggedInPermanent : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data object LockPermanent : NavTarget
|
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object RoomList : NavTarget
|
data object RoomList : NavTarget
|
||||||
|
|
||||||
|
|
@ -235,11 +227,6 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
NavTarget.LoggedInPermanent -> {
|
NavTarget.LoggedInPermanent -> {
|
||||||
createNode<LoggedInNode>(buildContext)
|
createNode<LoggedInNode>(buildContext)
|
||||||
}
|
}
|
||||||
NavTarget.LockPermanent -> {
|
|
||||||
lockScreenEntryPoint.nodeBuilder(this, buildContext)
|
|
||||||
.target(LockScreenEntryPoint.Target.Unlock)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
NavTarget.RoomList -> {
|
NavTarget.RoomList -> {
|
||||||
val callback = object : RoomListEntryPoint.Callback {
|
val callback = object : RoomListEntryPoint.Callback {
|
||||||
override fun onRoomClicked(roomId: RoomId) {
|
override fun onRoomClicked(roomId: RoomId) {
|
||||||
|
|
@ -430,15 +417,11 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
@Composable
|
@Composable
|
||||||
override fun View(modifier: Modifier) {
|
override fun View(modifier: Modifier) {
|
||||||
Box(modifier = modifier) {
|
Box(modifier = modifier) {
|
||||||
val lockScreenState by lockScreenStateService.lockState.collectAsState()
|
|
||||||
val ftueState by ftueService.state.collectAsState()
|
val ftueState by ftueService.state.collectAsState()
|
||||||
BackstackView()
|
BackstackView()
|
||||||
if (ftueState is FtueState.Complete) {
|
if (ftueState is FtueState.Complete) {
|
||||||
PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.LoggedInPermanent)
|
PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.LoggedInPermanent)
|
||||||
}
|
}
|
||||||
if (lockScreenState == LockScreenLockState.Locked) {
|
|
||||||
PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.LockPermanent)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
1
changelog.d/2692.bugfix
Normal file
1
changelog.d/2692.bugfix
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Fix modal contents overlapping screen lock pin.
|
||||||
|
|
@ -132,9 +132,8 @@ class FtueFlowNode @AssistedInject constructor(
|
||||||
lifecycleScope.launch { moveToNextStep() }
|
lifecycleScope.launch { moveToNextStep() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lockScreenEntryPoint.nodeBuilder(this, buildContext)
|
lockScreenEntryPoint.nodeBuilder(this, buildContext, LockScreenEntryPoint.Target.Setup)
|
||||||
.callback(callback)
|
.callback(callback)
|
||||||
.target(LockScreenEntryPoint.Target.Setup)
|
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,17 +16,19 @@
|
||||||
|
|
||||||
package io.element.android.features.lockscreen.api
|
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.modality.BuildContext
|
||||||
import com.bumble.appyx.core.node.Node
|
import com.bumble.appyx.core.node.Node
|
||||||
import com.bumble.appyx.core.plugin.Plugin
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
import io.element.android.libraries.architecture.FeatureEntryPoint
|
import io.element.android.libraries.architecture.FeatureEntryPoint
|
||||||
|
|
||||||
interface LockScreenEntryPoint : 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 {
|
interface NodeBuilder {
|
||||||
fun callback(callback: Callback): NodeBuilder
|
fun callback(callback: Callback): NodeBuilder
|
||||||
fun target(target: Target): NodeBuilder
|
|
||||||
fun build(): Node
|
fun build(): Node
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -37,6 +39,5 @@ interface LockScreenEntryPoint : FeatureEntryPoint {
|
||||||
enum class Target {
|
enum class Target {
|
||||||
Settings,
|
Settings,
|
||||||
Setup,
|
Setup,
|
||||||
Unlock
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
25
features/lockscreen/impl/src/main/AndroidManifest.xml
Normal file
25
features/lockscreen/impl/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
<!--
|
||||||
|
~ 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.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<application>
|
||||||
|
<activity
|
||||||
|
android:name="io.element.android.features.lockscreen.impl.unlock.activity.PinUnlockActivity"
|
||||||
|
android:configChanges="screenSize|screenLayout|orientation|keyboardHidden|keyboard|navigation|uiMode"/>
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
||||||
|
|
@ -16,18 +16,20 @@
|
||||||
|
|
||||||
package io.element.android.features.lockscreen.impl
|
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.modality.BuildContext
|
||||||
import com.bumble.appyx.core.node.Node
|
import com.bumble.appyx.core.node.Node
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
import io.element.android.features.lockscreen.api.LockScreenEntryPoint
|
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.architecture.createNode
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.AppScope
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@ContributesBinding(AppScope::class)
|
@ContributesBinding(AppScope::class)
|
||||||
class DefaultLockScreenEntryPoint @Inject constructor() : LockScreenEntryPoint {
|
class DefaultLockScreenEntryPoint @Inject constructor() : LockScreenEntryPoint {
|
||||||
override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): LockScreenEntryPoint.NodeBuilder {
|
override fun nodeBuilder(parentNode: Node, buildContext: BuildContext, navTarget: LockScreenEntryPoint.Target): LockScreenEntryPoint.NodeBuilder {
|
||||||
var innerTarget: LockScreenEntryPoint.Target = LockScreenEntryPoint.Target.Unlock
|
|
||||||
val callbacks = mutableListOf<LockScreenEntryPoint.Callback>()
|
val callbacks = mutableListOf<LockScreenEntryPoint.Callback>()
|
||||||
|
|
||||||
return object : LockScreenEntryPoint.NodeBuilder {
|
return object : LockScreenEntryPoint.NodeBuilder {
|
||||||
|
|
@ -36,15 +38,9 @@ class DefaultLockScreenEntryPoint @Inject constructor() : LockScreenEntryPoint {
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun target(target: LockScreenEntryPoint.Target): LockScreenEntryPoint.NodeBuilder {
|
|
||||||
innerTarget = target
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun build(): Node {
|
override fun build(): Node {
|
||||||
val inputs = LockScreenFlowNode.Inputs(
|
val inputs = LockScreenFlowNode.Inputs(
|
||||||
when (innerTarget) {
|
when (navTarget) {
|
||||||
LockScreenEntryPoint.Target.Unlock -> LockScreenFlowNode.NavTarget.Unlock
|
|
||||||
LockScreenEntryPoint.Target.Setup -> LockScreenFlowNode.NavTarget.Setup
|
LockScreenEntryPoint.Target.Setup -> LockScreenFlowNode.NavTarget.Setup
|
||||||
LockScreenEntryPoint.Target.Settings -> LockScreenFlowNode.NavTarget.Settings
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,6 @@ import io.element.android.anvilannotations.ContributesNode
|
||||||
import io.element.android.features.lockscreen.api.LockScreenEntryPoint
|
import io.element.android.features.lockscreen.api.LockScreenEntryPoint
|
||||||
import io.element.android.features.lockscreen.impl.settings.LockScreenSettingsFlowNode
|
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.setup.LockScreenSetupFlowNode
|
||||||
import io.element.android.features.lockscreen.impl.unlock.PinUnlockNode
|
|
||||||
import io.element.android.libraries.architecture.BackstackView
|
import io.element.android.libraries.architecture.BackstackView
|
||||||
import io.element.android.libraries.architecture.BaseFlowNode
|
import io.element.android.libraries.architecture.BaseFlowNode
|
||||||
import io.element.android.libraries.architecture.NodeInputs
|
import io.element.android.libraries.architecture.NodeInputs
|
||||||
|
|
@ -44,20 +43,17 @@ class LockScreenFlowNode @AssistedInject constructor(
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
) : BaseFlowNode<LockScreenFlowNode.NavTarget>(
|
) : BaseFlowNode<LockScreenFlowNode.NavTarget>(
|
||||||
backstack = BackStack(
|
backstack = BackStack(
|
||||||
initialElement = plugins.filterIsInstance(Inputs::class.java).first().initialNavTarget,
|
initialElement = plugins.filterIsInstance<Inputs>().first().initialNavTarget,
|
||||||
savedStateMap = buildContext.savedStateMap,
|
savedStateMap = buildContext.savedStateMap,
|
||||||
),
|
),
|
||||||
buildContext = buildContext,
|
buildContext = buildContext,
|
||||||
plugins = plugins,
|
plugins = plugins,
|
||||||
) {
|
) {
|
||||||
data class Inputs(
|
data class Inputs(
|
||||||
val initialNavTarget: NavTarget = NavTarget.Unlock,
|
val initialNavTarget: NavTarget,
|
||||||
) : NodeInputs
|
) : NodeInputs
|
||||||
|
|
||||||
sealed interface NavTarget : Parcelable {
|
sealed interface NavTarget : Parcelable {
|
||||||
@Parcelize
|
|
||||||
data object Unlock : NavTarget
|
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object Setup : NavTarget
|
data object Setup : NavTarget
|
||||||
|
|
||||||
|
|
@ -75,10 +71,6 @@ class LockScreenFlowNode @AssistedInject constructor(
|
||||||
|
|
||||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||||
return when (navTarget) {
|
return when (navTarget) {
|
||||||
NavTarget.Unlock -> {
|
|
||||||
val inputs = PinUnlockNode.Inputs(isInAppUnlock = false)
|
|
||||||
createNode<PinUnlockNode>(buildContext, plugins = listOf(inputs))
|
|
||||||
}
|
|
||||||
NavTarget.Setup -> {
|
NavTarget.Setup -> {
|
||||||
val callback = OnSetupDoneCallback(plugins())
|
val callback = OnSetupDoneCallback(plugins())
|
||||||
createNode<LockScreenSetupFlowNode>(buildContext, plugins = listOf(callback))
|
createNode<LockScreenSetupFlowNode>(buildContext, plugins = listOf(callback))
|
||||||
|
|
|
||||||
|
|
@ -103,13 +103,12 @@ class LockScreenSettingsFlowNode @AssistedInject constructor(
|
||||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||||
return when (navTarget) {
|
return when (navTarget) {
|
||||||
NavTarget.Unlock -> {
|
NavTarget.Unlock -> {
|
||||||
val inputs = PinUnlockNode.Inputs(isInAppUnlock = true)
|
|
||||||
val callback = object : PinUnlockNode.Callback {
|
val callback = object : PinUnlockNode.Callback {
|
||||||
override fun onUnlock() {
|
override fun onUnlock() {
|
||||||
backstack.newRoot(NavTarget.Settings)
|
backstack.newRoot(NavTarget.Settings)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
createNode<PinUnlockNode>(buildContext, plugins = listOf(inputs, callback))
|
createNode<PinUnlockNode>(buildContext, plugins = listOf(callback))
|
||||||
}
|
}
|
||||||
NavTarget.SetupPin -> {
|
NavTarget.SetupPin -> {
|
||||||
createNode<SetupPinNode>(buildContext)
|
createNode<SetupPinNode>(buildContext)
|
||||||
|
|
|
||||||
|
|
@ -26,8 +26,6 @@ import com.bumble.appyx.core.plugin.plugins
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
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
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
|
||||||
@ContributesNode(SessionScope::class)
|
@ContributesNode(SessionScope::class)
|
||||||
|
|
@ -40,12 +38,6 @@ class PinUnlockNode @AssistedInject constructor(
|
||||||
fun onUnlock()
|
fun onUnlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Inputs(
|
|
||||||
val isInAppUnlock: Boolean
|
|
||||||
) : NodeInputs
|
|
||||||
|
|
||||||
private val inputs: Inputs = inputs()
|
|
||||||
|
|
||||||
private fun onUnlock() {
|
private fun onUnlock() {
|
||||||
plugins<Callback>().forEach {
|
plugins<Callback>().forEach {
|
||||||
it.onUnlock()
|
it.onUnlock()
|
||||||
|
|
@ -62,7 +54,9 @@ class PinUnlockNode @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
PinUnlockView(
|
PinUnlockView(
|
||||||
state = state,
|
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
|
modifier = modifier
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.PinCodeManager
|
||||||
import io.element.android.features.lockscreen.impl.pin.model.PinEntry
|
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.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.AsyncData
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
||||||
import io.element.android.libraries.core.bool.orFalse
|
import io.element.android.libraries.core.bool.orFalse
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
@ -41,7 +41,7 @@ import javax.inject.Inject
|
||||||
class PinUnlockPresenter @Inject constructor(
|
class PinUnlockPresenter @Inject constructor(
|
||||||
private val pinCodeManager: PinCodeManager,
|
private val pinCodeManager: PinCodeManager,
|
||||||
private val biometricUnlockManager: BiometricUnlockManager,
|
private val biometricUnlockManager: BiometricUnlockManager,
|
||||||
private val matrixClient: MatrixClient,
|
private val signOut: SignOut,
|
||||||
private val coroutineScope: CoroutineScope,
|
private val coroutineScope: CoroutineScope,
|
||||||
private val pinUnlockHelper: PinUnlockHelper,
|
private val pinUnlockHelper: PinUnlockHelper,
|
||||||
) : Presenter<PinUnlockState> {
|
) : Presenter<PinUnlockState> {
|
||||||
|
|
@ -179,7 +179,7 @@ class PinUnlockPresenter @Inject constructor(
|
||||||
|
|
||||||
private fun CoroutineScope.signOut(signOutAction: MutableState<AsyncData<String?>>) = launch {
|
private fun CoroutineScope.signOut(signOutAction: MutableState<AsyncData<String?>>) = launch {
|
||||||
suspend {
|
suspend {
|
||||||
matrixClient.logout(ignoreSdkError = true)
|
signOut()
|
||||||
}.runCatchingUpdatingState(signOutAction)
|
}.runCatchingUpdatingState(signOutAction)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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<PinUnlockBindings>().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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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?
|
||||||
|
}
|
||||||
|
|
@ -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, String?> { _: 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, String?> { _: Boolean -> error("Failed to logout") }
|
||||||
|
authenticationService.getLatestSessionIdLambda = { matrixClient.sessionId }
|
||||||
|
matrixClient.logoutLambda = logoutLambda
|
||||||
|
val result = runCatching { sut.invoke() }
|
||||||
|
assertThat(result.isFailure).isTrue()
|
||||||
|
assert(logoutLambda).isCalledOnce()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.PinEntry
|
||||||
import io.element.android.features.lockscreen.impl.pin.model.assertText
|
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.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.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.CoroutineScope
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
@ -104,7 +106,9 @@ class PinUnlockPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - forgot pin flow`() = runTest {
|
fun `present - forgot pin flow`() = runTest {
|
||||||
val presenter = createPinUnlockPresenter(this)
|
val signOutLambda = lambdaRecorder<String?> { null }
|
||||||
|
val signOut = FakeSignOut(signOutLambda)
|
||||||
|
val presenter = createPinUnlockPresenter(this, signOut = signOut)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -131,6 +135,7 @@ class PinUnlockPresenterTest {
|
||||||
awaitItem().also { state ->
|
awaitItem().also { state ->
|
||||||
assertThat(state.signOutAction).isInstanceOf(AsyncData.Success::class.java)
|
assertThat(state.signOutAction).isInstanceOf(AsyncData.Success::class.java)
|
||||||
}
|
}
|
||||||
|
assert(signOutLambda).isCalledOnce().withNoParameter()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -142,6 +147,7 @@ class PinUnlockPresenterTest {
|
||||||
scope: CoroutineScope,
|
scope: CoroutineScope,
|
||||||
biometricUnlockManager: BiometricUnlockManager = FakeBiometricUnlockManager(),
|
biometricUnlockManager: BiometricUnlockManager = FakeBiometricUnlockManager(),
|
||||||
callback: PinCodeManager.Callback = DefaultPinCodeManagerCallback(),
|
callback: PinCodeManager.Callback = DefaultPinCodeManagerCallback(),
|
||||||
|
signOut: SignOut = FakeSignOut(),
|
||||||
): PinUnlockPresenter {
|
): PinUnlockPresenter {
|
||||||
val pinCodeManager = aPinCodeManager().apply {
|
val pinCodeManager = aPinCodeManager().apply {
|
||||||
addCallback(callback)
|
addCallback(callback)
|
||||||
|
|
@ -150,7 +156,7 @@ class PinUnlockPresenterTest {
|
||||||
return PinUnlockPresenter(
|
return PinUnlockPresenter(
|
||||||
pinCodeManager = pinCodeManager,
|
pinCodeManager = pinCodeManager,
|
||||||
biometricUnlockManager = biometricUnlockManager,
|
biometricUnlockManager = biometricUnlockManager,
|
||||||
matrixClient = FakeMatrixClient(),
|
signOut = signOut,
|
||||||
coroutineScope = scope,
|
coroutineScope = scope,
|
||||||
pinUnlockHelper = PinUnlockHelper(biometricUnlockManager, pinCodeManager),
|
pinUnlockHelper = PinUnlockHelper(biometricUnlockManager, pinCodeManager),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -144,7 +144,9 @@ class LogoutPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `present - logout with error then cancel`() = runTest {
|
fun `present - logout with error then cancel`() = runTest {
|
||||||
val matrixClient = FakeMatrixClient().apply {
|
val matrixClient = FakeMatrixClient().apply {
|
||||||
givenLogoutError(A_THROWABLE)
|
logoutLambda = { _ ->
|
||||||
|
throw A_THROWABLE
|
||||||
|
}
|
||||||
}
|
}
|
||||||
val presenter = createLogoutPresenter(
|
val presenter = createLogoutPresenter(
|
||||||
matrixClient,
|
matrixClient,
|
||||||
|
|
@ -170,7 +172,13 @@ class LogoutPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `present - logout with error then force`() = runTest {
|
fun `present - logout with error then force`() = runTest {
|
||||||
val matrixClient = FakeMatrixClient().apply {
|
val matrixClient = FakeMatrixClient().apply {
|
||||||
givenLogoutError(A_THROWABLE)
|
logoutLambda = { ignoreSdkError ->
|
||||||
|
if (!ignoreSdkError) {
|
||||||
|
throw A_THROWABLE
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
val presenter = createLogoutPresenter(
|
val presenter = createLogoutPresenter(
|
||||||
matrixClient,
|
matrixClient,
|
||||||
|
|
|
||||||
|
|
@ -125,7 +125,9 @@ class DefaultDirectLogoutPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `present - logout with error then cancel`() = runTest {
|
fun `present - logout with error then cancel`() = runTest {
|
||||||
val matrixClient = FakeMatrixClient().apply {
|
val matrixClient = FakeMatrixClient().apply {
|
||||||
givenLogoutError(A_THROWABLE)
|
logoutLambda = { _ ->
|
||||||
|
throw A_THROWABLE
|
||||||
|
}
|
||||||
}
|
}
|
||||||
val presenter = createDefaultDirectLogoutPresenter(
|
val presenter = createDefaultDirectLogoutPresenter(
|
||||||
matrixClient,
|
matrixClient,
|
||||||
|
|
@ -151,7 +153,13 @@ class DefaultDirectLogoutPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `present - logout with error then force`() = runTest {
|
fun `present - logout with error then force`() = runTest {
|
||||||
val matrixClient = FakeMatrixClient().apply {
|
val matrixClient = FakeMatrixClient().apply {
|
||||||
givenLogoutError(A_THROWABLE)
|
logoutLambda = { ignoreSdkError ->
|
||||||
|
if (!ignoreSdkError) {
|
||||||
|
throw A_THROWABLE
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
val presenter = createDefaultDirectLogoutPresenter(
|
val presenter = createDefaultDirectLogoutPresenter(
|
||||||
matrixClient,
|
matrixClient,
|
||||||
|
|
|
||||||
|
|
@ -212,9 +212,7 @@ class PreferencesFlowNode @AssistedInject constructor(
|
||||||
createNode<EditUserProfileNode>(buildContext, listOf(inputs))
|
createNode<EditUserProfileNode>(buildContext, listOf(inputs))
|
||||||
}
|
}
|
||||||
NavTarget.LockScreenSettings -> {
|
NavTarget.LockScreenSettings -> {
|
||||||
lockScreenEntryPoint.nodeBuilder(this, buildContext)
|
lockScreenEntryPoint.nodeBuilder(this, buildContext, LockScreenEntryPoint.Target.Settings).build()
|
||||||
.target(LockScreenEntryPoint.Target.Settings)
|
|
||||||
.build()
|
|
||||||
}
|
}
|
||||||
NavTarget.BlockedUsers -> {
|
NavTarget.BlockedUsers -> {
|
||||||
createNode<BlockedUsersNode>(buildContext)
|
createNode<BlockedUsersNode>(buildContext)
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,6 @@ import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.flowOf
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
|
@ -95,7 +94,6 @@ class FakeMatrixClient(
|
||||||
private var createRoomResult: Result<RoomId> = Result.success(A_ROOM_ID)
|
private var createRoomResult: Result<RoomId> = Result.success(A_ROOM_ID)
|
||||||
private var createDmResult: Result<RoomId> = Result.success(A_ROOM_ID)
|
private var createDmResult: Result<RoomId> = Result.success(A_ROOM_ID)
|
||||||
private var findDmResult: RoomId? = A_ROOM_ID
|
private var findDmResult: RoomId? = A_ROOM_ID
|
||||||
private var logoutFailure: Throwable? = null
|
|
||||||
private val getRoomResults = mutableMapOf<RoomId, MatrixRoom>()
|
private val getRoomResults = mutableMapOf<RoomId, MatrixRoom>()
|
||||||
private val searchUserResults = mutableMapOf<String, Result<MatrixSearchUserResults>>()
|
private val searchUserResults = mutableMapOf<String, Result<MatrixSearchUserResults>>()
|
||||||
private val getProfileResults = mutableMapOf<UserId, Result<MatrixUser>>()
|
private val getProfileResults = mutableMapOf<UserId, Result<MatrixUser>>()
|
||||||
|
|
@ -115,6 +113,9 @@ class FakeMatrixClient(
|
||||||
var getRoomInfoFlowLambda = { _: RoomId ->
|
var getRoomInfoFlowLambda = { _: RoomId ->
|
||||||
flowOf<Optional<MatrixRoomInfo>>(Optional.empty())
|
flowOf<Optional<MatrixRoomInfo>>(Optional.empty())
|
||||||
}
|
}
|
||||||
|
var logoutLambda: (Boolean) -> String? = {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun getRoom(roomId: RoomId): MatrixRoom? {
|
override suspend fun getRoom(roomId: RoomId): MatrixRoom? {
|
||||||
return getRoomResults[roomId]
|
return getRoomResults[roomId]
|
||||||
|
|
@ -159,12 +160,8 @@ class FakeMatrixClient(
|
||||||
override suspend fun clearCache() {
|
override suspend fun clearCache() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun logout(ignoreSdkError: Boolean): String? {
|
override suspend fun logout(ignoreSdkError: Boolean): String? = simulateLongTask {
|
||||||
delay(100)
|
return logoutLambda(ignoreSdkError)
|
||||||
if (ignoreSdkError.not()) {
|
|
||||||
logoutFailure?.let { throw it }
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun close() = Unit
|
override fun close() = Unit
|
||||||
|
|
@ -228,10 +225,6 @@ class FakeMatrixClient(
|
||||||
|
|
||||||
// Mocks
|
// Mocks
|
||||||
|
|
||||||
fun givenLogoutError(failure: Throwable?) {
|
|
||||||
logoutFailure = failure
|
|
||||||
}
|
|
||||||
|
|
||||||
fun givenCreateRoomResult(result: Result<RoomId>) {
|
fun givenCreateRoomResult(result: Result<RoomId>) {
|
||||||
createRoomResult = result
|
createRoomResult = result
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,13 +39,13 @@ class FakeAuthenticationService : MatrixAuthenticationService {
|
||||||
private var changeServerError: Throwable? = null
|
private var changeServerError: Throwable? = null
|
||||||
private var matrixClient: MatrixClient? = null
|
private var matrixClient: MatrixClient? = null
|
||||||
|
|
||||||
|
var getLatestSessionIdLambda: (() -> SessionId?) = { null }
|
||||||
|
|
||||||
override fun loggedInStateFlow(): Flow<LoggedInState> {
|
override fun loggedInStateFlow(): Flow<LoggedInState> {
|
||||||
return flowOf(LoggedInState.NotLoggedIn)
|
return flowOf(LoggedInState.NotLoggedIn)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getLatestSessionId(): SessionId? {
|
override suspend fun getLatestSessionId(): SessionId? = getLatestSessionIdLambda()
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun restoreSession(sessionId: SessionId): Result<MatrixClient> {
|
override suspend fun restoreSession(sessionId: SessionId): Result<MatrixClient> {
|
||||||
return if (matrixClient != null) {
|
return if (matrixClient != null) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue