PM-14410: App restart timeout action (#4237)

This commit is contained in:
David Perez 2024-11-06 11:40:54 -06:00 committed by GitHub
parent 88a741c93a
commit 29384596d4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 380 additions and 174 deletions

View file

@ -5,7 +5,7 @@ import androidx.lifecycle.LifecycleCoroutineScope
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityActivityManager import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityActivityManager
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityActivityManagerImpl import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityActivityManagerImpl
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityEnabledManager import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityEnabledManager
import com.x8bit.bitwarden.data.platform.manager.AppForegroundManager import com.x8bit.bitwarden.data.platform.manager.AppStateManager
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
@ -24,13 +24,13 @@ object ActivityAccessibilityModule {
fun providesAccessibilityActivityManager( fun providesAccessibilityActivityManager(
@ApplicationContext context: Context, @ApplicationContext context: Context,
accessibilityEnabledManager: AccessibilityEnabledManager, accessibilityEnabledManager: AccessibilityEnabledManager,
appForegroundManager: AppForegroundManager, appStateManager: AppStateManager,
lifecycleScope: LifecycleCoroutineScope, lifecycleScope: LifecycleCoroutineScope,
): AccessibilityActivityManager = ): AccessibilityActivityManager =
AccessibilityActivityManagerImpl( AccessibilityActivityManagerImpl(
context = context, context = context,
accessibilityEnabledManager = accessibilityEnabledManager, accessibilityEnabledManager = accessibilityEnabledManager,
appForegroundManager = appForegroundManager, appStateManager = appStateManager,
lifecycleScope = lifecycleScope, lifecycleScope = lifecycleScope,
) )
} }

View file

@ -3,7 +3,7 @@ package com.x8bit.bitwarden.data.autofill.accessibility.manager
import android.content.Context import android.content.Context
import androidx.lifecycle.LifecycleCoroutineScope import androidx.lifecycle.LifecycleCoroutineScope
import com.x8bit.bitwarden.data.autofill.accessibility.util.isAccessibilityServiceEnabled import com.x8bit.bitwarden.data.autofill.accessibility.util.isAccessibilityServiceEnabled
import com.x8bit.bitwarden.data.platform.manager.AppForegroundManager import com.x8bit.bitwarden.data.platform.manager.AppStateManager
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
@ -13,11 +13,11 @@ import kotlinx.coroutines.flow.onEach
class AccessibilityActivityManagerImpl( class AccessibilityActivityManagerImpl(
private val context: Context, private val context: Context,
private val accessibilityEnabledManager: AccessibilityEnabledManager, private val accessibilityEnabledManager: AccessibilityEnabledManager,
appForegroundManager: AppForegroundManager, appStateManager: AppStateManager,
lifecycleScope: LifecycleCoroutineScope, lifecycleScope: LifecycleCoroutineScope,
) : AccessibilityActivityManager { ) : AccessibilityActivityManager {
init { init {
appForegroundManager appStateManager
.appForegroundStateFlow .appForegroundStateFlow
.onEach { .onEach {
accessibilityEnabledManager.isAccessibilityEnabled = accessibilityEnabledManager.isAccessibilityEnabled =

View file

@ -8,7 +8,7 @@ import androidx.lifecycle.lifecycleScope
import com.x8bit.bitwarden.data.autofill.manager.AutofillActivityManager import com.x8bit.bitwarden.data.autofill.manager.AutofillActivityManager
import com.x8bit.bitwarden.data.autofill.manager.AutofillActivityManagerImpl import com.x8bit.bitwarden.data.autofill.manager.AutofillActivityManagerImpl
import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManager import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManager
import com.x8bit.bitwarden.data.platform.manager.AppForegroundManager import com.x8bit.bitwarden.data.platform.manager.AppStateManager
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
@ -27,13 +27,13 @@ object ActivityAutofillModule {
@Provides @Provides
fun provideAutofillActivityManager( fun provideAutofillActivityManager(
@ActivityScopedManager autofillManager: AutofillManager, @ActivityScopedManager autofillManager: AutofillManager,
appForegroundManager: AppForegroundManager, appStateManager: AppStateManager,
autofillEnabledManager: AutofillEnabledManager, autofillEnabledManager: AutofillEnabledManager,
lifecycleScope: LifecycleCoroutineScope, lifecycleScope: LifecycleCoroutineScope,
): AutofillActivityManager = ): AutofillActivityManager =
AutofillActivityManagerImpl( AutofillActivityManagerImpl(
autofillManager = autofillManager, autofillManager = autofillManager,
appForegroundManager = appForegroundManager, appStateManager = appStateManager,
autofillEnabledManager = autofillEnabledManager, autofillEnabledManager = autofillEnabledManager,
lifecycleScope = lifecycleScope, lifecycleScope = lifecycleScope,
) )

View file

@ -2,7 +2,7 @@ package com.x8bit.bitwarden.data.autofill.manager
import android.view.autofill.AutofillManager import android.view.autofill.AutofillManager
import androidx.lifecycle.LifecycleCoroutineScope import androidx.lifecycle.LifecycleCoroutineScope
import com.x8bit.bitwarden.data.platform.manager.AppForegroundManager import com.x8bit.bitwarden.data.platform.manager.AppStateManager
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
@ -12,7 +12,7 @@ import kotlinx.coroutines.flow.onEach
class AutofillActivityManagerImpl( class AutofillActivityManagerImpl(
private val autofillManager: AutofillManager, private val autofillManager: AutofillManager,
private val autofillEnabledManager: AutofillEnabledManager, private val autofillEnabledManager: AutofillEnabledManager,
appForegroundManager: AppForegroundManager, appStateManager: AppStateManager,
lifecycleScope: LifecycleCoroutineScope, lifecycleScope: LifecycleCoroutineScope,
) : AutofillActivityManager { ) : AutofillActivityManager {
private val isAutofillEnabledAndSupported: Boolean private val isAutofillEnabledAndSupported: Boolean
@ -21,7 +21,7 @@ class AutofillActivityManagerImpl(
autofillManager.isAutofillSupported autofillManager.isAutofillSupported
init { init {
appForegroundManager appStateManager
.appForegroundStateFlow .appForegroundStateFlow
.onEach { autofillEnabledManager.isAutofillEnabled = isAutofillEnabledAndSupported } .onEach { autofillEnabledManager.isAutofillEnabled = isAutofillEnabledAndSupported }
.launchIn(lifecycleScope) .launchIn(lifecycleScope)

View file

@ -1,15 +0,0 @@
package com.x8bit.bitwarden.data.platform.manager
import com.x8bit.bitwarden.data.platform.manager.model.AppForegroundState
import kotlinx.coroutines.flow.StateFlow
/**
* A manager for tracking app foreground state changes.
*/
interface AppForegroundManager {
/**
* Emits whenever there are changes to the app foreground state.
*/
val appForegroundStateFlow: StateFlow<AppForegroundState>
}

View file

@ -1,36 +0,0 @@
package com.x8bit.bitwarden.data.platform.manager
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import com.x8bit.bitwarden.data.platform.manager.model.AppForegroundState
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
/**
* Primary implementation of [AppForegroundManager].
*/
class AppForegroundManagerImpl(
processLifecycleOwner: LifecycleOwner = ProcessLifecycleOwner.get(),
) : AppForegroundManager {
private val mutableAppForegroundStateFlow =
MutableStateFlow(AppForegroundState.BACKGROUNDED)
override val appForegroundStateFlow: StateFlow<AppForegroundState>
get() = mutableAppForegroundStateFlow.asStateFlow()
init {
processLifecycleOwner.lifecycle.addObserver(
object : DefaultLifecycleObserver {
override fun onStart(owner: LifecycleOwner) {
mutableAppForegroundStateFlow.value = AppForegroundState.FOREGROUNDED
}
override fun onStop(owner: LifecycleOwner) {
mutableAppForegroundStateFlow.value = AppForegroundState.BACKGROUNDED
}
},
)
}
}

View file

@ -0,0 +1,24 @@
package com.x8bit.bitwarden.data.platform.manager
import com.x8bit.bitwarden.data.autofill.accessibility.BitwardenAccessibilityService
import com.x8bit.bitwarden.data.platform.manager.model.AppCreationState
import com.x8bit.bitwarden.data.platform.manager.model.AppForegroundState
import kotlinx.coroutines.flow.StateFlow
/**
* A manager for tracking app foreground state changes.
*/
interface AppStateManager {
/**
* Emits whenever there are changes to the app creation state.
*
* This is required because the [BitwardenAccessibilityService] will keep the app process alive
* when the app would otherwise be destroyed.
*/
val appCreatedStateFlow: StateFlow<AppCreationState>
/**
* Emits whenever there are changes to the app foreground state.
*/
val appForegroundStateFlow: StateFlow<AppForegroundState>
}

View file

@ -0,0 +1,72 @@
package com.x8bit.bitwarden.data.platform.manager
import android.app.Activity
import android.app.Application
import android.os.Bundle
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import com.x8bit.bitwarden.data.platform.manager.model.AppCreationState
import com.x8bit.bitwarden.data.platform.manager.model.AppForegroundState
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
/**
* Primary implementation of [AppStateManager].
*/
class AppStateManagerImpl(
application: Application,
processLifecycleOwner: LifecycleOwner = ProcessLifecycleOwner.get(),
) : AppStateManager {
private val mutableAppCreationStateFlow = MutableStateFlow(AppCreationState.DESTROYED)
private val mutableAppForegroundStateFlow = MutableStateFlow(AppForegroundState.BACKGROUNDED)
override val appCreatedStateFlow: StateFlow<AppCreationState>
get() = mutableAppCreationStateFlow.asStateFlow()
override val appForegroundStateFlow: StateFlow<AppForegroundState>
get() = mutableAppForegroundStateFlow.asStateFlow()
init {
application.registerActivityLifecycleCallbacks(AppCreationCallback())
processLifecycleOwner.lifecycle.addObserver(AppForegroundObserver())
}
private inner class AppForegroundObserver : DefaultLifecycleObserver {
override fun onStart(owner: LifecycleOwner) {
mutableAppForegroundStateFlow.value = AppForegroundState.FOREGROUNDED
}
override fun onStop(owner: LifecycleOwner) {
mutableAppForegroundStateFlow.value = AppForegroundState.BACKGROUNDED
}
}
private inner class AppCreationCallback : Application.ActivityLifecycleCallbacks {
private var activityCount: Int = 0
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
activityCount++
// Always be in a created state if we have an activity
mutableAppCreationStateFlow.value = AppCreationState.CREATED
}
override fun onActivityDestroyed(activity: Activity) {
activityCount--
if (activityCount == 0 && !activity.isChangingConfigurations) {
mutableAppCreationStateFlow.value = AppCreationState.DESTROYED
}
}
override fun onActivityStarted(activity: Activity) = Unit
override fun onActivityResumed(activity: Activity) = Unit
override fun onActivityPaused(activity: Activity) = Unit
override fun onActivityStopped(activity: Activity) = Unit
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) = Unit
}
}

View file

@ -14,8 +14,8 @@ import com.x8bit.bitwarden.data.platform.datasource.network.authenticator.Refres
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.BaseUrlInterceptors import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.BaseUrlInterceptors
import com.x8bit.bitwarden.data.platform.datasource.network.service.EventService import com.x8bit.bitwarden.data.platform.datasource.network.service.EventService
import com.x8bit.bitwarden.data.platform.datasource.network.service.PushService import com.x8bit.bitwarden.data.platform.datasource.network.service.PushService
import com.x8bit.bitwarden.data.platform.manager.AppForegroundManager import com.x8bit.bitwarden.data.platform.manager.AppStateManager
import com.x8bit.bitwarden.data.platform.manager.AppForegroundManagerImpl import com.x8bit.bitwarden.data.platform.manager.AppStateManagerImpl
import com.x8bit.bitwarden.data.platform.manager.AssetManager import com.x8bit.bitwarden.data.platform.manager.AssetManager
import com.x8bit.bitwarden.data.platform.manager.AssetManagerImpl import com.x8bit.bitwarden.data.platform.manager.AssetManagerImpl
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
@ -80,8 +80,9 @@ object PlatformManagerModule {
@Provides @Provides
@Singleton @Singleton
fun provideAppForegroundManager(): AppForegroundManager = fun provideAppStateManager(
AppForegroundManagerImpl() application: Application,
): AppStateManager = AppStateManagerImpl(application = application)
@Provides @Provides
@Singleton @Singleton
@ -267,11 +268,11 @@ object PlatformManagerModule {
@Singleton @Singleton
fun provideRestrictionManager( fun provideRestrictionManager(
@ApplicationContext context: Context, @ApplicationContext context: Context,
appForegroundManager: AppForegroundManager, appStateManager: AppStateManager,
dispatcherManager: DispatcherManager, dispatcherManager: DispatcherManager,
environmentRepository: EnvironmentRepository, environmentRepository: EnvironmentRepository,
): RestrictionManager = RestrictionManagerImpl( ): RestrictionManager = RestrictionManagerImpl(
appForegroundManager = appForegroundManager, appStateManager = appStateManager,
dispatcherManager = dispatcherManager, dispatcherManager = dispatcherManager,
context = context, context = context,
environmentRepository = environmentRepository, environmentRepository = environmentRepository,

View file

@ -0,0 +1,16 @@
package com.x8bit.bitwarden.data.platform.manager.model
/**
* Represents the creation state of the app.
*/
enum class AppCreationState {
/**
* Denotes that the app is currently created.
*/
CREATED,
/**
* Denotes that the app is currently destroyed.
*/
DESTROYED,
}

View file

@ -6,7 +6,7 @@ import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.content.RestrictionsManager import android.content.RestrictionsManager
import android.os.Bundle import android.os.Bundle
import com.x8bit.bitwarden.data.platform.manager.AppForegroundManager import com.x8bit.bitwarden.data.platform.manager.AppStateManager
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import com.x8bit.bitwarden.data.platform.manager.model.AppForegroundState import com.x8bit.bitwarden.data.platform.manager.model.AppForegroundState
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
@ -19,7 +19,7 @@ import kotlinx.coroutines.flow.onEach
* The default implementation of the [RestrictionManager]. * The default implementation of the [RestrictionManager].
*/ */
class RestrictionManagerImpl( class RestrictionManagerImpl(
appForegroundManager: AppForegroundManager, appStateManager: AppStateManager,
dispatcherManager: DispatcherManager, dispatcherManager: DispatcherManager,
private val context: Context, private val context: Context,
private val environmentRepository: EnvironmentRepository, private val environmentRepository: EnvironmentRepository,
@ -31,7 +31,7 @@ class RestrictionManagerImpl(
private var isReceiverRegistered = false private var isReceiverRegistered = false
init { init {
appForegroundManager appStateManager
.appForegroundStateFlow .appForegroundStateFlow
.onEach { .onEach {
when (it) { when (it) {

View file

@ -12,8 +12,9 @@ import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
import com.x8bit.bitwarden.data.auth.repository.util.toSdkParams import com.x8bit.bitwarden.data.auth.repository.util.toSdkParams
import com.x8bit.bitwarden.data.auth.repository.util.userAccountTokens import com.x8bit.bitwarden.data.auth.repository.util.userAccountTokens
import com.x8bit.bitwarden.data.auth.repository.util.userSwitchingChangesFlow import com.x8bit.bitwarden.data.auth.repository.util.userSwitchingChangesFlow
import com.x8bit.bitwarden.data.platform.manager.AppForegroundManager import com.x8bit.bitwarden.data.platform.manager.AppStateManager
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import com.x8bit.bitwarden.data.platform.manager.model.AppCreationState
import com.x8bit.bitwarden.data.platform.manager.model.AppForegroundState import com.x8bit.bitwarden.data.platform.manager.model.AppForegroundState
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeout import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeout
@ -65,7 +66,7 @@ class VaultLockManagerImpl(
private val authSdkSource: AuthSdkSource, private val authSdkSource: AuthSdkSource,
private val vaultSdkSource: VaultSdkSource, private val vaultSdkSource: VaultSdkSource,
private val settingsRepository: SettingsRepository, private val settingsRepository: SettingsRepository,
private val appForegroundManager: AppForegroundManager, private val appStateManager: AppStateManager,
private val userLogoutManager: UserLogoutManager, private val userLogoutManager: UserLogoutManager,
private val trustedDeviceManager: TrustedDeviceManager, private val trustedDeviceManager: TrustedDeviceManager,
dispatcherManager: DispatcherManager, dispatcherManager: DispatcherManager,
@ -90,6 +91,7 @@ class VaultLockManagerImpl(
get() = mutableVaultStateEventSharedFlow.asSharedFlow() get() = mutableVaultStateEventSharedFlow.asSharedFlow()
init { init {
observeAppCreationChanges()
observeAppForegroundChanges() observeAppForegroundChanges()
observeUserSwitchingChanges() observeUserSwitchingChanges()
observeVaultTimeoutChanges() observeVaultTimeoutChanges()
@ -302,10 +304,31 @@ class VaultLockManagerImpl(
} }
} }
private fun observeAppCreationChanges() {
appStateManager
.appCreatedStateFlow
.onEach { appCreationState ->
when (appCreationState) {
AppCreationState.CREATED -> Unit
AppCreationState.DESTROYED -> handleOnDestroyed()
}
}
.launchIn(unconfinedScope)
}
private fun handleOnDestroyed() {
activeUserId?.let { userId ->
checkForVaultTimeout(
userId = userId,
checkTimeoutReason = CheckTimeoutReason.APP_RESTARTED,
)
}
}
private fun observeAppForegroundChanges() { private fun observeAppForegroundChanges() {
var isFirstForeground = true var isFirstForeground = true
appForegroundManager appStateManager
.appForegroundStateFlow .appForegroundStateFlow
.onEach { appForegroundState -> .onEach { appForegroundState ->
when (appForegroundState) { when (appForegroundState) {

View file

@ -5,7 +5,7 @@ import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource
import com.x8bit.bitwarden.data.auth.manager.TrustedDeviceManager import com.x8bit.bitwarden.data.auth.manager.TrustedDeviceManager
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
import com.x8bit.bitwarden.data.platform.manager.AppForegroundManager import com.x8bit.bitwarden.data.platform.manager.AppStateManager
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
@ -72,7 +72,7 @@ object VaultManagerModule {
authSdkSource: AuthSdkSource, authSdkSource: AuthSdkSource,
vaultSdkSource: VaultSdkSource, vaultSdkSource: VaultSdkSource,
settingsRepository: SettingsRepository, settingsRepository: SettingsRepository,
appForegroundManager: AppForegroundManager, appStateManager: AppStateManager,
userLogoutManager: UserLogoutManager, userLogoutManager: UserLogoutManager,
dispatcherManager: DispatcherManager, dispatcherManager: DispatcherManager,
trustedDeviceManager: TrustedDeviceManager, trustedDeviceManager: TrustedDeviceManager,
@ -82,7 +82,7 @@ object VaultManagerModule {
authSdkSource = authSdkSource, authSdkSource = authSdkSource,
vaultSdkSource = vaultSdkSource, vaultSdkSource = vaultSdkSource,
settingsRepository = settingsRepository, settingsRepository = settingsRepository,
appForegroundManager = appForegroundManager, appStateManager = appStateManager,
userLogoutManager = userLogoutManager, userLogoutManager = userLogoutManager,
dispatcherManager = dispatcherManager, dispatcherManager = dispatcherManager,
trustedDeviceManager = trustedDeviceManager, trustedDeviceManager = trustedDeviceManager,

View file

@ -4,7 +4,7 @@ import android.content.Context
import androidx.lifecycle.LifecycleCoroutineScope import androidx.lifecycle.LifecycleCoroutineScope
import app.cash.turbine.test import app.cash.turbine.test
import com.x8bit.bitwarden.data.autofill.accessibility.util.isAccessibilityServiceEnabled import com.x8bit.bitwarden.data.autofill.accessibility.util.isAccessibilityServiceEnabled
import com.x8bit.bitwarden.data.platform.manager.AppForegroundManager import com.x8bit.bitwarden.data.platform.manager.AppStateManager
import com.x8bit.bitwarden.data.platform.manager.model.AppForegroundState import com.x8bit.bitwarden.data.platform.manager.model.AppForegroundState
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
@ -26,7 +26,7 @@ class AccessibilityActivityManagerTest {
private val accessibilityEnabledManager: AccessibilityEnabledManager = private val accessibilityEnabledManager: AccessibilityEnabledManager =
AccessibilityEnabledManagerImpl() AccessibilityEnabledManagerImpl()
private val mutableAppForegroundStateFlow = MutableStateFlow(AppForegroundState.BACKGROUNDED) private val mutableAppForegroundStateFlow = MutableStateFlow(AppForegroundState.BACKGROUNDED)
private val appForegroundManager: AppForegroundManager = mockk { private val appStateManager: AppStateManager = mockk {
every { appForegroundStateFlow } returns mutableAppForegroundStateFlow every { appForegroundStateFlow } returns mutableAppForegroundStateFlow
} }
private val lifecycleScope = mockk<LifecycleCoroutineScope> { private val lifecycleScope = mockk<LifecycleCoroutineScope> {
@ -43,7 +43,7 @@ class AccessibilityActivityManagerTest {
autofillActivityManager = AccessibilityActivityManagerImpl( autofillActivityManager = AccessibilityActivityManagerImpl(
context = context, context = context,
accessibilityEnabledManager = accessibilityEnabledManager, accessibilityEnabledManager = accessibilityEnabledManager,
appForegroundManager = appForegroundManager, appStateManager = appStateManager,
lifecycleScope = lifecycleScope, lifecycleScope = lifecycleScope,
) )
} }

View file

@ -3,7 +3,7 @@ package com.x8bit.bitwarden.data.autofill.manager
import android.view.autofill.AutofillManager import android.view.autofill.AutofillManager
import androidx.lifecycle.LifecycleCoroutineScope import androidx.lifecycle.LifecycleCoroutineScope
import app.cash.turbine.test import app.cash.turbine.test
import com.x8bit.bitwarden.data.platform.manager.AppForegroundManager import com.x8bit.bitwarden.data.platform.manager.AppStateManager
import com.x8bit.bitwarden.data.platform.manager.model.AppForegroundState import com.x8bit.bitwarden.data.platform.manager.model.AppForegroundState
import io.mockk.every import io.mockk.every
import io.mockk.just import io.mockk.just
@ -28,7 +28,7 @@ class AutofillActivityManagerTest {
private val autofillEnabledManager: AutofillEnabledManager = AutofillEnabledManagerImpl() private val autofillEnabledManager: AutofillEnabledManager = AutofillEnabledManagerImpl()
private val mutableAppForegroundStateFlow = MutableStateFlow(AppForegroundState.BACKGROUNDED) private val mutableAppForegroundStateFlow = MutableStateFlow(AppForegroundState.BACKGROUNDED)
private val appForegroundManager: AppForegroundManager = mockk { private val appStateManager: AppStateManager = mockk {
every { appForegroundStateFlow } returns mutableAppForegroundStateFlow every { appForegroundStateFlow } returns mutableAppForegroundStateFlow
} }
private val lifecycleScope = mockk<LifecycleCoroutineScope> { private val lifecycleScope = mockk<LifecycleCoroutineScope> {
@ -39,7 +39,7 @@ class AutofillActivityManagerTest {
@Suppress("unused") @Suppress("unused")
private val autofillActivityManager: AutofillActivityManager = AutofillActivityManagerImpl( private val autofillActivityManager: AutofillActivityManager = AutofillActivityManagerImpl(
autofillManager = autofillManager, autofillManager = autofillManager,
appForegroundManager = appForegroundManager, appStateManager = appStateManager,
autofillEnabledManager = autofillEnabledManager, autofillEnabledManager = autofillEnabledManager,
lifecycleScope = lifecycleScope, lifecycleScope = lifecycleScope,
) )

View file

@ -1,44 +0,0 @@
package com.x8bit.bitwarden.data.platform.manager
import app.cash.turbine.test
import com.x8bit.bitwarden.data.platform.manager.model.AppForegroundState
import com.x8bit.bitwarden.data.util.FakeLifecycleOwner
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
class AppForegroundManagerTest {
private val fakeLifecycleOwner = FakeLifecycleOwner()
private val appForegroundManager = AppForegroundManagerImpl(
processLifecycleOwner = fakeLifecycleOwner,
)
@Suppress("MaxLineLength")
@Test
fun `appForegroundStateFlow should emit whenever the underlying ProcessLifecycleOwner receives start and stop events`() =
runTest {
appForegroundManager.appForegroundStateFlow.test {
// Initial state is BACKGROUNDED
assertEquals(
AppForegroundState.BACKGROUNDED,
awaitItem(),
)
fakeLifecycleOwner.lifecycle.dispatchOnStart()
assertEquals(
AppForegroundState.FOREGROUNDED,
awaitItem(),
)
fakeLifecycleOwner.lifecycle.dispatchOnStop()
assertEquals(
AppForegroundState.BACKGROUNDED,
awaitItem(),
)
}
}
}

View file

@ -0,0 +1,82 @@
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.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.runs
import io.mockk.slot
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
class AppStateManagerTest {
private val activityLifecycleCallbacks = slot<Application.ActivityLifecycleCallbacks>()
private val application = mockk<Application> {
every { registerActivityLifecycleCallbacks(capture(activityLifecycleCallbacks)) } just runs
}
private val fakeLifecycleOwner = FakeLifecycleOwner()
private val appStateManager = AppStateManagerImpl(
application = application,
processLifecycleOwner = fakeLifecycleOwner,
)
@Suppress("MaxLineLength")
@Test
fun `appForegroundStateFlow should emit whenever the underlying ProcessLifecycleOwner receives start and stop events`() =
runTest {
appStateManager.appForegroundStateFlow.test {
// Initial state is BACKGROUNDED
assertEquals(
AppForegroundState.BACKGROUNDED,
awaitItem(),
)
fakeLifecycleOwner.lifecycle.dispatchOnStart()
assertEquals(
AppForegroundState.FOREGROUNDED,
awaitItem(),
)
fakeLifecycleOwner.lifecycle.dispatchOnStop()
assertEquals(
AppForegroundState.BACKGROUNDED,
awaitItem(),
)
}
}
@Suppress("MaxLineLength")
@Test
fun `appCreatedStateFlow should emit whenever the underlying activities are all destroyed or a creation event occurs`() =
runTest {
val activity = mockk<Activity> {
every { isChangingConfigurations } returns false
}
appStateManager.appCreatedStateFlow.test {
// Initial state is DESTROYED
assertEquals(AppCreationState.DESTROYED, awaitItem())
activityLifecycleCallbacks.captured.onActivityCreated(activity, null)
assertEquals(AppCreationState.CREATED, awaitItem())
activityLifecycleCallbacks.captured.onActivityCreated(activity, null)
expectNoEvents()
activityLifecycleCallbacks.captured.onActivityDestroyed(activity)
expectNoEvents()
activityLifecycleCallbacks.captured.onActivityDestroyed(activity)
assertEquals(AppCreationState.DESTROYED, awaitItem())
}
}
}

View file

@ -7,7 +7,7 @@ import android.os.Bundle
import com.x8bit.bitwarden.data.auth.datasource.disk.model.EnvironmentUrlDataJson import com.x8bit.bitwarden.data.auth.datasource.disk.model.EnvironmentUrlDataJson
import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager
import com.x8bit.bitwarden.data.platform.manager.model.AppForegroundState import com.x8bit.bitwarden.data.platform.manager.model.AppForegroundState
import com.x8bit.bitwarden.data.platform.manager.util.FakeAppForegroundManager import com.x8bit.bitwarden.data.platform.manager.util.FakeAppStateManager
import com.x8bit.bitwarden.data.platform.repository.model.Environment import com.x8bit.bitwarden.data.platform.repository.model.Environment
import com.x8bit.bitwarden.data.platform.repository.util.FakeEnvironmentRepository import com.x8bit.bitwarden.data.platform.repository.util.FakeEnvironmentRepository
import io.mockk.clearMocks import io.mockk.clearMocks
@ -27,7 +27,7 @@ class RestrictionManagerTest {
every { registerReceiver(any(), any()) } returns null every { registerReceiver(any(), any()) } returns null
every { unregisterReceiver(any()) } just runs every { unregisterReceiver(any()) } just runs
} }
private val fakeAppForegroundManager = FakeAppForegroundManager() private val fakeAppStateManager = FakeAppStateManager()
private val fakeDispatcherManager = FakeDispatcherManager().apply { private val fakeDispatcherManager = FakeDispatcherManager().apply {
setMain(unconfined) setMain(unconfined)
} }
@ -35,7 +35,7 @@ class RestrictionManagerTest {
private val restrictionsManager = mockk<RestrictionsManager>() private val restrictionsManager = mockk<RestrictionsManager>()
private val restrictionManager: RestrictionManager = RestrictionManagerImpl( private val restrictionManager: RestrictionManager = RestrictionManagerImpl(
appForegroundManager = fakeAppForegroundManager, appStateManager = fakeAppStateManager,
dispatcherManager = fakeDispatcherManager, dispatcherManager = fakeDispatcherManager,
context = context, context = context,
environmentRepository = fakeEnvironmentRepository, environmentRepository = fakeEnvironmentRepository,
@ -51,7 +51,7 @@ class RestrictionManagerTest {
fun `on app foreground with a null bundle should register receiver and do nothing else`() { fun `on app foreground with a null bundle should register receiver and do nothing else`() {
every { restrictionsManager.applicationRestrictions } returns null every { restrictionsManager.applicationRestrictions } returns null
fakeAppForegroundManager.appForegroundState = AppForegroundState.FOREGROUNDED fakeAppStateManager.appForegroundState = AppForegroundState.FOREGROUNDED
verify(exactly = 1) { verify(exactly = 1) {
context.registerReceiver(any(), any()) context.registerReceiver(any(), any())
@ -63,7 +63,7 @@ class RestrictionManagerTest {
fun `on app foreground with an empty bundle should register receiver and do nothing else`() { fun `on app foreground with an empty bundle should register receiver and do nothing else`() {
every { restrictionsManager.applicationRestrictions } returns mockBundle() every { restrictionsManager.applicationRestrictions } returns mockBundle()
fakeAppForegroundManager.appForegroundState = AppForegroundState.FOREGROUNDED fakeAppStateManager.appForegroundState = AppForegroundState.FOREGROUNDED
verify(exactly = 1) { verify(exactly = 1) {
context.registerReceiver(any(), any()) context.registerReceiver(any(), any())
@ -78,7 +78,7 @@ class RestrictionManagerTest {
restrictionsManager.applicationRestrictions restrictionsManager.applicationRestrictions
} returns mockBundle("key" to "unknown") } returns mockBundle("key" to "unknown")
fakeAppForegroundManager.appForegroundState = AppForegroundState.FOREGROUNDED fakeAppStateManager.appForegroundState = AppForegroundState.FOREGROUNDED
verify(exactly = 1) { verify(exactly = 1) {
context.registerReceiver(any(), any()) context.registerReceiver(any(), any())
@ -93,7 +93,7 @@ class RestrictionManagerTest {
restrictionsManager.applicationRestrictions restrictionsManager.applicationRestrictions
} returns mockBundle("baseEnvironmentUrl" to "https://vault.bitwarden.com") } returns mockBundle("baseEnvironmentUrl" to "https://vault.bitwarden.com")
fakeAppForegroundManager.appForegroundState = AppForegroundState.FOREGROUNDED fakeAppStateManager.appForegroundState = AppForegroundState.FOREGROUNDED
verify(exactly = 1) { verify(exactly = 1) {
context.registerReceiver(any(), any()) context.registerReceiver(any(), any())
@ -109,7 +109,7 @@ class RestrictionManagerTest {
restrictionsManager.applicationRestrictions restrictionsManager.applicationRestrictions
} returns mockBundle("baseEnvironmentUrl" to baseUrl) } returns mockBundle("baseEnvironmentUrl" to baseUrl)
fakeAppForegroundManager.appForegroundState = AppForegroundState.FOREGROUNDED fakeAppStateManager.appForegroundState = AppForegroundState.FOREGROUNDED
verify(exactly = 1) { verify(exactly = 1) {
context.registerReceiver(any(), any()) context.registerReceiver(any(), any())
@ -130,7 +130,7 @@ class RestrictionManagerTest {
restrictionsManager.applicationRestrictions restrictionsManager.applicationRestrictions
} returns mockBundle("baseEnvironmentUrl" to "https://vault.bitwarden.eu") } returns mockBundle("baseEnvironmentUrl" to "https://vault.bitwarden.eu")
fakeAppForegroundManager.appForegroundState = AppForegroundState.FOREGROUNDED fakeAppStateManager.appForegroundState = AppForegroundState.FOREGROUNDED
verify(exactly = 1) { verify(exactly = 1) {
context.registerReceiver(any(), any()) context.registerReceiver(any(), any())
@ -147,7 +147,7 @@ class RestrictionManagerTest {
restrictionsManager.applicationRestrictions restrictionsManager.applicationRestrictions
} returns mockBundle("baseEnvironmentUrl" to baseUrl) } returns mockBundle("baseEnvironmentUrl" to baseUrl)
fakeAppForegroundManager.appForegroundState = AppForegroundState.FOREGROUNDED fakeAppStateManager.appForegroundState = AppForegroundState.FOREGROUNDED
verify(exactly = 1) { verify(exactly = 1) {
context.registerReceiver(any(), any()) context.registerReceiver(any(), any())
@ -172,7 +172,7 @@ class RestrictionManagerTest {
restrictionsManager.applicationRestrictions restrictionsManager.applicationRestrictions
} returns mockBundle("baseEnvironmentUrl" to baseUrl) } returns mockBundle("baseEnvironmentUrl" to baseUrl)
fakeAppForegroundManager.appForegroundState = AppForegroundState.FOREGROUNDED fakeAppStateManager.appForegroundState = AppForegroundState.FOREGROUNDED
verify(exactly = 1) { verify(exactly = 1) {
context.registerReceiver(any(), any()) context.registerReceiver(any(), any())
@ -192,7 +192,7 @@ class RestrictionManagerTest {
restrictionsManager.applicationRestrictions restrictionsManager.applicationRestrictions
} returns mockBundle("baseEnvironmentUrl" to baseUrl) } returns mockBundle("baseEnvironmentUrl" to baseUrl)
fakeAppForegroundManager.appForegroundState = AppForegroundState.FOREGROUNDED fakeAppStateManager.appForegroundState = AppForegroundState.FOREGROUNDED
verify(exactly = 1) { verify(exactly = 1) {
context.registerReceiver(any(), any()) context.registerReceiver(any(), any())
@ -207,7 +207,7 @@ class RestrictionManagerTest {
@Test @Test
fun `on app background when not foregrounded should do nothing`() { fun `on app background when not foregrounded should do nothing`() {
fakeAppForegroundManager.appForegroundState = AppForegroundState.BACKGROUNDED fakeAppStateManager.appForegroundState = AppForegroundState.BACKGROUNDED
verify(exactly = 0) { verify(exactly = 0) {
context.unregisterReceiver(any()) context.unregisterReceiver(any())
@ -218,10 +218,10 @@ class RestrictionManagerTest {
@Test @Test
fun `on app background after foreground should unregister receiver`() { fun `on app background after foreground should unregister receiver`() {
every { restrictionsManager.applicationRestrictions } returns null every { restrictionsManager.applicationRestrictions } returns null
fakeAppForegroundManager.appForegroundState = AppForegroundState.FOREGROUNDED fakeAppStateManager.appForegroundState = AppForegroundState.FOREGROUNDED
clearMocks(context, restrictionsManager, answers = false) clearMocks(context, restrictionsManager, answers = false)
fakeAppForegroundManager.appForegroundState = AppForegroundState.BACKGROUNDED fakeAppStateManager.appForegroundState = AppForegroundState.BACKGROUNDED
verify(exactly = 1) { verify(exactly = 1) {
context.unregisterReceiver(any()) context.unregisterReceiver(any())

View file

@ -1,20 +1,34 @@
package com.x8bit.bitwarden.data.platform.manager.util package com.x8bit.bitwarden.data.platform.manager.util
import com.x8bit.bitwarden.data.platform.manager.AppForegroundManager import com.x8bit.bitwarden.data.platform.manager.AppStateManager
import com.x8bit.bitwarden.data.platform.manager.model.AppCreationState
import com.x8bit.bitwarden.data.platform.manager.model.AppForegroundState import com.x8bit.bitwarden.data.platform.manager.model.AppForegroundState
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
/** /**
* A faked implementation of [AppForegroundManager] * A faked implementation of [AppStateManager]
*/ */
class FakeAppForegroundManager : AppForegroundManager { class FakeAppStateManager : AppStateManager {
private val mutableAppCreationStateFlow = MutableStateFlow(AppCreationState.DESTROYED)
private val mutableAppForegroundStateFlow = MutableStateFlow(AppForegroundState.BACKGROUNDED) private val mutableAppForegroundStateFlow = MutableStateFlow(AppForegroundState.BACKGROUNDED)
override val appCreatedStateFlow: StateFlow<AppCreationState>
get() = mutableAppCreationStateFlow.asStateFlow()
override val appForegroundStateFlow: StateFlow<AppForegroundState> override val appForegroundStateFlow: StateFlow<AppForegroundState>
get() = mutableAppForegroundStateFlow.asStateFlow() get() = mutableAppForegroundStateFlow.asStateFlow()
/**
* The current [AppCreationState] tracked by the [appCreatedStateFlow].
*/
var appCreationState: AppCreationState
get() = mutableAppCreationStateFlow.value
set(value) {
mutableAppCreationStateFlow.value = value
}
/** /**
* The current [AppForegroundState] tracked by the [appForegroundStateFlow]. * The current [AppForegroundState] tracked by the [appForegroundStateFlow].
*/ */

View file

@ -15,8 +15,9 @@ import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
import com.x8bit.bitwarden.data.auth.manager.model.LogoutEvent import com.x8bit.bitwarden.data.auth.manager.model.LogoutEvent
import com.x8bit.bitwarden.data.auth.repository.util.toSdkParams import com.x8bit.bitwarden.data.auth.repository.util.toSdkParams
import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager
import com.x8bit.bitwarden.data.platform.manager.model.AppCreationState
import com.x8bit.bitwarden.data.platform.manager.model.AppForegroundState import com.x8bit.bitwarden.data.platform.manager.model.AppForegroundState
import com.x8bit.bitwarden.data.platform.manager.util.FakeAppForegroundManager import com.x8bit.bitwarden.data.platform.manager.util.FakeAppStateManager
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeout import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeout
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction
@ -53,7 +54,7 @@ import org.junit.jupiter.api.Test
@Suppress("LargeClass") @Suppress("LargeClass")
class VaultLockManagerTest { class VaultLockManagerTest {
private val fakeAuthDiskSource = FakeAuthDiskSource() private val fakeAuthDiskSource = FakeAuthDiskSource()
private val fakeAppForegroundManager = FakeAppForegroundManager() private val fakeAppStateManager = FakeAppStateManager()
private val authSdkSource: AuthSdkSource = mockk { private val authSdkSource: AuthSdkSource = mockk {
coEvery { coEvery {
hashPassword( hashPassword(
@ -90,7 +91,7 @@ class VaultLockManagerTest {
authSdkSource = authSdkSource, authSdkSource = authSdkSource,
vaultSdkSource = vaultSdkSource, vaultSdkSource = vaultSdkSource,
settingsRepository = settingsRepository, settingsRepository = settingsRepository,
appForegroundManager = fakeAppForegroundManager, appStateManager = fakeAppStateManager,
userLogoutManager = userLogoutManager, userLogoutManager = userLogoutManager,
trustedDeviceManager = trustedDeviceManager, trustedDeviceManager = trustedDeviceManager,
dispatcherManager = fakeDispatcherManager, dispatcherManager = fakeDispatcherManager,
@ -147,18 +148,86 @@ class VaultLockManagerTest {
} }
@Test @Test
fun `app coming into background subsequent times should perform timeout action if necessary`() { fun `app being destroyed should perform timeout action if necessary`() {
setAccountTokens() setAccountTokens()
fakeAuthDiskSource.userState = MOCK_USER_STATE fakeAuthDiskSource.userState = MOCK_USER_STATE
// Start in a foregrounded state
fakeAppForegroundManager.appForegroundState = AppForegroundState.FOREGROUNDED
// Will be used within each loop to reset the test to a suitable initial state. // Will be used within each loop to reset the test to a suitable initial state.
fun resetTest(vaultTimeout: VaultTimeout) { fun resetTest(vaultTimeout: VaultTimeout) {
clearVerifications(userLogoutManager) clearVerifications(userLogoutManager)
mutableVaultTimeoutStateFlow.value = vaultTimeout mutableVaultTimeoutStateFlow.value = vaultTimeout
fakeAppForegroundManager.appForegroundState = AppForegroundState.FOREGROUNDED 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
fakeAppStateManager.appForegroundState = AppForegroundState.FOREGROUNDED
// 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.appForegroundState = AppForegroundState.FOREGROUNDED
verifyUnlockedVaultBlocking(userId = USER_ID) verifyUnlockedVaultBlocking(userId = USER_ID)
assertTrue(vaultLockManager.isVaultUnlocked(USER_ID)) assertTrue(vaultLockManager.isVaultUnlocked(USER_ID))
} }
@ -168,7 +237,7 @@ class VaultLockManagerTest {
MOCK_TIMEOUTS.forEach { vaultTimeout -> MOCK_TIMEOUTS.forEach { vaultTimeout ->
resetTest(vaultTimeout = vaultTimeout) resetTest(vaultTimeout = vaultTimeout)
fakeAppForegroundManager.appForegroundState = AppForegroundState.BACKGROUNDED fakeAppStateManager.appForegroundState = AppForegroundState.BACKGROUNDED
// Advance by 6 minutes. Only actions with a timeout less than this will be triggered. // Advance by 6 minutes. Only actions with a timeout less than this will be triggered.
testDispatcher.scheduler.advanceTimeBy(delayTimeMillis = 6 * 60 * 1000L) testDispatcher.scheduler.advanceTimeBy(delayTimeMillis = 6 * 60 * 1000L)
@ -201,7 +270,7 @@ class VaultLockManagerTest {
mutableVaultTimeoutActionStateFlow.value = VaultTimeoutAction.LOGOUT mutableVaultTimeoutActionStateFlow.value = VaultTimeoutAction.LOGOUT
MOCK_TIMEOUTS.forEach { vaultTimeout -> MOCK_TIMEOUTS.forEach { vaultTimeout ->
resetTest(vaultTimeout = vaultTimeout) resetTest(vaultTimeout = vaultTimeout)
fakeAppForegroundManager.appForegroundState = AppForegroundState.BACKGROUNDED fakeAppStateManager.appForegroundState = AppForegroundState.BACKGROUNDED
// Advance by 6 minutes. Only actions with a timeout less than this will be triggered. // Advance by 6 minutes. Only actions with a timeout less than this will be triggered.
testDispatcher.scheduler.advanceTimeBy(delayTimeMillis = 6 * 60 * 1000L) testDispatcher.scheduler.advanceTimeBy(delayTimeMillis = 6 * 60 * 1000L)
@ -236,11 +305,11 @@ class VaultLockManagerTest {
mutableVaultTimeoutActionStateFlow.value = VaultTimeoutAction.LOCK mutableVaultTimeoutActionStateFlow.value = VaultTimeoutAction.LOCK
mutableVaultTimeoutStateFlow.value = VaultTimeout.Never mutableVaultTimeoutStateFlow.value = VaultTimeout.Never
fakeAppForegroundManager.appForegroundState = AppForegroundState.BACKGROUNDED fakeAppStateManager.appForegroundState = AppForegroundState.BACKGROUNDED
verifyUnlockedVaultBlocking(userId = USER_ID) verifyUnlockedVaultBlocking(userId = USER_ID)
assertTrue(vaultLockManager.isVaultUnlocked(USER_ID)) assertTrue(vaultLockManager.isVaultUnlocked(USER_ID))
fakeAppForegroundManager.appForegroundState = AppForegroundState.FOREGROUNDED fakeAppStateManager.appForegroundState = AppForegroundState.FOREGROUNDED
assertTrue(vaultLockManager.isVaultUnlocked(USER_ID)) assertTrue(vaultLockManager.isVaultUnlocked(USER_ID))
} }
@ -253,11 +322,11 @@ class VaultLockManagerTest {
mutableVaultTimeoutActionStateFlow.value = VaultTimeoutAction.LOCK mutableVaultTimeoutActionStateFlow.value = VaultTimeoutAction.LOCK
mutableVaultTimeoutStateFlow.value = VaultTimeout.OnAppRestart mutableVaultTimeoutStateFlow.value = VaultTimeout.OnAppRestart
fakeAppForegroundManager.appForegroundState = AppForegroundState.BACKGROUNDED fakeAppStateManager.appForegroundState = AppForegroundState.BACKGROUNDED
verifyUnlockedVaultBlocking(userId = USER_ID) verifyUnlockedVaultBlocking(userId = USER_ID)
assertTrue(vaultLockManager.isVaultUnlocked(USER_ID)) assertTrue(vaultLockManager.isVaultUnlocked(USER_ID))
fakeAppForegroundManager.appForegroundState = AppForegroundState.FOREGROUNDED fakeAppStateManager.appForegroundState = AppForegroundState.FOREGROUNDED
assertFalse(vaultLockManager.isVaultUnlocked(USER_ID)) assertFalse(vaultLockManager.isVaultUnlocked(USER_ID))
} }
@ -269,11 +338,11 @@ class VaultLockManagerTest {
mutableVaultTimeoutActionStateFlow.value = VaultTimeoutAction.LOCK mutableVaultTimeoutActionStateFlow.value = VaultTimeoutAction.LOCK
mutableVaultTimeoutStateFlow.value = VaultTimeout.ThirtyMinutes mutableVaultTimeoutStateFlow.value = VaultTimeout.ThirtyMinutes
fakeAppForegroundManager.appForegroundState = AppForegroundState.BACKGROUNDED fakeAppStateManager.appForegroundState = AppForegroundState.BACKGROUNDED
verifyUnlockedVaultBlocking(userId = USER_ID) verifyUnlockedVaultBlocking(userId = USER_ID)
assertTrue(vaultLockManager.isVaultUnlocked(USER_ID)) assertTrue(vaultLockManager.isVaultUnlocked(USER_ID))
fakeAppForegroundManager.appForegroundState = AppForegroundState.FOREGROUNDED fakeAppStateManager.appForegroundState = AppForegroundState.FOREGROUNDED
assertFalse(vaultLockManager.isVaultUnlocked(USER_ID)) assertFalse(vaultLockManager.isVaultUnlocked(USER_ID))
} }
@ -286,7 +355,7 @@ class VaultLockManagerTest {
mutableVaultTimeoutActionStateFlow.value = VaultTimeoutAction.LOCK mutableVaultTimeoutActionStateFlow.value = VaultTimeoutAction.LOCK
mutableVaultTimeoutStateFlow.value = VaultTimeout.ThirtyMinutes mutableVaultTimeoutStateFlow.value = VaultTimeout.ThirtyMinutes
fakeAppForegroundManager.appForegroundState = AppForegroundState.FOREGROUNDED fakeAppStateManager.appForegroundState = AppForegroundState.FOREGROUNDED
assertFalse(vaultLockManager.isVaultUnlocked(USER_ID)) assertFalse(vaultLockManager.isVaultUnlocked(USER_ID))
} }
@ -298,11 +367,11 @@ class VaultLockManagerTest {
mutableVaultTimeoutActionStateFlow.value = VaultTimeoutAction.LOGOUT mutableVaultTimeoutActionStateFlow.value = VaultTimeoutAction.LOGOUT
mutableVaultTimeoutStateFlow.value = VaultTimeout.ThirtyMinutes mutableVaultTimeoutStateFlow.value = VaultTimeout.ThirtyMinutes
fakeAppForegroundManager.appForegroundState = AppForegroundState.BACKGROUNDED fakeAppStateManager.appForegroundState = AppForegroundState.BACKGROUNDED
verifyUnlockedVaultBlocking(userId = USER_ID) verifyUnlockedVaultBlocking(userId = USER_ID)
assertTrue(vaultLockManager.isVaultUnlocked(USER_ID)) assertTrue(vaultLockManager.isVaultUnlocked(USER_ID))
fakeAppForegroundManager.appForegroundState = AppForegroundState.FOREGROUNDED fakeAppStateManager.appForegroundState = AppForegroundState.FOREGROUNDED
verify(exactly = 1) { settingsRepository.getVaultTimeoutActionStateFlow(USER_ID) } verify(exactly = 1) { settingsRepository.getVaultTimeoutActionStateFlow(USER_ID) }
} }
@ -314,11 +383,11 @@ class VaultLockManagerTest {
mutableVaultTimeoutActionStateFlow.value = VaultTimeoutAction.LOGOUT mutableVaultTimeoutActionStateFlow.value = VaultTimeoutAction.LOGOUT
mutableVaultTimeoutStateFlow.value = VaultTimeout.ThirtyMinutes mutableVaultTimeoutStateFlow.value = VaultTimeout.ThirtyMinutes
fakeAppForegroundManager.appForegroundState = AppForegroundState.BACKGROUNDED fakeAppStateManager.appForegroundState = AppForegroundState.BACKGROUNDED
verifyUnlockedVaultBlocking(userId = USER_ID) verifyUnlockedVaultBlocking(userId = USER_ID)
assertTrue(vaultLockManager.isVaultUnlocked(USER_ID)) assertTrue(vaultLockManager.isVaultUnlocked(USER_ID))
fakeAppForegroundManager.appForegroundState = AppForegroundState.FOREGROUNDED fakeAppStateManager.appForegroundState = AppForegroundState.FOREGROUNDED
verify(exactly = 0) { settingsRepository.getVaultTimeoutActionStateFlow(USER_ID) } verify(exactly = 0) { settingsRepository.getVaultTimeoutActionStateFlow(USER_ID) }
} }
@ -329,14 +398,14 @@ class VaultLockManagerTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE fakeAuthDiskSource.userState = MOCK_USER_STATE
// Start in a foregrounded state // Start in a foregrounded state
fakeAppForegroundManager.appForegroundState = AppForegroundState.BACKGROUNDED fakeAppStateManager.appForegroundState = AppForegroundState.BACKGROUNDED
// We want to skip the first time since that is different from subsequent foregrounds // We want to skip the first time since that is different from subsequent foregrounds
fakeAppForegroundManager.appForegroundState = AppForegroundState.FOREGROUNDED fakeAppStateManager.appForegroundState = AppForegroundState.FOREGROUNDED
// Will be used within each loop to reset the test to a suitable initial state. // Will be used within each loop to reset the test to a suitable initial state.
fun resetTest(vaultTimeout: VaultTimeout) { fun resetTest(vaultTimeout: VaultTimeout) {
mutableVaultTimeoutStateFlow.value = vaultTimeout mutableVaultTimeoutStateFlow.value = vaultTimeout
fakeAppForegroundManager.appForegroundState = AppForegroundState.BACKGROUNDED fakeAppStateManager.appForegroundState = AppForegroundState.BACKGROUNDED
clearVerifications(userLogoutManager) clearVerifications(userLogoutManager)
verifyUnlockedVaultBlocking(userId = USER_ID) verifyUnlockedVaultBlocking(userId = USER_ID)
assertTrue(vaultLockManager.isVaultUnlocked(USER_ID)) assertTrue(vaultLockManager.isVaultUnlocked(USER_ID))
@ -347,7 +416,7 @@ class VaultLockManagerTest {
MOCK_TIMEOUTS.forEach { vaultTimeout -> MOCK_TIMEOUTS.forEach { vaultTimeout ->
resetTest(vaultTimeout = vaultTimeout) resetTest(vaultTimeout = vaultTimeout)
fakeAppForegroundManager.appForegroundState = AppForegroundState.FOREGROUNDED fakeAppStateManager.appForegroundState = AppForegroundState.FOREGROUNDED
// Advance by 6 minutes. Only actions with a timeout less than this will be triggered. // Advance by 6 minutes. Only actions with a timeout less than this will be triggered.
testDispatcher.scheduler.advanceTimeBy(delayTimeMillis = 6 * 60 * 1000L) testDispatcher.scheduler.advanceTimeBy(delayTimeMillis = 6 * 60 * 1000L)
@ -361,7 +430,7 @@ class VaultLockManagerTest {
MOCK_TIMEOUTS.forEach { vaultTimeout -> MOCK_TIMEOUTS.forEach { vaultTimeout ->
resetTest(vaultTimeout = vaultTimeout) resetTest(vaultTimeout = vaultTimeout)
fakeAppForegroundManager.appForegroundState = AppForegroundState.FOREGROUNDED fakeAppStateManager.appForegroundState = AppForegroundState.FOREGROUNDED
// Advance by 6 minutes. Only actions with a timeout less than this will be triggered. // Advance by 6 minutes. Only actions with a timeout less than this will be triggered.
testDispatcher.scheduler.advanceTimeBy(delayTimeMillis = 6 * 60 * 1000L) testDispatcher.scheduler.advanceTimeBy(delayTimeMillis = 6 * 60 * 1000L)
@ -376,7 +445,7 @@ class VaultLockManagerTest {
fun `switching users should perform lock actions or start a timer for each user if necessary`() { fun `switching users should perform lock actions or start a timer for each user if necessary`() {
val userId2 = "mockId-2" val userId2 = "mockId-2"
setAccountTokens(listOf(USER_ID, userId2)) setAccountTokens(listOf(USER_ID, userId2))
fakeAppForegroundManager.appForegroundState = AppForegroundState.FOREGROUNDED fakeAppStateManager.appForegroundState = AppForegroundState.FOREGROUNDED
fakeAuthDiskSource.userState = UserStateJson( fakeAuthDiskSource.userState = UserStateJson(
activeUserId = USER_ID, activeUserId = USER_ID,
accounts = mapOf( accounts = mapOf(

View file

@ -46,7 +46,7 @@ Note that these data sources are constructed in a manner that adheres to a very
### Managers ### Managers
Manager classes represent something of a middle level of the data layer. While some manager classes like [VaultLockManager](../app/src/main/java/com/x8bit/bitwarden/data/vault/manager/VaultLockManager.kt) depend on the the lower-level data sources, others are wrappers around OS-level classes (ex: [AppForegroundManager](../app/src/main/java/com/x8bit/bitwarden/data/platform/manager/AppForegroundManager.kt)) while others have no dependencies at all (ex: [SpecialCircumstanceManager](../app/src/main/java/com/x8bit/bitwarden/data/platform/manager/SpecialCircumstanceManager.kt)). The commonality amongst the manager classes is that they tend to have a single discrete responsibility. These classes may also exist solely in the data layer for use inside a repository or manager class, like [AppForegroundManager](../app/src/main/java/com/x8bit/bitwarden/data/platform/manager/AppForegroundManager.kt), or may be exposed directly to the UI layer, like [SpecialCircumstanceManager](../app/src/main/java/com/x8bit/bitwarden/data/platform/manager/SpecialCircumstanceManager.kt). Manager classes represent something of a middle level of the data layer. While some manager classes like [VaultLockManager](../app/src/main/java/com/x8bit/bitwarden/data/vault/manager/VaultLockManager.kt) depend on the the lower-level data sources, others are wrappers around OS-level classes (ex: [AppStateManager](../app/src/main/java/com/x8bit/bitwarden/data/platform/manager/AppStateManager.kt)) while others have no dependencies at all (ex: [SpecialCircumstanceManager](../app/src/main/java/com/x8bit/bitwarden/data/platform/manager/SpecialCircumstanceManager.kt)). The commonality amongst the manager classes is that they tend to have a single discrete responsibility. These classes may also exist solely in the data layer for use inside a repository or manager class, like [AppStateManager](../app/src/main/java/com/x8bit/bitwarden/data/platform/manager/AppStateManager.kt), or may be exposed directly to the UI layer, like [SpecialCircumstanceManager](../app/src/main/java/com/x8bit/bitwarden/data/platform/manager/SpecialCircumstanceManager.kt).
### Repositories ### Repositories