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" }