From 6223f362c32a604d96676793e575e8d6ee1c0382 Mon Sep 17 00:00:00 2001 From: Dave Severns <149429124+dseverns-livefront@users.noreply.github.com> Date: Fri, 20 Dec 2024 17:05:30 -0500 Subject: [PATCH] PM-16062 Prevent account locks for ongoing autofill requests (#4498) --- .../data/autofill/util/AutofillIntentUtils.kt | 10 + .../platform/manager/AppStateManagerImpl.kt | 10 +- .../manager/model/AppCreationState.kt | 8 +- .../vault/manager/VaultLockManagerImpl.kt | 111 ++++++---- .../platform/manager/AppStateManagerTest.kt | 12 +- .../manager/util/FakeAppStateManager.kt | 3 +- .../vault/manager/VaultLockManagerTest.kt | 202 +++++++++--------- 7 files changed, 204 insertions(+), 152 deletions(-) diff --git a/app/src/main/java/com/x8bit/bitwarden/data/autofill/util/AutofillIntentUtils.kt b/app/src/main/java/com/x8bit/bitwarden/data/autofill/util/AutofillIntentUtils.kt index 2125cc903d8..f659d188556 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/autofill/util/AutofillIntentUtils.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/autofill/util/AutofillIntentUtils.kt @@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.autofill.util +import android.app.Activity import android.app.PendingIntent import android.app.assist.AssistStructure import android.content.Context @@ -147,3 +148,12 @@ fun Intent.getAutofillSelectionDataOrNull(): AutofillSelectionData? = fun Intent.getTotpCopyIntentOrNull(): AutofillTotpCopyData? = getBundleExtra(AUTOFILL_BUNDLE_KEY) ?.getSafeParcelableExtra(AUTOFILL_TOTP_COPY_DATA_KEY) + +/** + * Checks if the given [Activity] was created for Autofill. This is useful to avoid locking the + * vault if one of the Autofill services starts the only only instance of the [MainActivity]. + */ +val Activity.createdForAutofill: Boolean + get() = intent.getAutofillSelectionDataOrNull() != null || + intent.getAutofillSaveItemOrNull() != null || + intent.getAutofillAssistStructureOrNull() != null diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/AppStateManagerImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/AppStateManagerImpl.kt index 4baa775c079..96ee8ecdcd3 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/AppStateManagerImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/AppStateManagerImpl.kt @@ -6,6 +6,7 @@ import android.os.Bundle import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner +import com.x8bit.bitwarden.data.autofill.util.createdForAutofill import com.x8bit.bitwarden.data.platform.manager.model.AppCreationState import com.x8bit.bitwarden.data.platform.manager.model.AppForegroundState import kotlinx.coroutines.flow.MutableStateFlow @@ -19,7 +20,8 @@ class AppStateManagerImpl( application: Application, processLifecycleOwner: LifecycleOwner = ProcessLifecycleOwner.get(), ) : AppStateManager { - private val mutableAppCreationStateFlow = MutableStateFlow(AppCreationState.DESTROYED) + private val mutableAppCreationStateFlow = + MutableStateFlow(AppCreationState.Destroyed) private val mutableAppForegroundStateFlow = MutableStateFlow(AppForegroundState.BACKGROUNDED) override val appCreatedStateFlow: StateFlow @@ -49,13 +51,15 @@ class AppStateManagerImpl( override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { activityCount++ // Always be in a created state if we have an activity - mutableAppCreationStateFlow.value = AppCreationState.CREATED + mutableAppCreationStateFlow.value = AppCreationState.Created( + isAutoFill = activity.createdForAutofill, + ) } override fun onActivityDestroyed(activity: Activity) { activityCount-- if (activityCount == 0 && !activity.isChangingConfigurations) { - mutableAppCreationStateFlow.value = AppCreationState.DESTROYED + mutableAppCreationStateFlow.value = AppCreationState.Destroyed } } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/AppCreationState.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/AppCreationState.kt index e5bb388c954..3e105829a25 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/AppCreationState.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/AppCreationState.kt @@ -3,14 +3,16 @@ package com.x8bit.bitwarden.data.platform.manager.model /** * Represents the creation state of the app. */ -enum class AppCreationState { +sealed class AppCreationState { /** * Denotes that the app is currently created. + * + * @param isAutoFill Whether the app was created for autofill. */ - CREATED, + data class Created(val isAutoFill: Boolean) : AppCreationState() /** * Denotes that the app is currently destroyed. */ - DESTROYED, + data object Destroyed : AppCreationState() } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/manager/VaultLockManagerImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/manager/VaultLockManagerImpl.kt index 1320c2ca3f7..9fadf13d1ad 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/manager/VaultLockManagerImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/manager/VaultLockManagerImpl.kt @@ -305,29 +305,40 @@ class VaultLockManagerImpl( } private fun observeAppCreationChanges() { + var isFirstCreated = true appStateManager .appCreatedStateFlow .onEach { appCreationState -> when (appCreationState) { - AppCreationState.CREATED -> Unit - AppCreationState.DESTROYED -> handleOnDestroyed() + is AppCreationState.Created -> { + handleOnCreated( + createdForAutofill = appCreationState.isAutoFill, + isFirstCreated = isFirstCreated, + ) + isFirstCreated = false + } + + AppCreationState.Destroyed -> Unit } } .launchIn(unconfinedScope) } - private fun handleOnDestroyed() { - activeUserId?.let { userId -> - checkForVaultTimeout( - userId = userId, - checkTimeoutReason = CheckTimeoutReason.APP_RESTARTED, - ) - } + private fun handleOnCreated( + createdForAutofill: Boolean, + isFirstCreated: Boolean, + ) { + val userId = activeUserId ?: return + checkForVaultTimeout( + userId = userId, + checkTimeoutReason = CheckTimeoutReason.AppCreated( + firstTimeCreation = isFirstCreated, + createdForAutofill = createdForAutofill, + ), + ) } private fun observeAppForegroundChanges() { - var isFirstForeground = true - appStateManager .appForegroundStateFlow .onEach { appForegroundState -> @@ -336,10 +347,7 @@ class VaultLockManagerImpl( handleOnBackground() } - AppForegroundState.FOREGROUNDED -> { - handleOnForeground(isFirstForeground = isFirstForeground) - isFirstForeground = false - } + AppForegroundState.FOREGROUNDED -> handleOnForeground() } } .launchIn(unconfinedScope) @@ -349,19 +357,13 @@ class VaultLockManagerImpl( val userId = activeUserId ?: return checkForVaultTimeout( userId = userId, - checkTimeoutReason = CheckTimeoutReason.APP_BACKGROUNDED, + checkTimeoutReason = CheckTimeoutReason.AppBackgrounded, ) } - private fun handleOnForeground(isFirstForeground: Boolean) { + private fun handleOnForeground() { val userId = activeUserId ?: return userIdTimerJobMap[userId]?.cancel() - if (isFirstForeground) { - checkForVaultTimeout( - userId = userId, - checkTimeoutReason = CheckTimeoutReason.APP_RESTARTED, - ) - } } private fun observeUserSwitchingChanges() { @@ -461,7 +463,7 @@ class VaultLockManagerImpl( // Check if the user's timeout action should be performed as we switch away. checkForVaultTimeout( userId = previousActiveUserId, - checkTimeoutReason = CheckTimeoutReason.USER_CHANGED, + checkTimeoutReason = CheckTimeoutReason.UserChanged, ) } @@ -491,10 +493,19 @@ class VaultLockManagerImpl( VaultTimeout.OnAppRestart -> { // If this is an app restart, trigger the timeout action; otherwise ignore. - if (checkTimeoutReason == CheckTimeoutReason.APP_RESTARTED) { - // On restart the vault should be locked already but we may need to soft-logout - // the user. - handleTimeoutAction(userId = userId, vaultTimeoutAction = vaultTimeoutAction) + if (checkTimeoutReason is CheckTimeoutReason.AppCreated) { + // We need to check the timeout action on the first time creation no matter what + // for all subsequent creations we should check if this is for autofill and + // and if it is we skip checking the timeout action. + if ( + checkTimeoutReason.firstTimeCreation || + !checkTimeoutReason.createdForAutofill + ) { + handleTimeoutAction( + userId = userId, + vaultTimeoutAction = vaultTimeoutAction, + ) + } } } @@ -502,16 +513,18 @@ class VaultLockManagerImpl( when (checkTimeoutReason) { // Always preform the timeout action on app restart to ensure the user is // in the correct state. - CheckTimeoutReason.APP_RESTARTED -> { - handleTimeoutAction( - userId = userId, - vaultTimeoutAction = vaultTimeoutAction, - ) + is CheckTimeoutReason.AppCreated -> { + if (checkTimeoutReason.firstTimeCreation) { + handleTimeoutAction( + userId = userId, + vaultTimeoutAction = vaultTimeoutAction, + ) + } } // User no longer active or engaging with the app. - CheckTimeoutReason.APP_BACKGROUNDED, - CheckTimeoutReason.USER_CHANGED, + CheckTimeoutReason.AppBackgrounded, + CheckTimeoutReason.UserChanged, -> { handleTimeoutActionWithDelay( userId = userId, @@ -589,11 +602,29 @@ class VaultLockManagerImpl( } /** - * Helper enum that indicates the reason we are checking for timeout. + * Helper sealed class which denotes the reason to check the vault timeout. */ - private enum class CheckTimeoutReason { - APP_BACKGROUNDED, - APP_RESTARTED, - USER_CHANGED, + private sealed class CheckTimeoutReason { + /** + * Indicates the app has been backgrounded but is still running. + */ + data object AppBackgrounded : CheckTimeoutReason() + + /** + * Indicates the app has entered a Created state. + * + * @param firstTimeCreation if this is the first time the process is being created. + * @param createdForAutofill if the the creation event is due to an activity being launched + * for autofill. + */ + data class AppCreated( + val firstTimeCreation: Boolean, + val createdForAutofill: Boolean, + ) : CheckTimeoutReason() + + /** + * Indicates that the current user has changed. + */ + data object UserChanged : CheckTimeoutReason() } } diff --git a/app/src/test/java/com/x8bit/bitwarden/data/platform/manager/AppStateManagerTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/platform/manager/AppStateManagerTest.kt index 0021001d22e..f4f77743ea9 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/platform/manager/AppStateManagerTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/platform/manager/AppStateManagerTest.kt @@ -3,14 +3,17 @@ package com.x8bit.bitwarden.data.platform.manager import android.app.Activity import android.app.Application import app.cash.turbine.test +import com.x8bit.bitwarden.data.autofill.util.createdForAutofill import com.x8bit.bitwarden.data.platform.manager.model.AppCreationState import com.x8bit.bitwarden.data.platform.manager.model.AppForegroundState import com.x8bit.bitwarden.data.util.FakeLifecycleOwner import io.mockk.every import io.mockk.just import io.mockk.mockk +import io.mockk.mockkStatic import io.mockk.runs import io.mockk.slot +import io.mockk.unmockkStatic import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test @@ -59,15 +62,17 @@ class AppStateManagerTest { @Test fun `appCreatedStateFlow should emit whenever the underlying activities are all destroyed or a creation event occurs`() = runTest { + mockkStatic(Activity::createdForAutofill) val activity = mockk { every { isChangingConfigurations } returns false + every { createdForAutofill } returns false } appStateManager.appCreatedStateFlow.test { // Initial state is DESTROYED - assertEquals(AppCreationState.DESTROYED, awaitItem()) + assertEquals(AppCreationState.Destroyed, awaitItem()) activityLifecycleCallbacks.captured.onActivityCreated(activity, null) - assertEquals(AppCreationState.CREATED, awaitItem()) + assertEquals(AppCreationState.Created(isAutoFill = false), awaitItem()) activityLifecycleCallbacks.captured.onActivityCreated(activity, null) expectNoEvents() @@ -76,7 +81,8 @@ class AppStateManagerTest { expectNoEvents() activityLifecycleCallbacks.captured.onActivityDestroyed(activity) - assertEquals(AppCreationState.DESTROYED, awaitItem()) + assertEquals(AppCreationState.Destroyed, awaitItem()) } + unmockkStatic(Activity::createdForAutofill) } } diff --git a/app/src/test/java/com/x8bit/bitwarden/data/platform/manager/util/FakeAppStateManager.kt b/app/src/test/java/com/x8bit/bitwarden/data/platform/manager/util/FakeAppStateManager.kt index 74a7ee939dd..2d37d7630a8 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/platform/manager/util/FakeAppStateManager.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/platform/manager/util/FakeAppStateManager.kt @@ -11,7 +11,8 @@ import kotlinx.coroutines.flow.asStateFlow * A faked implementation of [AppStateManager] */ class FakeAppStateManager : AppStateManager { - private val mutableAppCreationStateFlow = MutableStateFlow(AppCreationState.DESTROYED) + private val mutableAppCreationStateFlow = + MutableStateFlow(AppCreationState.Destroyed) private val mutableAppForegroundStateFlow = MutableStateFlow(AppForegroundState.BACKGROUNDED) override val appCreatedStateFlow: StateFlow diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/manager/VaultLockManagerTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/manager/VaultLockManagerTest.kt index 92784cfc5f6..e902f334af2 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/vault/manager/VaultLockManagerTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/manager/VaultLockManagerTest.kt @@ -147,80 +147,12 @@ class VaultLockManagerTest { } } - @Test - fun `app being destroyed should perform timeout action if necessary`() { - setAccountTokens() - fakeAuthDiskSource.userState = MOCK_USER_STATE - - // Will be used within each loop to reset the test to a suitable initial state. - fun resetTest(vaultTimeout: VaultTimeout) { - clearVerifications(userLogoutManager) - mutableVaultTimeoutStateFlow.value = vaultTimeout - fakeAppStateManager.appCreationState = AppCreationState.CREATED - verifyUnlockedVaultBlocking(userId = USER_ID) - assertTrue(vaultLockManager.isVaultUnlocked(USER_ID)) - } - - // Test Lock action - mutableVaultTimeoutActionStateFlow.value = VaultTimeoutAction.LOCK - MOCK_TIMEOUTS.forEach { vaultTimeout -> - resetTest(vaultTimeout = vaultTimeout) - fakeAppStateManager.appCreationState = AppCreationState.DESTROYED - - when (vaultTimeout) { - VaultTimeout.FifteenMinutes, - VaultTimeout.ThirtyMinutes, - VaultTimeout.OneHour, - VaultTimeout.FourHours, - is VaultTimeout.Custom, - VaultTimeout.Immediately, - VaultTimeout.OneMinute, - VaultTimeout.FiveMinutes, - VaultTimeout.OnAppRestart, - -> { - assertFalse(vaultLockManager.isVaultUnlocked(USER_ID)) - } - - VaultTimeout.Never -> { - assertTrue(vaultLockManager.isVaultUnlocked(USER_ID)) - } - } - verify(exactly = 0) { userLogoutManager.softLogout(any()) } - } - - // Test Logout action - mutableVaultTimeoutActionStateFlow.value = VaultTimeoutAction.LOGOUT - MOCK_TIMEOUTS.forEach { vaultTimeout -> - resetTest(vaultTimeout = vaultTimeout) - fakeAppStateManager.appCreationState = AppCreationState.DESTROYED - - when (vaultTimeout) { - VaultTimeout.OnAppRestart, - VaultTimeout.FifteenMinutes, - VaultTimeout.ThirtyMinutes, - VaultTimeout.OneHour, - VaultTimeout.FourHours, - is VaultTimeout.Custom, - VaultTimeout.Immediately, - VaultTimeout.OneMinute, - VaultTimeout.FiveMinutes, - -> { - verify(exactly = 1) { userLogoutManager.softLogout(any()) } - } - - VaultTimeout.Never -> { - verify(exactly = 0) { userLogoutManager.softLogout(any()) } - } - } - } - } - @Test fun `app coming into background subsequent times should perform timeout action if necessary`() { setAccountTokens() fakeAuthDiskSource.userState = MOCK_USER_STATE - // Start in a foregrounded state + // Start in a foregrounded state. fakeAppStateManager.appForegroundState = AppForegroundState.FOREGROUNDED // Will be used within each loop to reset the test to a suitable initial state. @@ -300,62 +232,50 @@ class VaultLockManagerTest { @Suppress("MaxLineLength") @Test - fun `app coming into foreground for the first time for Never timeout should not perform timeout action`() { + fun `app being created for the first time for Never timeout should not perform timeout action`() { fakeAuthDiskSource.userState = MOCK_USER_STATE mutableVaultTimeoutActionStateFlow.value = VaultTimeoutAction.LOCK mutableVaultTimeoutStateFlow.value = VaultTimeout.Never - fakeAppStateManager.appForegroundState = AppForegroundState.BACKGROUNDED + fakeAppStateManager.appCreationState = AppCreationState.Destroyed verifyUnlockedVaultBlocking(userId = USER_ID) assertTrue(vaultLockManager.isVaultUnlocked(USER_ID)) - fakeAppStateManager.appForegroundState = AppForegroundState.FOREGROUNDED + fakeAppStateManager.appCreationState = AppCreationState.Created(isAutoFill = true) assertTrue(vaultLockManager.isVaultUnlocked(USER_ID)) } @Suppress("MaxLineLength") @Test - fun `app coming into foreground for the first time for OnAppRestart timeout should lock vaults if necessary`() { + fun `app being created for the first time for OnAppRestart timeout should lock vaults if necessary`() { setAccountTokens() fakeAuthDiskSource.userState = MOCK_USER_STATE mutableVaultTimeoutActionStateFlow.value = VaultTimeoutAction.LOCK mutableVaultTimeoutStateFlow.value = VaultTimeout.OnAppRestart - fakeAppStateManager.appForegroundState = AppForegroundState.BACKGROUNDED + fakeAppStateManager.appCreationState = AppCreationState.Destroyed verifyUnlockedVaultBlocking(userId = USER_ID) assertTrue(vaultLockManager.isVaultUnlocked(USER_ID)) - fakeAppStateManager.appForegroundState = AppForegroundState.FOREGROUNDED + fakeAppStateManager.appCreationState = AppCreationState.Created(isAutoFill = false) assertFalse(vaultLockManager.isVaultUnlocked(USER_ID)) } + @Suppress("MaxLineLength") @Test - fun `app coming into foreground for the first time for other timeout should do nothing`() { + fun `app being created for the first time for other timeouts should check timeout action `() { setAccountTokens() fakeAuthDiskSource.userState = MOCK_USER_STATE mutableVaultTimeoutActionStateFlow.value = VaultTimeoutAction.LOCK mutableVaultTimeoutStateFlow.value = VaultTimeout.ThirtyMinutes - fakeAppStateManager.appForegroundState = AppForegroundState.BACKGROUNDED + fakeAppStateManager.appCreationState = AppCreationState.Destroyed verifyUnlockedVaultBlocking(userId = USER_ID) assertTrue(vaultLockManager.isVaultUnlocked(USER_ID)) - fakeAppStateManager.appForegroundState = AppForegroundState.FOREGROUNDED - - assertFalse(vaultLockManager.isVaultUnlocked(USER_ID)) - } - - @Suppress("MaxLineLength") - @Test - fun `app coming into foreground for the first time for non-Never timeout should clear existing times and perform timeout action`() { - setAccountTokens() - fakeAuthDiskSource.userState = MOCK_USER_STATE - mutableVaultTimeoutActionStateFlow.value = VaultTimeoutAction.LOCK - mutableVaultTimeoutStateFlow.value = VaultTimeout.ThirtyMinutes - - fakeAppStateManager.appForegroundState = AppForegroundState.FOREGROUNDED + fakeAppStateManager.appCreationState = AppCreationState.Created(isAutoFill = true) assertFalse(vaultLockManager.isVaultUnlocked(USER_ID)) } @@ -367,11 +287,11 @@ class VaultLockManagerTest { mutableVaultTimeoutActionStateFlow.value = VaultTimeoutAction.LOGOUT mutableVaultTimeoutStateFlow.value = VaultTimeout.ThirtyMinutes - fakeAppStateManager.appForegroundState = AppForegroundState.BACKGROUNDED + fakeAppStateManager.appCreationState = AppCreationState.Destroyed verifyUnlockedVaultBlocking(userId = USER_ID) assertTrue(vaultLockManager.isVaultUnlocked(USER_ID)) - fakeAppStateManager.appForegroundState = AppForegroundState.FOREGROUNDED + fakeAppStateManager.appCreationState = AppCreationState.Created(isAutoFill = false) verify(exactly = 1) { settingsRepository.getVaultTimeoutActionStateFlow(USER_ID) } } @@ -393,19 +313,97 @@ class VaultLockManagerTest { } @Test - fun `app coming into foreground subsequent times should do nothing`() { + fun `app being created subsequent times should do nothing except for OnAppRestart`() { + setAccountTokens() + fakeAuthDiskSource.userState = MOCK_USER_STATE + + // We want to skip the first time since that is different from subsequent creations + fakeAppStateManager.appCreationState = AppCreationState.Created(isAutoFill = false) + + // Will be used within each loop to reset the test to a suitable initial state. + fun resetTest(vaultTimeout: VaultTimeout) { + mutableVaultTimeoutStateFlow.value = vaultTimeout + fakeAppStateManager.appCreationState = AppCreationState.Destroyed + clearVerifications(userLogoutManager) + verifyUnlockedVaultBlocking(userId = USER_ID) + assertTrue(vaultLockManager.isVaultUnlocked(USER_ID)) + } + + // Test Lock action + mutableVaultTimeoutActionStateFlow.value = VaultTimeoutAction.LOCK + MOCK_TIMEOUTS.forEach { vaultTimeout -> + resetTest(vaultTimeout = vaultTimeout) + + fakeAppStateManager.appCreationState = + AppCreationState.Created(isAutoFill = false) + // Advance by 6 minutes. Only actions with a timeout less than this will be triggered. + testDispatcher.scheduler.advanceTimeBy(delayTimeMillis = 6 * 60 * 1000L) + + when (vaultTimeout) { + + VaultTimeout.OnAppRestart -> assertFalse(vaultLockManager.isVaultUnlocked(USER_ID)) + is VaultTimeout.Custom, + VaultTimeout.FifteenMinutes, + VaultTimeout.FiveMinutes, + VaultTimeout.FourHours, + VaultTimeout.Immediately, + VaultTimeout.Never, + VaultTimeout.OneHour, + VaultTimeout.OneMinute, + VaultTimeout.ThirtyMinutes, + -> { + assertTrue(vaultLockManager.isVaultUnlocked(USER_ID)) + } + } + verify(exactly = 0) { userLogoutManager.softLogout(any()) } + } + + // Test Logout action + mutableVaultTimeoutActionStateFlow.value = VaultTimeoutAction.LOGOUT + MOCK_TIMEOUTS.forEach { vaultTimeout -> + resetTest(vaultTimeout = vaultTimeout) + + fakeAppStateManager.appCreationState = + AppCreationState.Created(isAutoFill = false) + // Advance by 6 minutes. Only actions with a timeout less than this will be triggered. + testDispatcher.scheduler.advanceTimeBy(delayTimeMillis = 6 * 60 * 1000L) + + assertTrue(vaultLockManager.isVaultUnlocked(USER_ID)) + when (vaultTimeout) { + + VaultTimeout.OnAppRestart -> { + verify(exactly = 1) { userLogoutManager.softLogout(any()) } + } + + is VaultTimeout.Custom, + VaultTimeout.FifteenMinutes, + VaultTimeout.FiveMinutes, + VaultTimeout.FourHours, + VaultTimeout.Immediately, + VaultTimeout.Never, + VaultTimeout.OneHour, + VaultTimeout.OneMinute, + VaultTimeout.ThirtyMinutes, + -> { + verify(exactly = 0) { userLogoutManager.softLogout(any()) } + } + } + } + } + + @Suppress("MaxLineLength") + @Test + fun `app being created subsequent times for AutoFill should do nothing regardless of timeout`() { setAccountTokens() fakeAuthDiskSource.userState = MOCK_USER_STATE - // Start in a foregrounded state - fakeAppStateManager.appForegroundState = AppForegroundState.BACKGROUNDED // We want to skip the first time since that is different from subsequent foregrounds - fakeAppStateManager.appForegroundState = AppForegroundState.FOREGROUNDED + fakeAppStateManager.appCreationState = AppCreationState.Created(isAutoFill = false) // Will be used within each loop to reset the test to a suitable initial state. fun resetTest(vaultTimeout: VaultTimeout) { mutableVaultTimeoutStateFlow.value = vaultTimeout - fakeAppStateManager.appForegroundState = AppForegroundState.BACKGROUNDED + fakeAppStateManager.appCreationState = AppCreationState.Destroyed clearVerifications(userLogoutManager) verifyUnlockedVaultBlocking(userId = USER_ID) assertTrue(vaultLockManager.isVaultUnlocked(USER_ID)) @@ -416,11 +414,11 @@ class VaultLockManagerTest { MOCK_TIMEOUTS.forEach { vaultTimeout -> resetTest(vaultTimeout = vaultTimeout) - fakeAppStateManager.appForegroundState = AppForegroundState.FOREGROUNDED + fakeAppStateManager.appCreationState = + AppCreationState.Created(isAutoFill = true) // Advance by 6 minutes. Only actions with a timeout less than this will be triggered. testDispatcher.scheduler.advanceTimeBy(delayTimeMillis = 6 * 60 * 1000L) - // Vault is never locked while foregrounded, no matter the timeout. assertTrue(vaultLockManager.isVaultUnlocked(USER_ID)) verify(exactly = 0) { userLogoutManager.softLogout(any()) } } @@ -430,11 +428,11 @@ class VaultLockManagerTest { MOCK_TIMEOUTS.forEach { vaultTimeout -> resetTest(vaultTimeout = vaultTimeout) - fakeAppStateManager.appForegroundState = AppForegroundState.FOREGROUNDED + fakeAppStateManager.appCreationState = + AppCreationState.Created(isAutoFill = true) // Advance by 6 minutes. Only actions with a timeout less than this will be triggered. testDispatcher.scheduler.advanceTimeBy(delayTimeMillis = 6 * 60 * 1000L) - // Vault is never locked while foregrounded, no matter the timeout. assertTrue(vaultLockManager.isVaultUnlocked(USER_ID)) verify(exactly = 0) { userLogoutManager.softLogout(any()) } }