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.AccessibilityActivityManagerImpl
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.Provides
import dagger.hilt.InstallIn
@ -24,13 +24,13 @@ object ActivityAccessibilityModule {
fun providesAccessibilityActivityManager(
@ApplicationContext context: Context,
accessibilityEnabledManager: AccessibilityEnabledManager,
appForegroundManager: AppForegroundManager,
appStateManager: AppStateManager,
lifecycleScope: LifecycleCoroutineScope,
): AccessibilityActivityManager =
AccessibilityActivityManagerImpl(
context = context,
accessibilityEnabledManager = accessibilityEnabledManager,
appForegroundManager = appForegroundManager,
appStateManager = appStateManager,
lifecycleScope = lifecycleScope,
)
}

View file

@ -3,7 +3,7 @@ package com.x8bit.bitwarden.data.autofill.accessibility.manager
import android.content.Context
import androidx.lifecycle.LifecycleCoroutineScope
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.onEach
@ -13,11 +13,11 @@ import kotlinx.coroutines.flow.onEach
class AccessibilityActivityManagerImpl(
private val context: Context,
private val accessibilityEnabledManager: AccessibilityEnabledManager,
appForegroundManager: AppForegroundManager,
appStateManager: AppStateManager,
lifecycleScope: LifecycleCoroutineScope,
) : AccessibilityActivityManager {
init {
appForegroundManager
appStateManager
.appForegroundStateFlow
.onEach {
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.AutofillActivityManagerImpl
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.Provides
import dagger.hilt.InstallIn
@ -27,13 +27,13 @@ object ActivityAutofillModule {
@Provides
fun provideAutofillActivityManager(
@ActivityScopedManager autofillManager: AutofillManager,
appForegroundManager: AppForegroundManager,
appStateManager: AppStateManager,
autofillEnabledManager: AutofillEnabledManager,
lifecycleScope: LifecycleCoroutineScope,
): AutofillActivityManager =
AutofillActivityManagerImpl(
autofillManager = autofillManager,
appForegroundManager = appForegroundManager,
appStateManager = appStateManager,
autofillEnabledManager = autofillEnabledManager,
lifecycleScope = lifecycleScope,
)

View file

@ -2,7 +2,7 @@ package com.x8bit.bitwarden.data.autofill.manager
import android.view.autofill.AutofillManager
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.onEach
@ -12,7 +12,7 @@ import kotlinx.coroutines.flow.onEach
class AutofillActivityManagerImpl(
private val autofillManager: AutofillManager,
private val autofillEnabledManager: AutofillEnabledManager,
appForegroundManager: AppForegroundManager,
appStateManager: AppStateManager,
lifecycleScope: LifecycleCoroutineScope,
) : AutofillActivityManager {
private val isAutofillEnabledAndSupported: Boolean
@ -21,7 +21,7 @@ class AutofillActivityManagerImpl(
autofillManager.isAutofillSupported
init {
appForegroundManager
appStateManager
.appForegroundStateFlow
.onEach { autofillEnabledManager.isAutofillEnabled = isAutofillEnabledAndSupported }
.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.service.EventService
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.AppForegroundManagerImpl
import com.x8bit.bitwarden.data.platform.manager.AppStateManager
import com.x8bit.bitwarden.data.platform.manager.AppStateManagerImpl
import com.x8bit.bitwarden.data.platform.manager.AssetManager
import com.x8bit.bitwarden.data.platform.manager.AssetManagerImpl
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
@ -80,8 +80,9 @@ object PlatformManagerModule {
@Provides
@Singleton
fun provideAppForegroundManager(): AppForegroundManager =
AppForegroundManagerImpl()
fun provideAppStateManager(
application: Application,
): AppStateManager = AppStateManagerImpl(application = application)
@Provides
@Singleton
@ -267,11 +268,11 @@ object PlatformManagerModule {
@Singleton
fun provideRestrictionManager(
@ApplicationContext context: Context,
appForegroundManager: AppForegroundManager,
appStateManager: AppStateManager,
dispatcherManager: DispatcherManager,
environmentRepository: EnvironmentRepository,
): RestrictionManager = RestrictionManagerImpl(
appForegroundManager = appForegroundManager,
appStateManager = appStateManager,
dispatcherManager = dispatcherManager,
context = context,
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.RestrictionsManager
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.model.AppForegroundState
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
@ -19,7 +19,7 @@ import kotlinx.coroutines.flow.onEach
* The default implementation of the [RestrictionManager].
*/
class RestrictionManagerImpl(
appForegroundManager: AppForegroundManager,
appStateManager: AppStateManager,
dispatcherManager: DispatcherManager,
private val context: Context,
private val environmentRepository: EnvironmentRepository,
@ -31,7 +31,7 @@ class RestrictionManagerImpl(
private var isReceiverRegistered = false
init {
appForegroundManager
appStateManager
.appForegroundStateFlow
.onEach {
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.userAccountTokens
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.model.AppCreationState
import com.x8bit.bitwarden.data.platform.manager.model.AppForegroundState
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeout
@ -65,7 +66,7 @@ class VaultLockManagerImpl(
private val authSdkSource: AuthSdkSource,
private val vaultSdkSource: VaultSdkSource,
private val settingsRepository: SettingsRepository,
private val appForegroundManager: AppForegroundManager,
private val appStateManager: AppStateManager,
private val userLogoutManager: UserLogoutManager,
private val trustedDeviceManager: TrustedDeviceManager,
dispatcherManager: DispatcherManager,
@ -90,6 +91,7 @@ class VaultLockManagerImpl(
get() = mutableVaultStateEventSharedFlow.asSharedFlow()
init {
observeAppCreationChanges()
observeAppForegroundChanges()
observeUserSwitchingChanges()
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() {
var isFirstForeground = true
appForegroundManager
appStateManager
.appForegroundStateFlow
.onEach { 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.manager.TrustedDeviceManager
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.repository.SettingsRepository
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
@ -72,7 +72,7 @@ object VaultManagerModule {
authSdkSource: AuthSdkSource,
vaultSdkSource: VaultSdkSource,
settingsRepository: SettingsRepository,
appForegroundManager: AppForegroundManager,
appStateManager: AppStateManager,
userLogoutManager: UserLogoutManager,
dispatcherManager: DispatcherManager,
trustedDeviceManager: TrustedDeviceManager,
@ -82,7 +82,7 @@ object VaultManagerModule {
authSdkSource = authSdkSource,
vaultSdkSource = vaultSdkSource,
settingsRepository = settingsRepository,
appForegroundManager = appForegroundManager,
appStateManager = appStateManager,
userLogoutManager = userLogoutManager,
dispatcherManager = dispatcherManager,
trustedDeviceManager = trustedDeviceManager,

View file

@ -4,7 +4,7 @@ import android.content.Context
import androidx.lifecycle.LifecycleCoroutineScope
import app.cash.turbine.test
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 io.mockk.every
import io.mockk.mockk
@ -26,7 +26,7 @@ class AccessibilityActivityManagerTest {
private val accessibilityEnabledManager: AccessibilityEnabledManager =
AccessibilityEnabledManagerImpl()
private val mutableAppForegroundStateFlow = MutableStateFlow(AppForegroundState.BACKGROUNDED)
private val appForegroundManager: AppForegroundManager = mockk {
private val appStateManager: AppStateManager = mockk {
every { appForegroundStateFlow } returns mutableAppForegroundStateFlow
}
private val lifecycleScope = mockk<LifecycleCoroutineScope> {
@ -43,7 +43,7 @@ class AccessibilityActivityManagerTest {
autofillActivityManager = AccessibilityActivityManagerImpl(
context = context,
accessibilityEnabledManager = accessibilityEnabledManager,
appForegroundManager = appForegroundManager,
appStateManager = appStateManager,
lifecycleScope = lifecycleScope,
)
}

View file

@ -3,7 +3,7 @@ package com.x8bit.bitwarden.data.autofill.manager
import android.view.autofill.AutofillManager
import androidx.lifecycle.LifecycleCoroutineScope
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 io.mockk.every
import io.mockk.just
@ -28,7 +28,7 @@ class AutofillActivityManagerTest {
private val autofillEnabledManager: AutofillEnabledManager = AutofillEnabledManagerImpl()
private val mutableAppForegroundStateFlow = MutableStateFlow(AppForegroundState.BACKGROUNDED)
private val appForegroundManager: AppForegroundManager = mockk {
private val appStateManager: AppStateManager = mockk {
every { appForegroundStateFlow } returns mutableAppForegroundStateFlow
}
private val lifecycleScope = mockk<LifecycleCoroutineScope> {
@ -39,7 +39,7 @@ class AutofillActivityManagerTest {
@Suppress("unused")
private val autofillActivityManager: AutofillActivityManager = AutofillActivityManagerImpl(
autofillManager = autofillManager,
appForegroundManager = appForegroundManager,
appStateManager = appStateManager,
autofillEnabledManager = autofillEnabledManager,
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.platform.base.FakeDispatcherManager
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.util.FakeEnvironmentRepository
import io.mockk.clearMocks
@ -27,7 +27,7 @@ class RestrictionManagerTest {
every { registerReceiver(any(), any()) } returns null
every { unregisterReceiver(any()) } just runs
}
private val fakeAppForegroundManager = FakeAppForegroundManager()
private val fakeAppStateManager = FakeAppStateManager()
private val fakeDispatcherManager = FakeDispatcherManager().apply {
setMain(unconfined)
}
@ -35,7 +35,7 @@ class RestrictionManagerTest {
private val restrictionsManager = mockk<RestrictionsManager>()
private val restrictionManager: RestrictionManager = RestrictionManagerImpl(
appForegroundManager = fakeAppForegroundManager,
appStateManager = fakeAppStateManager,
dispatcherManager = fakeDispatcherManager,
context = context,
environmentRepository = fakeEnvironmentRepository,
@ -51,7 +51,7 @@ class RestrictionManagerTest {
fun `on app foreground with a null bundle should register receiver and do nothing else`() {
every { restrictionsManager.applicationRestrictions } returns null
fakeAppForegroundManager.appForegroundState = AppForegroundState.FOREGROUNDED
fakeAppStateManager.appForegroundState = AppForegroundState.FOREGROUNDED
verify(exactly = 1) {
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`() {
every { restrictionsManager.applicationRestrictions } returns mockBundle()
fakeAppForegroundManager.appForegroundState = AppForegroundState.FOREGROUNDED
fakeAppStateManager.appForegroundState = AppForegroundState.FOREGROUNDED
verify(exactly = 1) {
context.registerReceiver(any(), any())
@ -78,7 +78,7 @@ class RestrictionManagerTest {
restrictionsManager.applicationRestrictions
} returns mockBundle("key" to "unknown")
fakeAppForegroundManager.appForegroundState = AppForegroundState.FOREGROUNDED
fakeAppStateManager.appForegroundState = AppForegroundState.FOREGROUNDED
verify(exactly = 1) {
context.registerReceiver(any(), any())
@ -93,7 +93,7 @@ class RestrictionManagerTest {
restrictionsManager.applicationRestrictions
} returns mockBundle("baseEnvironmentUrl" to "https://vault.bitwarden.com")
fakeAppForegroundManager.appForegroundState = AppForegroundState.FOREGROUNDED
fakeAppStateManager.appForegroundState = AppForegroundState.FOREGROUNDED
verify(exactly = 1) {
context.registerReceiver(any(), any())
@ -109,7 +109,7 @@ class RestrictionManagerTest {
restrictionsManager.applicationRestrictions
} returns mockBundle("baseEnvironmentUrl" to baseUrl)
fakeAppForegroundManager.appForegroundState = AppForegroundState.FOREGROUNDED
fakeAppStateManager.appForegroundState = AppForegroundState.FOREGROUNDED
verify(exactly = 1) {
context.registerReceiver(any(), any())
@ -130,7 +130,7 @@ class RestrictionManagerTest {
restrictionsManager.applicationRestrictions
} returns mockBundle("baseEnvironmentUrl" to "https://vault.bitwarden.eu")
fakeAppForegroundManager.appForegroundState = AppForegroundState.FOREGROUNDED
fakeAppStateManager.appForegroundState = AppForegroundState.FOREGROUNDED
verify(exactly = 1) {
context.registerReceiver(any(), any())
@ -147,7 +147,7 @@ class RestrictionManagerTest {
restrictionsManager.applicationRestrictions
} returns mockBundle("baseEnvironmentUrl" to baseUrl)
fakeAppForegroundManager.appForegroundState = AppForegroundState.FOREGROUNDED
fakeAppStateManager.appForegroundState = AppForegroundState.FOREGROUNDED
verify(exactly = 1) {
context.registerReceiver(any(), any())
@ -172,7 +172,7 @@ class RestrictionManagerTest {
restrictionsManager.applicationRestrictions
} returns mockBundle("baseEnvironmentUrl" to baseUrl)
fakeAppForegroundManager.appForegroundState = AppForegroundState.FOREGROUNDED
fakeAppStateManager.appForegroundState = AppForegroundState.FOREGROUNDED
verify(exactly = 1) {
context.registerReceiver(any(), any())
@ -192,7 +192,7 @@ class RestrictionManagerTest {
restrictionsManager.applicationRestrictions
} returns mockBundle("baseEnvironmentUrl" to baseUrl)
fakeAppForegroundManager.appForegroundState = AppForegroundState.FOREGROUNDED
fakeAppStateManager.appForegroundState = AppForegroundState.FOREGROUNDED
verify(exactly = 1) {
context.registerReceiver(any(), any())
@ -207,7 +207,7 @@ class RestrictionManagerTest {
@Test
fun `on app background when not foregrounded should do nothing`() {
fakeAppForegroundManager.appForegroundState = AppForegroundState.BACKGROUNDED
fakeAppStateManager.appForegroundState = AppForegroundState.BACKGROUNDED
verify(exactly = 0) {
context.unregisterReceiver(any())
@ -218,10 +218,10 @@ class RestrictionManagerTest {
@Test
fun `on app background after foreground should unregister receiver`() {
every { restrictionsManager.applicationRestrictions } returns null
fakeAppForegroundManager.appForegroundState = AppForegroundState.FOREGROUNDED
fakeAppStateManager.appForegroundState = AppForegroundState.FOREGROUNDED
clearMocks(context, restrictionsManager, answers = false)
fakeAppForegroundManager.appForegroundState = AppForegroundState.BACKGROUNDED
fakeAppStateManager.appForegroundState = AppForegroundState.BACKGROUNDED
verify(exactly = 1) {
context.unregisterReceiver(any())

View file

@ -1,20 +1,34 @@
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 kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
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)
override val appCreatedStateFlow: StateFlow<AppCreationState>
get() = mutableAppCreationStateFlow.asStateFlow()
override val appForegroundStateFlow: StateFlow<AppForegroundState>
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].
*/

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.repository.util.toSdkParams
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.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.model.VaultTimeout
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction
@ -53,7 +54,7 @@ import org.junit.jupiter.api.Test
@Suppress("LargeClass")
class VaultLockManagerTest {
private val fakeAuthDiskSource = FakeAuthDiskSource()
private val fakeAppForegroundManager = FakeAppForegroundManager()
private val fakeAppStateManager = FakeAppStateManager()
private val authSdkSource: AuthSdkSource = mockk {
coEvery {
hashPassword(
@ -90,7 +91,7 @@ class VaultLockManagerTest {
authSdkSource = authSdkSource,
vaultSdkSource = vaultSdkSource,
settingsRepository = settingsRepository,
appForegroundManager = fakeAppForegroundManager,
appStateManager = fakeAppStateManager,
userLogoutManager = userLogoutManager,
trustedDeviceManager = trustedDeviceManager,
dispatcherManager = fakeDispatcherManager,
@ -147,18 +148,86 @@ class VaultLockManagerTest {
}
@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()
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.
fun resetTest(vaultTimeout: VaultTimeout) {
clearVerifications(userLogoutManager)
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)
assertTrue(vaultLockManager.isVaultUnlocked(USER_ID))
}
@ -168,7 +237,7 @@ class VaultLockManagerTest {
MOCK_TIMEOUTS.forEach { 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.
testDispatcher.scheduler.advanceTimeBy(delayTimeMillis = 6 * 60 * 1000L)
@ -201,7 +270,7 @@ class VaultLockManagerTest {
mutableVaultTimeoutActionStateFlow.value = VaultTimeoutAction.LOGOUT
MOCK_TIMEOUTS.forEach { 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.
testDispatcher.scheduler.advanceTimeBy(delayTimeMillis = 6 * 60 * 1000L)
@ -236,11 +305,11 @@ class VaultLockManagerTest {
mutableVaultTimeoutActionStateFlow.value = VaultTimeoutAction.LOCK
mutableVaultTimeoutStateFlow.value = VaultTimeout.Never
fakeAppForegroundManager.appForegroundState = AppForegroundState.BACKGROUNDED
fakeAppStateManager.appForegroundState = AppForegroundState.BACKGROUNDED
verifyUnlockedVaultBlocking(userId = USER_ID)
assertTrue(vaultLockManager.isVaultUnlocked(USER_ID))
fakeAppForegroundManager.appForegroundState = AppForegroundState.FOREGROUNDED
fakeAppStateManager.appForegroundState = AppForegroundState.FOREGROUNDED
assertTrue(vaultLockManager.isVaultUnlocked(USER_ID))
}
@ -253,11 +322,11 @@ class VaultLockManagerTest {
mutableVaultTimeoutActionStateFlow.value = VaultTimeoutAction.LOCK
mutableVaultTimeoutStateFlow.value = VaultTimeout.OnAppRestart
fakeAppForegroundManager.appForegroundState = AppForegroundState.BACKGROUNDED
fakeAppStateManager.appForegroundState = AppForegroundState.BACKGROUNDED
verifyUnlockedVaultBlocking(userId = USER_ID)
assertTrue(vaultLockManager.isVaultUnlocked(USER_ID))
fakeAppForegroundManager.appForegroundState = AppForegroundState.FOREGROUNDED
fakeAppStateManager.appForegroundState = AppForegroundState.FOREGROUNDED
assertFalse(vaultLockManager.isVaultUnlocked(USER_ID))
}
@ -269,11 +338,11 @@ class VaultLockManagerTest {
mutableVaultTimeoutActionStateFlow.value = VaultTimeoutAction.LOCK
mutableVaultTimeoutStateFlow.value = VaultTimeout.ThirtyMinutes
fakeAppForegroundManager.appForegroundState = AppForegroundState.BACKGROUNDED
fakeAppStateManager.appForegroundState = AppForegroundState.BACKGROUNDED
verifyUnlockedVaultBlocking(userId = USER_ID)
assertTrue(vaultLockManager.isVaultUnlocked(USER_ID))
fakeAppForegroundManager.appForegroundState = AppForegroundState.FOREGROUNDED
fakeAppStateManager.appForegroundState = AppForegroundState.FOREGROUNDED
assertFalse(vaultLockManager.isVaultUnlocked(USER_ID))
}
@ -286,7 +355,7 @@ class VaultLockManagerTest {
mutableVaultTimeoutActionStateFlow.value = VaultTimeoutAction.LOCK
mutableVaultTimeoutStateFlow.value = VaultTimeout.ThirtyMinutes
fakeAppForegroundManager.appForegroundState = AppForegroundState.FOREGROUNDED
fakeAppStateManager.appForegroundState = AppForegroundState.FOREGROUNDED
assertFalse(vaultLockManager.isVaultUnlocked(USER_ID))
}
@ -298,11 +367,11 @@ class VaultLockManagerTest {
mutableVaultTimeoutActionStateFlow.value = VaultTimeoutAction.LOGOUT
mutableVaultTimeoutStateFlow.value = VaultTimeout.ThirtyMinutes
fakeAppForegroundManager.appForegroundState = AppForegroundState.BACKGROUNDED
fakeAppStateManager.appForegroundState = AppForegroundState.BACKGROUNDED
verifyUnlockedVaultBlocking(userId = USER_ID)
assertTrue(vaultLockManager.isVaultUnlocked(USER_ID))
fakeAppForegroundManager.appForegroundState = AppForegroundState.FOREGROUNDED
fakeAppStateManager.appForegroundState = AppForegroundState.FOREGROUNDED
verify(exactly = 1) { settingsRepository.getVaultTimeoutActionStateFlow(USER_ID) }
}
@ -314,11 +383,11 @@ class VaultLockManagerTest {
mutableVaultTimeoutActionStateFlow.value = VaultTimeoutAction.LOGOUT
mutableVaultTimeoutStateFlow.value = VaultTimeout.ThirtyMinutes
fakeAppForegroundManager.appForegroundState = AppForegroundState.BACKGROUNDED
fakeAppStateManager.appForegroundState = AppForegroundState.BACKGROUNDED
verifyUnlockedVaultBlocking(userId = USER_ID)
assertTrue(vaultLockManager.isVaultUnlocked(USER_ID))
fakeAppForegroundManager.appForegroundState = AppForegroundState.FOREGROUNDED
fakeAppStateManager.appForegroundState = AppForegroundState.FOREGROUNDED
verify(exactly = 0) { settingsRepository.getVaultTimeoutActionStateFlow(USER_ID) }
}
@ -329,14 +398,14 @@ class VaultLockManagerTest {
fakeAuthDiskSource.userState = MOCK_USER_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
fakeAppForegroundManager.appForegroundState = AppForegroundState.FOREGROUNDED
fakeAppStateManager.appForegroundState = AppForegroundState.FOREGROUNDED
// Will be used within each loop to reset the test to a suitable initial state.
fun resetTest(vaultTimeout: VaultTimeout) {
mutableVaultTimeoutStateFlow.value = vaultTimeout
fakeAppForegroundManager.appForegroundState = AppForegroundState.BACKGROUNDED
fakeAppStateManager.appForegroundState = AppForegroundState.BACKGROUNDED
clearVerifications(userLogoutManager)
verifyUnlockedVaultBlocking(userId = USER_ID)
assertTrue(vaultLockManager.isVaultUnlocked(USER_ID))
@ -347,7 +416,7 @@ class VaultLockManagerTest {
MOCK_TIMEOUTS.forEach { 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.
testDispatcher.scheduler.advanceTimeBy(delayTimeMillis = 6 * 60 * 1000L)
@ -361,7 +430,7 @@ class VaultLockManagerTest {
MOCK_TIMEOUTS.forEach { 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.
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`() {
val userId2 = "mockId-2"
setAccountTokens(listOf(USER_ID, userId2))
fakeAppForegroundManager.appForegroundState = AppForegroundState.FOREGROUNDED
fakeAppStateManager.appForegroundState = AppForegroundState.FOREGROUNDED
fakeAuthDiskSource.userState = UserStateJson(
activeUserId = USER_ID,
accounts = mapOf(

View file

@ -46,7 +46,7 @@ Note that these data sources are constructed in a manner that adheres to a very
### 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