From 3d75867a15de7c0c606664beda52604b24ae0de5 Mon Sep 17 00:00:00 2001 From: Brian Yencho <brian@livefront.com> Date: Sat, 13 Jan 2024 11:15:06 -0600 Subject: [PATCH] Add AppForegroundManager (#599) --- README.md | 5 ++ app/build.gradle.kts | 1 + .../platform/manager/AppForegroundManager.kt | 15 ++++ .../manager/AppForegroundManagerImpl.kt | 36 ++++++++ .../manager/di/PlatformManagerModule.kt | 7 ++ .../manager/model/AppForegroundState.kt | 16 ++++ .../manager/AppForegroundManagerTest.kt | 87 +++++++++++++++++++ gradle/libs.versions.toml | 1 + 8 files changed, 168 insertions(+) create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/platform/manager/AppForegroundManager.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/platform/manager/AppForegroundManagerImpl.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/AppForegroundState.kt create mode 100644 app/src/test/java/com/x8bit/bitwarden/data/platform/manager/AppForegroundManagerTest.kt diff --git a/README.md b/README.md index efce1d86d..bd3cbf61a 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,11 @@ The following is a list of all third-party dependencies included as part of the - Purpose: Display and capture images for barcode scanning. - License: Apache 2.0 +- **AndroidX Lifecycle** + - https://developer.android.com/jetpack/androidx/releases/lifecycle + - Purpose: Lifecycle aware components and tooling. + - License: Apache 2.0 + - **AndroidX Security** - https://developer.android.com/jetpack/androidx/releases/security - Purpose: Safely manage keys and encrypt files and sharedpreferences. diff --git a/app/build.gradle.kts b/app/build.gradle.kts index becd19745..ff3414f50 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -162,6 +162,7 @@ dependencies { implementation(libs.androidx.compose.ui.tooling.preview) implementation(libs.androidx.core.ktx) implementation(libs.androidx.hilt.navigation.compose) + implementation(libs.androidx.lifecycle.process) implementation(libs.androidx.lifecycle.runtime.compose) implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.navigation.compose) diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/AppForegroundManager.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/AppForegroundManager.kt new file mode 100644 index 000000000..268842c5f --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/AppForegroundManager.kt @@ -0,0 +1,15 @@ +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> +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/AppForegroundManagerImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/AppForegroundManagerImpl.kt new file mode 100644 index 000000000..3f0a51075 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/AppForegroundManagerImpl.kt @@ -0,0 +1,36 @@ +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 + } + }, + ) + } +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/di/PlatformManagerModule.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/di/PlatformManagerModule.kt index 0ae08134a..aa885dc58 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/di/PlatformManagerModule.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/di/PlatformManagerModule.kt @@ -5,6 +5,8 @@ import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.platform.datasource.network.authenticator.RefreshAuthenticator import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.AuthTokenInterceptor import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.BaseUrlInterceptors +import com.x8bit.bitwarden.data.platform.manager.AppForegroundManager +import com.x8bit.bitwarden.data.platform.manager.AppForegroundManagerImpl import com.x8bit.bitwarden.data.platform.manager.NetworkConfigManager import com.x8bit.bitwarden.data.platform.manager.NetworkConfigManagerImpl import com.x8bit.bitwarden.data.platform.manager.SdkClientManager @@ -29,6 +31,11 @@ import javax.inject.Singleton @InstallIn(SingletonComponent::class) object PlatformManagerModule { + @Provides + @Singleton + fun provideAppForegroundManager(): AppForegroundManager = + AppForegroundManagerImpl() + @Provides @Singleton fun provideClock(): Clock = Clock.systemDefaultZone() diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/AppForegroundState.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/AppForegroundState.kt new file mode 100644 index 000000000..166f9dd3a --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/AppForegroundState.kt @@ -0,0 +1,16 @@ +package com.x8bit.bitwarden.data.platform.manager.model + +/** + * Represents the foreground state of the app. + */ +enum class AppForegroundState { + /** + * Denotes that the app is backgrounded. + */ + BACKGROUNDED, + + /** + * Denotes that the app is foregrounded. + */ + FOREGROUNDED, +} diff --git a/app/src/test/java/com/x8bit/bitwarden/data/platform/manager/AppForegroundManagerTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/platform/manager/AppForegroundManagerTest.kt new file mode 100644 index 000000000..82337a19b --- /dev/null +++ b/app/src/test/java/com/x8bit/bitwarden/data/platform/manager/AppForegroundManagerTest.kt @@ -0,0 +1,87 @@ +package com.x8bit.bitwarden.data.platform.manager + +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleObserver +import androidx.lifecycle.LifecycleOwner +import app.cash.turbine.test +import com.x8bit.bitwarden.data.platform.manager.model.AppForegroundState +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(), + ) + } + } +} + +private class FakeLifecycle( + private val lifecycleOwner: LifecycleOwner, +) : Lifecycle() { + private val observers = mutableSetOf<DefaultLifecycleObserver>() + + override var currentState: State = State.INITIALIZED + + override fun addObserver(observer: LifecycleObserver) { + observers += (observer as DefaultLifecycleObserver) + } + + override fun removeObserver(observer: LifecycleObserver) { + observers -= (observer as DefaultLifecycleObserver) + } + + /** + * Triggers [DefaultLifecycleObserver.onStart] calls for each registered observer. + */ + fun dispatchOnStart() { + currentState = State.STARTED + observers.forEach { observer -> + observer.onStart(lifecycleOwner) + } + } + + /** + * Triggers [DefaultLifecycleObserver.onStop] calls for each registered observer. + */ + fun dispatchOnStop() { + currentState = State.CREATED + observers.forEach { observer -> + observer.onStop(lifecycleOwner) + } + } +} + +private class FakeLifecycleOwner : LifecycleOwner { + override val lifecycle: FakeLifecycle = FakeLifecycle(this) +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d7bf470cd..83b8405c7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -71,6 +71,7 @@ androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" } androidx-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" } androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidxCore" } androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "androidxHiltNavigationCompose" } +androidx-lifecycle-process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "androidxLifecycle" } androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidxLifecycle" } androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "androidxLifecycle" } androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "androidxNavigation" }