From 04c06e30c69506b4ad83fd90470cce3ea7fd4dec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Saleniuk?= <30429749+saleniuk@users.noreply.github.com> Date: Fri, 13 Oct 2023 17:15:35 +0200 Subject: [PATCH] fix: bypassing app lock after timeout (#2324) --- .../wire/android/datastore/GlobalDataStore.kt | 16 +- .../kotlin/com/wire/android/di/AppModule.kt | 5 - .../feature/ObserveAppLockConfigUseCase.kt | 6 +- .../com/wire/android/ui/WireActivity.kt | 2 +- .../home/appLock/EnterLockScreenViewModel.kt | 2 + .../ui/home/appLock/LockCodeTimeManager.kt | 84 +++++---- .../ObserveAppLockConfigUseCaseTest.kt | 8 +- .../home/appLock/LockCodeTimeManagerTest.kt | 175 +++++++++++------- 8 files changed, 169 insertions(+), 129 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/datastore/GlobalDataStore.kt b/app/src/main/kotlin/com/wire/android/datastore/GlobalDataStore.kt index 4faccc0df24..01a0f703bce 100644 --- a/app/src/main/kotlin/com/wire/android/datastore/GlobalDataStore.kt +++ b/app/src/main/kotlin/com/wire/android/datastore/GlobalDataStore.kt @@ -26,7 +26,6 @@ import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.intPreferencesKey -import androidx.datastore.preferences.core.longPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore import com.wire.android.BuildConfig @@ -53,7 +52,6 @@ class GlobalDataStore @Inject constructor(@ApplicationContext private val contex private val IS_LOGGING_ENABLED = booleanPreferencesKey("is_logging_enabled") private val IS_ENCRYPTED_PROTEUS_STORAGE_ENABLED = booleanPreferencesKey("is_encrypted_proteus_storage_enabled") private val APP_LOCK_PASSCODE = stringPreferencesKey("app_lock_passcode") - private val APP_LOCK_TIMESTAMP = longPreferencesKey("app_lock_timestamp") private val Context.dataStore: DataStore by preferencesDataStore(name = PREFERENCES_NAME) private fun userMigrationStatusKey(userId: String): Preferences.Key = intPreferencesKey("user_migration_status_$userId") private fun userDoubleTapToastStatusKey(userId: String): Preferences.Key = @@ -138,6 +136,7 @@ class GlobalDataStore @Inject constructor(@ApplicationContext private val contex suspend fun getShouldShowDoubleTapToast(userId: String): Boolean = getBooleanPreference(userDoubleTapToastStatusKey(userId), true).first() + // returns a flow with decoded passcode @Suppress("TooGenericExceptionCaught") fun getAppLockPasscodeFlow(): Flow = context.dataStore.data.map { @@ -150,6 +149,12 @@ class GlobalDataStore @Inject constructor(@ApplicationContext private val contex } } + // returns a flow only informing whether the passcode is set, without the need to decode it + fun isAppLockPasscodeSetFlow(): Flow = + context.dataStore.data.map { + it.contains(APP_LOCK_PASSCODE) + } + suspend fun clearAppLockPasscode() { context.dataStore.edit { it.remove(APP_LOCK_PASSCODE) @@ -167,11 +172,4 @@ class GlobalDataStore @Inject constructor(@ApplicationContext private val contex } } } - - fun getAppLockTimestampFlow(): Flow = - context.dataStore.data.map { it[APP_LOCK_TIMESTAMP] } - - suspend fun setAppLockTimestamp(timestamp: Long) { - context.dataStore.edit { it[APP_LOCK_TIMESTAMP] = timestamp } - } } diff --git a/app/src/main/kotlin/com/wire/android/di/AppModule.kt b/app/src/main/kotlin/com/wire/android/di/AppModule.kt index 6395f4ca06f..3fdc3230101 100644 --- a/app/src/main/kotlin/com/wire/android/di/AppModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/AppModule.kt @@ -27,7 +27,6 @@ import android.media.MediaPlayer import androidx.core.app.NotificationManagerCompat import com.wire.android.BuildConfig import com.wire.android.mapper.MessageResourceProvider -import com.wire.android.ui.home.appLock.CurrentTimestampProvider import com.wire.android.util.dispatchers.DefaultDispatcherProvider import com.wire.android.util.dispatchers.DispatcherProvider import dagger.Module @@ -80,8 +79,4 @@ object AppModule { ) } } - - @Singleton - @Provides - fun provideCurrentTimestampProvider(): CurrentTimestampProvider = { System.currentTimeMillis() } } diff --git a/app/src/main/kotlin/com/wire/android/feature/ObserveAppLockConfigUseCase.kt b/app/src/main/kotlin/com/wire/android/feature/ObserveAppLockConfigUseCase.kt index 2ff0d39a761..446028384ce 100644 --- a/app/src/main/kotlin/com/wire/android/feature/ObserveAppLockConfigUseCase.kt +++ b/app/src/main/kotlin/com/wire/android/feature/ObserveAppLockConfigUseCase.kt @@ -29,10 +29,10 @@ class ObserveAppLockConfigUseCase @Inject constructor( ) { operator fun invoke(): Flow = - globalDataStore.getAppLockPasscodeFlow().map { // TODO: include checking if any logged account does not enforce app-lock + globalDataStore.isAppLockPasscodeSetFlow().map { // TODO: include checking if any logged account does not enforce app-lock when { - it.isNullOrEmpty() -> AppLockConfig.Disabled - else -> AppLockConfig.Enabled + it -> AppLockConfig.Enabled + else -> AppLockConfig.Disabled } } } diff --git a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt index ca25a4564b0..1f6299032a8 100644 --- a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt @@ -240,7 +240,7 @@ class WireActivity : AppCompatActivity() { @Composable private fun handleAppLock() { LaunchedEffect(Unit) { - lockCodeTimeManager.shouldLock() + lockCodeTimeManager.isLocked() .filter { it } .collectLatest { navigationCommands.emit(NavigationCommand(EnterLockCodeScreenDestination)) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/appLock/EnterLockScreenViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/appLock/EnterLockScreenViewModel.kt index 01640a5b537..53e1af79367 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/appLock/EnterLockScreenViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/appLock/EnterLockScreenViewModel.kt @@ -38,6 +38,7 @@ class EnterLockScreenViewModel @Inject constructor( private val validatePassword: ValidatePasswordUseCase, private val globalDataStore: GlobalDataStore, private val dispatchers: DispatcherProvider, + private val lockCodeTimeManager: LockCodeTimeManager, ) : ViewModel() { var state: EnterLockCodeViewState by mutableStateOf(EnterLockCodeViewState()) @@ -71,6 +72,7 @@ class EnterLockScreenViewModel @Inject constructor( val storedPasscode = withContext(dispatchers.io()) { globalDataStore.getAppLockPasscodeFlow().firstOrNull() } withContext(dispatchers.main()) { state = if (storedPasscode == state.password.text.sha256()) { + lockCodeTimeManager.appUnlocked() state.copy(done = true) } else { state.copy(error = EnterLockCodeError.InvalidValue) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/appLock/LockCodeTimeManager.kt b/app/src/main/kotlin/com/wire/android/ui/home/appLock/LockCodeTimeManager.kt index 5e79f2ff473..cb0f168f4df 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/appLock/LockCodeTimeManager.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/appLock/LockCodeTimeManager.kt @@ -17,20 +17,23 @@ */ package com.wire.android.ui.home.appLock -import com.wire.android.datastore.GlobalDataStore import com.wire.android.di.ApplicationScope import com.wire.android.feature.AppLockConfig import com.wire.android.feature.ObserveAppLockConfigUseCase import com.wire.android.util.CurrentScreenManager import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.scan -import kotlinx.coroutines.flow.shareIn -import kotlinx.coroutines.flow.take +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import javax.inject.Inject import javax.inject.Singleton @@ -39,45 +42,46 @@ class LockCodeTimeManager @Inject constructor( @ApplicationScope private val appCoroutineScope: CoroutineScope, currentScreenManager: CurrentScreenManager, observeAppLockConfigUseCase: ObserveAppLockConfigUseCase, - globalDataStore: GlobalDataStore, - currentTimestamp: CurrentTimestampProvider, ) { - @Suppress("MagicNumber") - private val lockCodeRequiredFlow = globalDataStore.getAppLockTimestampFlow().take(1) - .flatMapLatest { lastAppLockTimestamp -> + private val isLockedFlow = MutableStateFlow(false) + + init { + // first, set initial value - if app lock is enabled then app needs to be locked right away + runBlocking { + observeAppLockConfigUseCase().firstOrNull()?.let { appLockConfig -> + if (appLockConfig !is AppLockConfig.Disabled) { + isLockedFlow.value = true + } + } + } + @Suppress("MagicNumber") + // next, listen for app lock config and app visibility changes to determine if app should be locked + appCoroutineScope.launch { combine( - currentScreenManager.isAppVisibleFlow() - .scan(AppVisibilityTimestampData(lastAppLockTimestamp ?: -1, false)) { previousData, currentlyVisible -> - if (previousData.isAppVisible != currentlyVisible) { - val timestamp = if (!currentlyVisible) { // app moved to background - currentTimestamp().also { - globalDataStore.setAppLockTimestamp(it) - } - } else previousData.timestamp - AppVisibilityTimestampData( - timestamp = timestamp, - isAppVisible = currentlyVisible - ) - } else previousData - }, - observeAppLockConfigUseCase() - ) { appVisibilityTimestampData, appLockConfig -> - appVisibilityTimestampData.isAppVisible - && appLockConfig !is AppLockConfig.Disabled - && appVisibilityTimestampData.timestamp >= 0 - && (currentTimestamp() - appVisibilityTimestampData.timestamp) > (appLockConfig.timeoutInSeconds * 1000) + observeAppLockConfigUseCase(), + currentScreenManager.isAppVisibleFlow(), + ::Pair + ).flatMapLatest { (appLockConfig, isInForeground) -> + when { + appLockConfig is AppLockConfig.Disabled -> flowOf(false) + + !isInForeground && !isLockedFlow.value -> flow { + delay(appLockConfig.timeoutInSeconds * 1000L) + emit(true) + } + + else -> emptyFlow() + } + }.collectLatest { + isLockedFlow.value = it } - .distinctUntilChanged() } - .shareIn(scope = appCoroutineScope, started = SharingStarted.Eagerly, replay = 1) + } - fun shouldLock(): Flow = lockCodeRequiredFlow + fun appUnlocked() { + isLockedFlow.value = false + } - private data class AppVisibilityTimestampData( - val timestamp: Long, - val isAppVisible: Boolean - ) + fun isLocked(): Flow = isLockedFlow } - -typealias CurrentTimestampProvider = () -> Long diff --git a/app/src/test/kotlin/com/wire/android/feature/ObserveAppLockConfigUseCaseTest.kt b/app/src/test/kotlin/com/wire/android/feature/ObserveAppLockConfigUseCaseTest.kt index 6c3c50ee934..33143b9d01f 100644 --- a/app/src/test/kotlin/com/wire/android/feature/ObserveAppLockConfigUseCaseTest.kt +++ b/app/src/test/kotlin/com/wire/android/feature/ObserveAppLockConfigUseCaseTest.kt @@ -32,7 +32,7 @@ class ObserveAppLockConfigUseCaseTest { @Test fun givenPasscodeIsSet_whenObservingAppLockConfig_thenReturnEnabled() = runTest { val (_, useCase) = Arrangement() - .withAppLockPasscode("1234") + .withAppLockPasscodeSet(true) .arrange() val result = useCase.invoke().firstOrNull() @@ -43,7 +43,7 @@ class ObserveAppLockConfigUseCaseTest { @Test fun givenPasscodeIsNotSet_whenObservingAppLockConfig_thenReturnDisabled() = runTest { val (_, useCase) = Arrangement() - .withAppLockPasscode(null) + .withAppLockPasscodeSet(false) .arrange() val result = useCase.invoke().firstOrNull() @@ -60,8 +60,8 @@ class ObserveAppLockConfigUseCaseTest { MockKAnnotations.init(this, relaxUnitFun = true) } - fun withAppLockPasscode(passcode: String?) = apply { - every { globalDataStore.getAppLockPasscodeFlow() } returns flowOf(passcode) + fun withAppLockPasscodeSet(value: Boolean) = apply { + every { globalDataStore.isAppLockPasscodeSetFlow() } returns flowOf(value) } fun arrange() = this to useCase diff --git a/app/src/test/kotlin/com/wire/android/ui/home/appLock/LockCodeTimeManagerTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/appLock/LockCodeTimeManagerTest.kt index 47f04b96029..65a8e8b1fa4 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/appLock/LockCodeTimeManagerTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/appLock/LockCodeTimeManagerTest.kt @@ -17,18 +17,16 @@ */ package com.wire.android.ui.home.appLock -import com.wire.android.datastore.GlobalDataStore +import app.cash.turbine.test import com.wire.android.feature.AppLockConfig import com.wire.android.feature.ObserveAppLockConfigUseCase import com.wire.android.util.CurrentScreenManager import io.mockk.MockKAnnotations -import io.mockk.coEvery import io.mockk.every import io.mockk.impl.annotations.MockK import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestDispatcher import kotlinx.coroutines.test.advanceTimeBy @@ -41,85 +39,139 @@ class LockCodeTimeManagerTest { private val dispatcher = StandardTestDispatcher() - private fun testStopAndStart(appLockConfig: AppLockConfig, delay: Long, expected: Boolean) = - runTest(dispatcher) { - val (arrangement, manager) = Arrangement(dispatcher) - .withAppLockConfig(appLockConfig) - .withIsAppVisible(true) - .arrange() - advanceUntilIdle() - arrangement.withIsAppVisible(false) - advanceTimeBy(delay) - arrangement.withIsAppVisible(true) - advanceUntilIdle() - val result = manager.shouldLock().first() - assertEquals(expected, result) - } - private fun AppLockConfig.timeoutInMillis(): Long = this.timeoutInSeconds * 1000L + private fun testInitialStart(appLockConfig: AppLockConfig, expected: Boolean) = runTest(dispatcher) { + // given + val (arrangement, manager) = Arrangement(dispatcher) + .withAppLockConfig(appLockConfig) + .withIsAppVisible(false) + .arrange() + advanceUntilIdle() + // when + arrangement.withIsAppVisible(true) + advanceUntilIdle() + // then + assertEquals(expected, manager.isLocked().first()) + } + @Test - fun givenLockEnabledAndAppOpen_whenAppClosedAndOpenedAgainBeforeLockTimeout_thenDoNotRequirePasscode() = - testStopAndStart(AppLockConfig.Enabled, AppLockConfig.Enabled.timeoutInMillis() - 100L, false) + fun givenLockEnabled_whenAppInitiallyOpened_thenLocked() = + testInitialStart(AppLockConfig.Enabled, true) @Test - fun givenLockEnabledAndAppOpen_whenAppClosedAndOpenedAgainAfterLockTimeout_thenRequirePasscode() = - testStopAndStart(AppLockConfig.Enabled, AppLockConfig.Enabled.timeoutInMillis() + 100L, true) + fun givenLockDisabled_whenAppInitiallyOpened_thenNotLocked() = + testInitialStart(AppLockConfig.Disabled, false) + + private fun testStop(appLockConfig: AppLockConfig, delayAfterStop: Long, expected: Boolean) = runTest(dispatcher) { + // given + val (arrangement, manager) = Arrangement(dispatcher) + .withAppLockConfig(appLockConfig) + .withIsAppVisible(true) + .arrange() + manager.appUnlocked() + advanceUntilIdle() + // when + arrangement.withIsAppVisible(false) + advanceTimeBy(delayAfterStop) + // then + assertEquals(expected, manager.isLocked().first()) + } @Test - fun givenLockDisabledAndAppOpen_whenAppClosedAndOpenedAgainBeforeLockTimeout_thenDoNotRequirePasscode() = - testStopAndStart(AppLockConfig.Disabled, AppLockConfig.Disabled.timeoutInMillis() - 100L, false) + fun givenLockEnabledAndAppOpenedUnlocked_whenAppClosedAndWaitedMoreThanTimeout_thenLocked() = + testStop(AppLockConfig.Enabled, AppLockConfig.Enabled.timeoutInMillis() + 100L, true) @Test - fun givenLockDisabledAndAppOpen_whenAppClosedAndOpenedAgainAfterLockTimeout_thenDoNotRequirePasscode() = - testStopAndStart(AppLockConfig.Disabled, AppLockConfig.Disabled.timeoutInMillis() + 100L, false) + fun givenLockEnabledAndAppOpenedUnlocked_whenAppClosedAndWaitedLessThanTimeout_thenNotLocked() = + testStop(AppLockConfig.Enabled, AppLockConfig.Enabled.timeoutInMillis() - 100L, false) - private fun testStart(appLockConfig: AppLockConfig, withInitialTimestamp: Boolean, delay: Long, expected: Boolean) = - runTest(dispatcher) { - val (arrangement, manager) = Arrangement(dispatcher) - .withInitialAppLockTimestamp(if (withInitialTimestamp) dispatcher.scheduler.currentTime else -1) - .withAppLockConfig(appLockConfig) - .withIsAppVisible(false) - .arrange() - advanceUntilIdle() - advanceTimeBy(delay) - arrangement.withIsAppVisible(true) - advanceUntilIdle() - val result = manager.shouldLock().first() - assertEquals(expected, result) - } + @Test + fun givenLockDisabledAndAppOpenedUnlocked_whenAppClosedAndWaitedMoreThanTimeout_thenNotLocked() = + testStop(AppLockConfig.Disabled, AppLockConfig.Disabled.timeoutInMillis() + 100L, false) @Test - fun givenLockEnabledAndNoInitialTimestamp_whenAppOpenedBeforeLockTimeout_thenDoNotRequirePasscode() = - testStart(AppLockConfig.Enabled, false, AppLockConfig.Enabled.timeoutInMillis() - 100, false) + fun givenLockDisabledAndAppOpenedUnlocked_whenAppClosedAndWaitedLessThanTimeout_thenNotLocked() = + testStop(AppLockConfig.Disabled, AppLockConfig.Disabled.timeoutInMillis() - 100L, false) @Test - fun givenLockEnabledAndNoInitialTimestamp_whenAppOpenedAfterLockTimeout_thenDoNotRequirePasscode() = - testStart(AppLockConfig.Enabled, false, AppLockConfig.Enabled.timeoutInMillis() + 100, false) + fun givenLockEnabledAndAppOpenedUnlocked_whenAppClosedAnd_thenAfterTimeoutShouldChangeFromNotLockedToLocked() = runTest(dispatcher) { + // given + val (arrangement, manager) = Arrangement(dispatcher) + .withAppLockConfig(AppLockConfig.Enabled) + .withIsAppVisible(true) + .arrange() + manager.appUnlocked() + advanceUntilIdle() + // when-then + manager.isLocked().test { + arrangement.withIsAppVisible(false) + assertEquals(false, awaitItem()) + assertEquals(true, awaitItem()) + } + } + + private fun testStopAndStart(appLockConfig: AppLockConfig, startDelay: Long, expected: Boolean) = runTest(dispatcher) { + // given + val (arrangement, manager) = Arrangement(dispatcher) + .withAppLockConfig(appLockConfig) + .withIsAppVisible(true) + .arrange() + manager.appUnlocked() + advanceUntilIdle() + // when + arrangement.withIsAppVisible(false) + advanceTimeBy(startDelay) + arrangement.withIsAppVisible(true) + // then + assertEquals(expected, manager.isLocked().first()) + } @Test - fun givenLockEnabledAndInitialTimestamp_whenAppOpenedBeforeLockTimeout_thenDoNotRequirePasscode() = - testStart(AppLockConfig.Enabled, true, AppLockConfig.Enabled.timeoutInMillis() - 100, false) + fun givenLockEnabledAndAppOpenedUnlocked_whenAppClosedAndOpenedAgainBeforeLockTimeout_thenNotLocked() = + testStopAndStart(AppLockConfig.Enabled, AppLockConfig.Enabled.timeoutInMillis() - 100L, false) @Test - fun givenLockEnabledAndInitialTimestamp_whenAppOpenedAfterLockTimeout_thenRequirePasscode() = - testStart(AppLockConfig.Enabled, true, AppLockConfig.Enabled.timeoutInMillis() + 100, true) + fun givenLockEnabledAndAppOpenedUnlocked_whenAppClosedAndOpenedAgainAfterLockTimeout_thenLocked() = + testStopAndStart(AppLockConfig.Enabled, AppLockConfig.Enabled.timeoutInMillis() + 100L, true) @Test - fun givenLockDisabledAndNoInitialTimestamp_whenAppOpenedBeforeLockTimeout_thenDoNotRequirePasscode() = - testStart(AppLockConfig.Disabled, false, AppLockConfig.Disabled.timeoutInMillis() - 100, false) + fun givenLockDisabledAndAppOpenedUnlocked_whenAppClosedAndOpenedAgainBeforeLockTimeout_thenNotLocked() = + testStopAndStart(AppLockConfig.Disabled, AppLockConfig.Disabled.timeoutInMillis() - 100L, false) @Test - fun givenLockDisabledAndNoInitialTimestamp_whenAppOpenedAfterLockTimeout_thenDoNotRequirePasscode() = - testStart(AppLockConfig.Disabled, false, AppLockConfig.Disabled.timeoutInMillis() + 100, false) + fun givenLockDisabledAndAppOpenedUnlocked_whenAppClosedAndOpenedAgainAfterLockTimeout_thenNotLocked() = + testStopAndStart(AppLockConfig.Disabled, AppLockConfig.Disabled.timeoutInMillis() + 100L, false) @Test - fun givenLockDisabledAndInitialTimestamp_whenAppOpenedBeforeLockTimeout_thenDoNotRequirePasscode() = - testStart(AppLockConfig.Disabled, true, AppLockConfig.Disabled.timeoutInMillis() - 100, false) + fun givenLockEnabledAndAppOpenedLocked_whenAppClosedAndOpenedBeforeLockTimeout_thenShouldStillBeLocked() = runTest(dispatcher) { + // given + val (arrangement, manager) = Arrangement(dispatcher) + .withAppLockConfig(AppLockConfig.Enabled) + .withIsAppVisible(false) + .arrange() + advanceUntilIdle() + // when + advanceTimeBy(AppLockConfig.Enabled.timeoutInMillis() - 100L) + arrangement.withIsAppVisible(true) + // then + assertEquals(true, manager.isLocked().first()) + } @Test - fun givenLockDisabledAndInitialTimestamp_whenAppOpenedAfterLockTimeout_thenDoNotRequirePasscode() = - testStart(AppLockConfig.Disabled, true, AppLockConfig.Disabled.timeoutInMillis() + 100, false) + fun givenLockEnabledAndAppOpenedLocked_whenAppIsUnlocked_thenNotLocked() = runTest(dispatcher) { + // given + val (arrangement, manager) = Arrangement(dispatcher) + .withAppLockConfig(AppLockConfig.Enabled) + .withIsAppVisible(true) + .arrange() + advanceUntilIdle() + // when + manager.appUnlocked() + advanceUntilIdle() + // then + assertEquals(false, manager.isLocked().first()) + } class Arrangement(dispatcher: TestDispatcher) { @@ -129,16 +181,11 @@ class LockCodeTimeManagerTest { @MockK private lateinit var observeAppLockConfigUseCase: ObserveAppLockConfigUseCase - @MockK - private lateinit var globalDataStore: GlobalDataStore - private val lockCodeTimeManager by lazy { LockCodeTimeManager( CoroutineScope(dispatcher), currentScreenManager, observeAppLockConfigUseCase, - globalDataStore, - dispatcher.scheduler::currentTime ) } @@ -149,12 +196,6 @@ class LockCodeTimeManagerTest { init { MockKAnnotations.init(this, relaxUnitFun = true) - withInitialAppLockTimestamp(-1L) - coEvery { globalDataStore.setAppLockTimestamp(any()) } returns Unit - } - - fun withInitialAppLockTimestamp(value: Long = -1L): Arrangement = apply { - every { globalDataStore.getAppLockTimestampFlow() } returns flowOf(value) } fun withIsAppVisible(value: Boolean): Arrangement = apply {