SysUiViewModel.hydratedStateOf

Adds hydratedStateOf - a utility to help developers define a "hydrated"
(kept up-to-date) snapshot state (AKA compose State) property with a
single line of code and without needing to add a collector in the
activation code.

Bug: 354269846
Test: unit tests added
Test: manually verified with the next CL
Flag: NONE unused utility (in this CL)
Change-Id: Ibe0ccc623253d6df76cdef5c0842dda381b5a48a
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index 3aa89ee..c1bb55c 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -893,6 +893,7 @@
     ],
     static_libs: [
         "RoboTestLibraries",
+        "androidx.compose.runtime_runtime",
     ],
     libs: [
         "android.test.runner",
@@ -929,6 +930,7 @@
     ],
     static_libs: [
         "RoboTestLibraries",
+        "androidx.compose.runtime_runtime",
     ],
     libs: [
         "android.test.runner",
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/SysUiViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/SysUiViewModelTest.kt
index 976dc52..7d57220 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/SysUiViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/SysUiViewModelTest.kt
@@ -17,8 +17,14 @@
 package com.android.systemui.lifecycle
 
 import android.view.View
+import androidx.compose.foundation.layout.Column
+import androidx.compose.material3.Text
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertTextEquals
+import androidx.compose.ui.test.hasTestTag
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
@@ -26,6 +32,8 @@
 import com.android.systemui.util.Assert
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
@@ -149,6 +157,48 @@
 
         assertThat(viewModel.isActivated).isTrue()
     }
+
+    @Test
+    fun hydratedStateOf() {
+        val keepAliveMutable = mutableStateOf(true)
+        val upstreamStateFlow = MutableStateFlow(true)
+        val upstreamFlow = upstreamStateFlow.map { !it }
+        composeRule.setContent {
+            val keepAlive by keepAliveMutable
+            if (keepAlive) {
+                val viewModel = rememberViewModel {
+                    FakeSysUiViewModel(
+                        upstreamFlow = upstreamFlow,
+                        upstreamStateFlow = upstreamStateFlow,
+                    )
+                }
+
+                Column {
+                    Text(
+                        "upstreamStateFlow=${viewModel.stateBackedByStateFlow}",
+                        Modifier.testTag("upstreamStateFlow")
+                    )
+                    Text(
+                        "upstreamFlow=${viewModel.stateBackedByFlow}",
+                        Modifier.testTag("upstreamFlow")
+                    )
+                }
+            }
+        }
+
+        composeRule.waitForIdle()
+        composeRule
+            .onNode(hasTestTag("upstreamStateFlow"))
+            .assertTextEquals("upstreamStateFlow=true")
+        composeRule.onNode(hasTestTag("upstreamFlow")).assertTextEquals("upstreamFlow=false")
+
+        composeRule.runOnUiThread { upstreamStateFlow.value = false }
+        composeRule.waitForIdle()
+        composeRule
+            .onNode(hasTestTag("upstreamStateFlow"))
+            .assertTextEquals("upstreamStateFlow=false")
+        composeRule.onNode(hasTestTag("upstreamFlow")).assertTextEquals("upstreamFlow=true")
+    }
 }
 
 private class FakeViewModel : SysUiViewModel() {
diff --git a/packages/SystemUI/src/com/android/systemui/lifecycle/SysUiViewModel.kt b/packages/SystemUI/src/com/android/systemui/lifecycle/SysUiViewModel.kt
index 104b076..32c4760 100644
--- a/packages/SystemUI/src/com/android/systemui/lifecycle/SysUiViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/lifecycle/SysUiViewModel.kt
@@ -18,13 +18,45 @@
 
 import android.view.View
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.snapshots.StateFactoryMarker
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.launch
 
 /** Base class for all System UI view-models. */
 abstract class SysUiViewModel : BaseActivatable() {
 
+    @StateFactoryMarker
+    fun <T> hydratedStateOf(
+        source: StateFlow<T>,
+    ): State<T> {
+        return hydratedStateOf(
+            initialValue = source.value,
+            source = source,
+        )
+    }
+
+    @StateFactoryMarker
+    fun <T> hydratedStateOf(
+        initialValue: T,
+        source: Flow<T>,
+    ): State<T> {
+        val mutableState = mutableStateOf(initialValue)
+        addChild(
+            object : BaseActivatable() {
+                override suspend fun onActivated(): Nothing {
+                    source.collect { mutableState.value = it }
+                    awaitCancellation()
+                }
+            }
+        )
+        return mutableState
+    }
+
     override suspend fun onActivated(): Nothing {
         awaitCancellation()
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/lifecycle/FakeSysUiViewModel.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/lifecycle/FakeSysUiViewModel.kt
index c0bb9a6..90cd8c7 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/lifecycle/FakeSysUiViewModel.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/lifecycle/FakeSysUiViewModel.kt
@@ -16,15 +16,27 @@
 
 package com.android.systemui.lifecycle
 
+import androidx.compose.runtime.getValue
 import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.flowOf
 
 class FakeSysUiViewModel(
     private val onActivation: () -> Unit = {},
     private val onDeactivation: () -> Unit = {},
+    private val upstreamFlow: Flow<Boolean> = flowOf(true),
+    private val upstreamStateFlow: StateFlow<Boolean> = MutableStateFlow(true).asStateFlow(),
 ) : SysUiViewModel() {
+
     var activationCount = 0
     var cancellationCount = 0
 
+    val stateBackedByFlow: Boolean by hydratedStateOf(initialValue = true, source = upstreamFlow)
+    val stateBackedByStateFlow: Boolean by hydratedStateOf(source = upstreamStateFlow)
+
     override suspend fun onActivated(): Nothing {
         activationCount++
         onActivation()