[flexiglass] Structured concurrency fixes

- Breaks up BaseActivatable to Hydrator and ExclusiveActivatable
- Hydrator managed a set of static children
- SysUiViewModel no longer implements Activatable
- All converted view-models properly migrated to implement SysUiViModel
  and extend ExclusiveActivatable if they need to be Activatable
- The existing usage of hydratedStateOf migrated to use a Hydrator
- rememberViewModel and viewModel view-model factory functons changes to
  only activate view-models that are activatable

Bug: 354270224
Test: unit tests refactored and moved around to follow the new class
split
Test: manually verified no harm done with a Flexiglass smoke test: shade
and QS over lockscreen, bouncer, unlock, shade and QS over unlocked
device, relock
Flag: com.android.systemui.scene_container

Change-Id: I138795d49271923c14d677a64b1488c72d816282
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/BaseActivatableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/BaseActivatableTest.kt
deleted file mode 100644
index f6f58c9..0000000
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/BaseActivatableTest.kt
+++ /dev/null
@@ -1,328 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-@file:OptIn(ExperimentalCoroutinesApi::class)
-
-package com.android.systemui.lifecycle
-
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.kosmos.testScope
-import com.android.systemui.testKosmos
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.test.runCurrent
-import kotlinx.coroutines.test.runTest
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-class BaseActivatableTest : SysuiTestCase() {
-
-    private val kosmos = testKosmos()
-    private val testScope = kosmos.testScope
-
-    private val underTest = FakeActivatable()
-
-    @Test
-    fun activate() =
-        testScope.runTest {
-            assertThat(underTest.isActive).isFalse()
-            assertThat(underTest.activationCount).isEqualTo(0)
-            assertThat(underTest.cancellationCount).isEqualTo(0)
-
-            underTest.activateIn(testScope)
-            runCurrent()
-            assertThat(underTest.isActive).isTrue()
-            assertThat(underTest.activationCount).isEqualTo(1)
-            assertThat(underTest.cancellationCount).isEqualTo(0)
-        }
-
-    @Test
-    fun activate_andCancel() =
-        testScope.runTest {
-            assertThat(underTest.isActive).isFalse()
-            assertThat(underTest.activationCount).isEqualTo(0)
-            assertThat(underTest.cancellationCount).isEqualTo(0)
-
-            val job = Job()
-            underTest.activateIn(testScope, context = job)
-            runCurrent()
-            assertThat(underTest.isActive).isTrue()
-            assertThat(underTest.activationCount).isEqualTo(1)
-            assertThat(underTest.cancellationCount).isEqualTo(0)
-
-            job.cancel()
-            runCurrent()
-            assertThat(underTest.isActive).isFalse()
-            assertThat(underTest.activationCount).isEqualTo(1)
-            assertThat(underTest.cancellationCount).isEqualTo(1)
-        }
-
-    @Test
-    fun activate_afterCancellation() =
-        testScope.runTest {
-            assertThat(underTest.isActive).isFalse()
-            assertThat(underTest.activationCount).isEqualTo(0)
-            assertThat(underTest.cancellationCount).isEqualTo(0)
-
-            val job = Job()
-            underTest.activateIn(testScope, context = job)
-            runCurrent()
-            assertThat(underTest.isActive).isTrue()
-            assertThat(underTest.activationCount).isEqualTo(1)
-            assertThat(underTest.cancellationCount).isEqualTo(0)
-
-            job.cancel()
-            runCurrent()
-            assertThat(underTest.isActive).isFalse()
-            assertThat(underTest.activationCount).isEqualTo(1)
-            assertThat(underTest.cancellationCount).isEqualTo(1)
-
-            underTest.activateIn(testScope)
-            runCurrent()
-            assertThat(underTest.isActive).isTrue()
-            assertThat(underTest.activationCount).isEqualTo(2)
-            assertThat(underTest.cancellationCount).isEqualTo(1)
-        }
-
-    @Test(expected = IllegalStateException::class)
-    fun activate_whileActive_throws() =
-        testScope.runTest {
-            assertThat(underTest.isActive).isFalse()
-            assertThat(underTest.activationCount).isEqualTo(0)
-            assertThat(underTest.cancellationCount).isEqualTo(0)
-
-            underTest.activateIn(testScope)
-            runCurrent()
-            assertThat(underTest.isActive).isTrue()
-            assertThat(underTest.activationCount).isEqualTo(1)
-            assertThat(underTest.cancellationCount).isEqualTo(0)
-
-            underTest.activateIn(testScope)
-            runCurrent()
-        }
-
-    @Test
-    fun addChild_beforeActive_activatesChildrenOnceActivated() =
-        testScope.runTest {
-            val child1 = FakeActivatable()
-            val child2 = FakeActivatable()
-            assertThat(child1.isActive).isFalse()
-            assertThat(child2.isActive).isFalse()
-
-            assertThat(underTest.isActive).isFalse()
-            underTest.addChild(child1)
-            underTest.addChild(child2)
-            assertThat(underTest.isActive).isFalse()
-            assertThat(child1.isActive).isFalse()
-            assertThat(child2.isActive).isFalse()
-
-            underTest.activateIn(this)
-            runCurrent()
-
-            assertThat(underTest.isActive).isTrue()
-            assertThat(child1.isActive).isTrue()
-            assertThat(child2.isActive).isTrue()
-        }
-
-    @Test
-    fun addChild_whileActive_activatesChildrenImmediately() =
-        testScope.runTest {
-            underTest.activateIn(this)
-            runCurrent()
-            assertThat(underTest.isActive).isTrue()
-
-            val child1 = FakeActivatable()
-            val child2 = FakeActivatable()
-            assertThat(child1.isActive).isFalse()
-            assertThat(child2.isActive).isFalse()
-
-            underTest.addChild(child1)
-            underTest.addChild(child2)
-            runCurrent()
-
-            assertThat(child1.isActive).isTrue()
-            assertThat(child2.isActive).isTrue()
-        }
-
-    @Test
-    fun addChild_afterCancellation_doesNotActivateChildren() =
-        testScope.runTest {
-            val job = Job()
-            underTest.activateIn(this, context = job)
-            runCurrent()
-            assertThat(underTest.isActive).isTrue()
-            job.cancel()
-            runCurrent()
-            assertThat(underTest.isActive).isFalse()
-
-            val child1 = FakeActivatable()
-            val child2 = FakeActivatable()
-            assertThat(child1.isActive).isFalse()
-            assertThat(child2.isActive).isFalse()
-
-            underTest.addChild(child1)
-            underTest.addChild(child2)
-            runCurrent()
-
-            assertThat(child1.isActive).isFalse()
-            assertThat(child2.isActive).isFalse()
-        }
-
-    @Test
-    fun activate_cancellation_cancelsCurrentChildren() =
-        testScope.runTest {
-            val job = Job()
-            underTest.activateIn(this, context = job)
-            runCurrent()
-            assertThat(underTest.isActive).isTrue()
-
-            val child1 = FakeActivatable()
-            val child2 = FakeActivatable()
-            assertThat(child1.isActive).isFalse()
-            assertThat(child2.isActive).isFalse()
-
-            underTest.addChild(child1)
-            underTest.addChild(child2)
-            runCurrent()
-
-            assertThat(child1.isActive).isTrue()
-            assertThat(child2.isActive).isTrue()
-
-            job.cancel()
-            runCurrent()
-            assertThat(underTest.isActive).isFalse()
-            assertThat(child1.isActive).isFalse()
-            assertThat(child2.isActive).isFalse()
-        }
-
-    @Test
-    fun activate_afterCancellation_reactivatesCurrentChildren() =
-        testScope.runTest {
-            val job = Job()
-            underTest.activateIn(this, context = job)
-            runCurrent()
-            assertThat(underTest.isActive).isTrue()
-
-            val child1 = FakeActivatable()
-            val child2 = FakeActivatable()
-            assertThat(child1.isActive).isFalse()
-            assertThat(child2.isActive).isFalse()
-
-            underTest.addChild(child1)
-            underTest.addChild(child2)
-            runCurrent()
-
-            assertThat(child1.isActive).isTrue()
-            assertThat(child2.isActive).isTrue()
-
-            job.cancel()
-            runCurrent()
-            assertThat(underTest.isActive).isFalse()
-            assertThat(child1.isActive).isFalse()
-            assertThat(child2.isActive).isFalse()
-
-            underTest.activateIn(this)
-            runCurrent()
-            assertThat(underTest.isActive).isTrue()
-            assertThat(child1.isActive).isTrue()
-            assertThat(child2.isActive).isTrue()
-        }
-
-    @Test
-    fun removeChild_beforeActive_neverActivatesChild() =
-        testScope.runTest {
-            val child1 = FakeActivatable()
-            val child2 = FakeActivatable()
-            assertThat(child1.isActive).isFalse()
-            assertThat(child2.isActive).isFalse()
-
-            assertThat(underTest.isActive).isFalse()
-            underTest.addChild(child1)
-            underTest.addChild(child2)
-            assertThat(underTest.isActive).isFalse()
-            assertThat(child1.isActive).isFalse()
-            assertThat(child2.isActive).isFalse()
-        }
-
-    @Test
-    fun removeChild_whileActive_cancelsChild() =
-        testScope.runTest {
-            val child1 = FakeActivatable()
-            val child2 = FakeActivatable()
-            assertThat(child1.isActive).isFalse()
-            assertThat(child2.isActive).isFalse()
-
-            assertThat(underTest.isActive).isFalse()
-            underTest.addChild(child1)
-            underTest.addChild(child2)
-            assertThat(underTest.isActive).isFalse()
-            assertThat(child1.isActive).isFalse()
-            assertThat(child2.isActive).isFalse()
-
-            underTest.activateIn(this)
-            runCurrent()
-            assertThat(underTest.isActive).isTrue()
-            assertThat(child1.isActive).isTrue()
-            assertThat(child2.isActive).isTrue()
-
-            underTest.removeChild(child1)
-            runCurrent()
-            assertThat(underTest.isActive).isTrue()
-            assertThat(child1.isActive).isFalse()
-            assertThat(child2.isActive).isTrue()
-        }
-
-    @Test
-    fun removeChild_afterCancellation_doesNotReactivateChildren() =
-        testScope.runTest {
-            val child1 = FakeActivatable()
-            val child2 = FakeActivatable()
-            assertThat(child1.isActive).isFalse()
-            assertThat(child2.isActive).isFalse()
-
-            assertThat(underTest.isActive).isFalse()
-            underTest.addChild(child1)
-            underTest.addChild(child2)
-            assertThat(underTest.isActive).isFalse()
-            assertThat(child1.isActive).isFalse()
-            assertThat(child2.isActive).isFalse()
-
-            val job = Job()
-            underTest.activateIn(this, context = job)
-            runCurrent()
-            assertThat(underTest.isActive).isTrue()
-            assertThat(child1.isActive).isTrue()
-            assertThat(child2.isActive).isTrue()
-
-            job.cancel()
-            runCurrent()
-            assertThat(underTest.isActive).isFalse()
-            assertThat(child1.isActive).isFalse()
-            assertThat(child2.isActive).isFalse()
-
-            underTest.removeChild(child1)
-            underTest.activateIn(this)
-            runCurrent()
-            assertThat(underTest.isActive).isTrue()
-            assertThat(child1.isActive).isFalse()
-            assertThat(child2.isActive).isTrue()
-        }
-}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/ExclusiveActivatableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/ExclusiveActivatableTest.kt
new file mode 100644
index 0000000..81b9180
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/ExclusiveActivatableTest.kt
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.lifecycle
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ExclusiveActivatableTest : SysuiTestCase() {
+
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+
+    private val underTest = FakeActivatable()
+
+    @Test
+    fun activate() =
+        testScope.runTest {
+            assertThat(underTest.activationCount).isEqualTo(0)
+            assertThat(underTest.cancellationCount).isEqualTo(0)
+
+            underTest.activateIn(testScope)
+            runCurrent()
+            assertThat(underTest.activationCount).isEqualTo(1)
+            assertThat(underTest.cancellationCount).isEqualTo(0)
+        }
+
+    @Test
+    fun activate_andCancel() =
+        testScope.runTest {
+            assertThat(underTest.activationCount).isEqualTo(0)
+            assertThat(underTest.cancellationCount).isEqualTo(0)
+
+            val job = Job()
+            underTest.activateIn(testScope, context = job)
+            runCurrent()
+            assertThat(underTest.activationCount).isEqualTo(1)
+            assertThat(underTest.cancellationCount).isEqualTo(0)
+
+            job.cancel()
+            runCurrent()
+            assertThat(underTest.activationCount).isEqualTo(1)
+            assertThat(underTest.cancellationCount).isEqualTo(1)
+        }
+
+    @Test
+    fun activate_afterCancellation() =
+        testScope.runTest {
+            assertThat(underTest.activationCount).isEqualTo(0)
+            assertThat(underTest.cancellationCount).isEqualTo(0)
+
+            val job = Job()
+            underTest.activateIn(testScope, context = job)
+            runCurrent()
+            assertThat(underTest.activationCount).isEqualTo(1)
+            assertThat(underTest.cancellationCount).isEqualTo(0)
+
+            job.cancel()
+            runCurrent()
+            assertThat(underTest.activationCount).isEqualTo(1)
+            assertThat(underTest.cancellationCount).isEqualTo(1)
+
+            underTest.activateIn(testScope)
+            runCurrent()
+            assertThat(underTest.activationCount).isEqualTo(2)
+            assertThat(underTest.cancellationCount).isEqualTo(1)
+        }
+
+    @Test(expected = IllegalStateException::class)
+    fun activate_whileActive_throws() =
+        testScope.runTest {
+            assertThat(underTest.activationCount).isEqualTo(0)
+            assertThat(underTest.cancellationCount).isEqualTo(0)
+
+            underTest.activateIn(testScope)
+            runCurrent()
+            assertThat(underTest.activationCount).isEqualTo(1)
+            assertThat(underTest.cancellationCount).isEqualTo(0)
+
+            underTest.activateIn(testScope)
+            runCurrent()
+        }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/HydratorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/HydratorTest.kt
new file mode 100644
index 0000000..8c9c527
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/HydratorTest.kt
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.lifecycle
+
+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
+import com.android.systemui.SysuiTestCase
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.map
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class HydratorTest : SysuiTestCase() {
+
+    @get:Rule val composeRule = createComposeRule()
+
+    @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")
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneContentViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneContentViewModelTest.kt
index 1118a61..e2149d9 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneContentViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneContentViewModelTest.kt
@@ -26,7 +26,6 @@
 import com.android.systemui.flags.Flags
 import com.android.systemui.flags.fakeFeatureFlagsClassic
 import com.android.systemui.kosmos.testScope
-import com.android.systemui.lifecycle.activateIn
 import com.android.systemui.media.controls.data.repository.mediaFilterRepository
 import com.android.systemui.media.controls.domain.pipeline.interactor.mediaCarouselInteractor
 import com.android.systemui.media.controls.shared.model.MediaData
@@ -82,7 +81,6 @@
                 footerActionsController = footerActionsController,
                 mediaCarouselInteractor = kosmos.mediaCarouselInteractor,
             )
-        underTest.activateIn(testScope)
     }
 
     @Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneActionsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneActionsViewModelTest.kt
index 206d3ac..dd4432d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneActionsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneActionsViewModelTest.kt
@@ -55,7 +55,6 @@
         testScope.runTest {
             val actions by collectLastValue(underTest.actions)
 
-            assertThat(underTest.isActive).isFalse()
             assertThat(actions).isEmpty()
         }
 
@@ -66,7 +65,6 @@
             underTest.activateIn(testScope)
             runCurrent()
 
-            assertThat(underTest.isActive).isTrue()
             assertThat(actions).isEmpty()
         }
 
@@ -76,7 +74,6 @@
             val actions by collectLastValue(underTest.actions)
             underTest.activateIn(testScope)
             runCurrent()
-            assertThat(underTest.isActive).isTrue()
 
             val expected1 =
                 mapOf(
@@ -116,7 +113,6 @@
 
             job.cancel()
             runCurrent()
-            assertThat(underTest.isActive).isFalse()
             assertThat(actions).isEmpty()
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt
index df50e8f..abca518 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt
@@ -21,6 +21,7 @@
 import com.android.systemui.authentication.domain.interactor.AuthenticationResult
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
+import com.android.systemui.lifecycle.ExclusiveActivatable
 import com.android.systemui.lifecycle.SysUiViewModel
 import kotlinx.coroutines.awaitCancellation
 import kotlinx.coroutines.channels.Channel
@@ -39,7 +40,7 @@
      * being able to attempt to unlock the device.
      */
     val isInputEnabled: StateFlow<Boolean>,
-) : SysUiViewModel() {
+) : SysUiViewModel, ExclusiveActivatable() {
 
     private val _animateFailure = MutableStateFlow(false)
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModel.kt
index cfd4f50..d21eccd 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModel.kt
@@ -38,6 +38,7 @@
 import com.android.systemui.deviceentry.shared.model.FaceTimeoutMessage
 import com.android.systemui.deviceentry.shared.model.FingerprintFailureMessage
 import com.android.systemui.deviceentry.shared.model.FingerprintLockoutMessage
+import com.android.systemui.lifecycle.ExclusiveActivatable
 import com.android.systemui.lifecycle.SysUiViewModel
 import com.android.systemui.res.R.string.kg_too_many_failed_attempts_countdown
 import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel
@@ -79,7 +80,7 @@
     private val deviceUnlockedInteractor: DeviceUnlockedInteractor,
     private val deviceEntryBiometricsAllowedInteractor: DeviceEntryBiometricsAllowedInteractor,
     private val flags: ComposeBouncerFlags,
-) : SysUiViewModel() {
+) : SysUiViewModel, ExclusiveActivatable() {
     /**
      * A message shown when the user has attempted the wrong credential too many times and now must
      * wait a while before attempting to authenticate again.
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneContentViewModel.kt
index 63b6f01..79e5f8d 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneContentViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneContentViewModel.kt
@@ -33,6 +33,7 @@
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.common.shared.model.Text
 import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.lifecycle.ExclusiveActivatable
 import com.android.systemui.lifecycle.SysUiViewModel
 import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel
 import dagger.assisted.AssistedFactory
@@ -62,7 +63,7 @@
     private val pinViewModelFactory: PinBouncerViewModel.Factory,
     private val patternViewModelFactory: PatternBouncerViewModel.Factory,
     private val passwordViewModelFactory: PasswordBouncerViewModel.Factory,
-) : SysUiViewModel() {
+) : SysUiViewModel, ExclusiveActivatable() {
     private val _selectedUserImage = MutableStateFlow<Bitmap?>(null)
     val selectedUserImage: StateFlow<Bitmap?> = _selectedUserImage.asStateFlow()
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt
index 3e6dd8e..2b6c3c0 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt
@@ -24,6 +24,7 @@
 import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
 import com.android.systemui.keyguard.shared.model.ClockSize
+import com.android.systemui.lifecycle.ExclusiveActivatable
 import com.android.systemui.lifecycle.SysUiViewModel
 import com.android.systemui.res.R
 import com.android.systemui.scene.domain.interactor.SceneContainerOcclusionInteractor
@@ -59,7 +60,7 @@
     private val unfoldTransitionInteractor: UnfoldTransitionInteractor,
     private val occlusionInteractor: SceneContainerOcclusionInteractor,
     private val deviceEntryInteractor: DeviceEntryInteractor,
-) : SysUiViewModel() {
+) : SysUiViewModel, ExclusiveActivatable() {
     @VisibleForTesting val clockSize = clockInteractor.clockSize
 
     val isUdfpsVisible: Boolean
diff --git a/packages/SystemUI/src/com/android/systemui/lifecycle/BaseActivatable.kt b/packages/SystemUI/src/com/android/systemui/lifecycle/BaseActivatable.kt
deleted file mode 100644
index 03476ec..0000000
--- a/packages/SystemUI/src/com/android/systemui/lifecycle/BaseActivatable.kt
+++ /dev/null
@@ -1,115 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.lifecycle
-
-import java.util.concurrent.atomic.AtomicBoolean
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.awaitCancellation
-import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.coroutineScope
-import kotlinx.coroutines.flow.receiveAsFlow
-import kotlinx.coroutines.launch
-
-/**
- * A base [Activatable] with the following characteristics:
- * 1. **Can be concurrently activated by no more than one owner.** A previous call to [activate]
- *    must be canceled before a new call to [activate] can be made. Trying to call [activate] while
- *    already active will fail with an error
- * 2. **Can manage child [Activatable]s**. See [addChild] and [removeChild]. Added children
- *    automatically track the activation state of the parent such that when the parent is active,
- *    the children are active and vice-versa. Children are also retained such that deactivating the
- *    parent and reactivating it also cancels and reactivates the children.
- */
-abstract class BaseActivatable : Activatable {
-
-    private val _isActive = AtomicBoolean(false)
-
-    var isActive: Boolean
-        get() = _isActive.get()
-        private set(value) {
-            _isActive.set(value)
-        }
-
-    final override suspend fun activate(): Nothing {
-        val allowed = _isActive.compareAndSet(false, true)
-        check(allowed) { "Cannot activate an already active activatable!" }
-
-        coroutineScope {
-            try {
-                launch { manageChildren() }
-                onActivated()
-            } finally {
-                isActive = false
-            }
-        }
-    }
-
-    /**
-     * Notifies that the [Activatable] has been activated.
-     *
-     * Serves as an entrypoint to kick off coroutine work that the object requires in order to keep
-     * its state fresh and/or perform side-effects.
-     *
-     * The method suspends and doesn't return until all work required by the object is finished. In
-     * most cases, it's expected for the work to remain ongoing forever so this method will forever
-     * suspend its caller until the coroutine that called it is canceled.
-     *
-     * Implementations could follow this pattern:
-     * ```kotlin
-     * override suspend fun onActivated(): Nothing {
-     *     coroutineScope {
-     *         launch { ... }
-     *         launch { ... }
-     *         launch { ... }
-     *     }
-     * }
-     * ```
-     *
-     * @see activate
-     */
-    protected abstract suspend fun onActivated(): Nothing
-
-    private val newChildren = Channel<Activatable>(Channel.BUFFERED)
-    private val jobByChild: MutableMap<Activatable, Job> by lazy { mutableMapOf() }
-
-    private suspend fun manageChildren(): Nothing {
-        coroutineScope {
-            // Reactivate children that were added during a previous activation:
-            jobByChild.keys.forEach { child -> jobByChild[child] = launch { child.activate() } }
-
-            // Process requests to add more children:
-            newChildren.receiveAsFlow().collect { newChild ->
-                removeChildInternal(newChild)
-                jobByChild[newChild] = launch { newChild.activate() }
-            }
-
-            awaitCancellation()
-        }
-    }
-
-    fun addChild(child: Activatable) {
-        newChildren.trySend(child)
-    }
-
-    fun removeChild(child: Activatable) {
-        removeChildInternal(child)
-    }
-
-    private fun removeChildInternal(child: Activatable) {
-        jobByChild.remove(child)?.cancel()
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/lifecycle/ExclusiveActivatable.kt b/packages/SystemUI/src/com/android/systemui/lifecycle/ExclusiveActivatable.kt
new file mode 100644
index 0000000..0837398
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/lifecycle/ExclusiveActivatable.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.lifecycle
+
+import java.util.concurrent.atomic.AtomicBoolean
+
+/**
+ * A base [Activatable] that can only be activated by a single owner (hence "exclusive"). A previous
+ * call to [activate] must be canceled before a new call to [activate] can be made. Trying to call
+ * [activate] while already active will result in a runtime error.
+ */
+abstract class ExclusiveActivatable : Activatable {
+
+    private val _isActive = AtomicBoolean(false)
+
+    protected var isActive: Boolean
+        get() = _isActive.get()
+        private set(value) {
+            _isActive.set(value)
+        }
+
+    final override suspend fun activate(): Nothing {
+        val allowed = _isActive.compareAndSet(false, true)
+        check(allowed) { "Cannot activate an already active ExclusiveActivatable!" }
+
+        try {
+            onActivated()
+        } finally {
+            isActive = false
+        }
+    }
+
+    /**
+     * Notifies that the [Activatable] has been activated.
+     *
+     * Serves as an entrypoint to kick off coroutine work that the object requires in order to keep
+     * its state fresh and/or perform side-effects.
+     *
+     * The method suspends and doesn't return until all work required by the object is finished. In
+     * most cases, it's expected for the work to remain ongoing forever so this method will forever
+     * suspend its caller until the coroutine that called it is canceled.
+     *
+     * Implementations could follow this pattern:
+     * ```kotlin
+     * override suspend fun onActivated(): Nothing {
+     *     coroutineScope {
+     *         launch { ... }
+     *         launch { ... }
+     *         launch { ... }
+     *         awaitCancellation()
+     *     }
+     * }
+     * ```
+     *
+     * @see activate
+     */
+    protected abstract suspend fun onActivated(): Nothing
+}
diff --git a/packages/SystemUI/src/com/android/systemui/lifecycle/Hydrator.kt b/packages/SystemUI/src/com/android/systemui/lifecycle/Hydrator.kt
new file mode 100644
index 0000000..59ec2af
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/lifecycle/Hydrator.kt
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.lifecycle
+
+import androidx.compose.runtime.State
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.snapshots.StateFactoryMarker
+import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.launch
+
+/**
+ * Keeps snapshot/Compose [State]s up-to-date.
+ *
+ * ```kotlin
+ * val hydrator = Hydrator()
+ * val state: Int by hydrator.hydratedStateOf(upstreamFlow)
+ *
+ * override suspend fun activate(): Nothing {
+ *     hydrator.activate()
+ * }
+ * ```
+ */
+class Hydrator : ExclusiveActivatable() {
+
+    private val children = mutableListOf<Activatable>()
+
+    /**
+     * Returns a snapshot [State] that's kept up-to-date as long as the [SysUiViewModel] is active.
+     *
+     * @param source The upstream [StateFlow] to collect from; values emitted to it will be
+     *   automatically set on the returned [State].
+     */
+    @StateFactoryMarker
+    fun <T> hydratedStateOf(
+        source: StateFlow<T>,
+    ): State<T> {
+        return hydratedStateOf(
+            initialValue = source.value,
+            source = source,
+        )
+    }
+
+    /**
+     * Returns a snapshot [State] that's kept up-to-date as long as the [SysUiViewModel] is active.
+     *
+     * @param initialValue The first value to place on the [State]
+     * @param source The upstream [Flow] to collect from; values emitted to it will be automatically
+     *   set on the returned [State].
+     */
+    @StateFactoryMarker
+    fun <T> hydratedStateOf(
+        initialValue: T,
+        source: Flow<T>,
+    ): State<T> {
+        check(!isActive) { "Cannot call hydratedStateOf after Hydrator is already active." }
+
+        val mutableState = mutableStateOf(initialValue)
+        children.add(
+            object : ExclusiveActivatable() {
+                override suspend fun onActivated(): Nothing {
+                    source.collect { mutableState.value = it }
+                    awaitCancellation()
+                }
+            }
+        )
+        return mutableState
+    }
+
+    override suspend fun onActivated() = coroutineScope {
+        children.forEach { child -> launch { child.activate() } }
+        awaitCancellation()
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/lifecycle/SysUiViewModel.kt b/packages/SystemUI/src/com/android/systemui/lifecycle/SysUiViewModel.kt
index 979eaef..29ffcbd 100644
--- a/packages/SystemUI/src/com/android/systemui/lifecycle/SysUiViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/lifecycle/SysUiViewModel.kt
@@ -18,75 +18,31 @@
 
 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 androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.remember
 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() {
-
-    /**
-     * Returns a snapshot [State] that's kept up-to-date as long as the [SysUiViewModel] is active.
-     *
-     * @param source The upstream [StateFlow] to collect from; values emitted to it will be
-     *   automatically set on the returned [State].
-     */
-    @StateFactoryMarker
-    fun <T> hydratedStateOf(
-        source: StateFlow<T>,
-    ): State<T> {
-        return hydratedStateOf(
-            initialValue = source.value,
-            source = source,
-        )
-    }
-
-    /**
-     * Returns a snapshot [State] that's kept up-to-date as long as the [SysUiViewModel] is active.
-     *
-     * @param initialValue The first value to place on the [State]
-     * @param source The upstream [Flow] to collect from; values emitted to it will be automatically
-     *   set on the returned [State].
-     */
-    @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()
-    }
-}
+/** Defines interface for all System UI view-models. */
+interface SysUiViewModel
 
 /**
- * Returns a remembered [SysUiViewModel] of the type [T] that's automatically kept active until this
- * composable leaves the composition.
- *
- * If the [key] changes, the old [SysUiViewModel] is deactivated and a new one will be instantiated,
+ * Returns a remembered [SysUiViewModel] of the type [T]. If the returned instance is also an
+ * [Activatable], it's automatically kept active until this composable leaves the composition; if
+ * the [key] changes, the old [SysUiViewModel] is deactivated and a new one will be instantiated,
  * activated, and returned.
  */
 @Composable
 fun <T : SysUiViewModel> rememberViewModel(
     key: Any = Unit,
     factory: () -> T,
-): T = rememberActivated(key, factory)
+): T {
+    val instance = remember(key) { factory() }
+    if (instance is Activatable) {
+        LaunchedEffect(instance) { instance.activate() }
+    }
+    return instance
+}
 
 /**
  * Invokes [block] in a new coroutine with a new [SysUiViewModel] that is automatically activated
@@ -100,6 +56,8 @@
 ): Nothing =
     repeatOnWindowLifecycle(minWindowLifecycleState) {
         val instance = factory()
-        launch { instance.activate() }
+        if (instance is Activatable) {
+            launch { instance.activate() }
+        }
         block(instance)
     }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneContentViewModel.kt
index 55b8f5f..12f3c9c 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneContentViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneContentViewModel.kt
@@ -44,7 +44,7 @@
     private val footerActionsViewModelFactory: FooterActionsViewModel.Factory,
     private val footerActionsController: FooterActionsController,
     val mediaCarouselInteractor: MediaCarouselInteractor,
-) : SysUiViewModel() {
+) : SysUiViewModel {
 
     val isMediaVisible: StateFlow<Boolean> = mediaCarouselInteractor.hasAnyMediaOrRecommendation
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneContentViewModel.kt
index abfca4b..cb99be4 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneContentViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneContentViewModel.kt
@@ -35,7 +35,7 @@
 constructor(
     val overlayShadeViewModelFactory: OverlayShadeViewModel.Factory,
     val quickSettingsContainerViewModel: QuickSettingsContainerViewModel,
-) : SysUiViewModel() {
+) : SysUiViewModel {
 
     @AssistedFactory
     interface Factory {
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneActionsViewModel.kt
index b5de1b6..9144f16d 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneActionsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneActionsViewModel.kt
@@ -18,6 +18,7 @@
 
 import com.android.compose.animation.scene.UserAction
 import com.android.compose.animation.scene.UserActionResult
+import com.android.systemui.lifecycle.ExclusiveActivatable
 import com.android.systemui.lifecycle.SysUiViewModel
 import kotlinx.coroutines.awaitCancellation
 import kotlinx.coroutines.flow.MutableStateFlow
@@ -32,7 +33,7 @@
  * need to worry about resetting the value of [actions] when the view-model is deactivated/canceled,
  * this base class takes care of it.
  */
-abstract class SceneActionsViewModel : SysUiViewModel() {
+abstract class SceneActionsViewModel : SysUiViewModel, ExclusiveActivatable() {
 
     private val _actions = MutableStateFlow<Map<UserAction, UserActionResult>>(emptyMap())
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
index f8a9f8c..9dfb745 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
@@ -24,6 +24,8 @@
 import com.android.compose.animation.scene.UserActionResult
 import com.android.systemui.classifier.Classifier
 import com.android.systemui.classifier.domain.interactor.FalsingInteractor
+import com.android.systemui.lifecycle.ExclusiveActivatable
+import com.android.systemui.lifecycle.Hydrator
 import com.android.systemui.lifecycle.SysUiViewModel
 import com.android.systemui.power.domain.interactor.PowerInteractor
 import com.android.systemui.scene.domain.interactor.SceneInteractor
@@ -32,7 +34,6 @@
 import dagger.assisted.Assisted
 import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
-import kotlinx.coroutines.awaitCancellation
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.StateFlow
 
@@ -45,7 +46,7 @@
     private val powerInteractor: PowerInteractor,
     private val logger: SceneLogger,
     @Assisted private val motionEventHandlerReceiver: (MotionEventHandler?) -> Unit,
-) : SysUiViewModel() {
+) : SysUiViewModel, ExclusiveActivatable() {
     /**
      * Keys of all scenes in the container.
      *
@@ -57,8 +58,10 @@
     /** The scene that should be rendered. */
     val currentScene: StateFlow<SceneKey> = sceneInteractor.currentScene
 
+    private val hydrator = Hydrator()
+
     /** Whether the container is visible. */
-    val isVisible: Boolean by hydratedStateOf(sceneInteractor.isVisible)
+    val isVisible: Boolean by hydrator.hydratedStateOf(sceneInteractor.isVisible)
 
     override suspend fun onActivated(): Nothing {
         try {
@@ -75,7 +78,8 @@
                     }
                 }
             )
-            awaitCancellation()
+
+            hydrator.activate()
         } finally {
             // Clears the previously-sent MotionEventHandler so the owner of the view-model releases
             // their reference to it.
diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/ui/viewModel/BrightnessMirrorViewModel.kt b/packages/SystemUI/src/com/android/systemui/settings/brightness/ui/viewModel/BrightnessMirrorViewModel.kt
index 7f8c146..706797d 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/brightness/ui/viewModel/BrightnessMirrorViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/ui/viewModel/BrightnessMirrorViewModel.kt
@@ -37,7 +37,7 @@
     private val brightnessMirrorShowingInteractor: BrightnessMirrorShowingInteractor,
     @Main private val resources: Resources,
     val sliderControllerFactory: BrightnessSliderController.Factory,
-) : SysUiViewModel(), MirrorController {
+) : SysUiViewModel, MirrorController {
 
     private val tempPosition = IntArray(2)
 
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/OverlayShadeViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/OverlayShadeViewModel.kt
index 00c0235..25ae44e 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/OverlayShadeViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/OverlayShadeViewModel.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.shade.ui.viewmodel
 
 import com.android.compose.animation.scene.SceneKey
+import com.android.systemui.lifecycle.ExclusiveActivatable
 import com.android.systemui.lifecycle.SysUiViewModel
 import com.android.systemui.scene.domain.interactor.SceneInteractor
 import com.android.systemui.scene.shared.model.SceneFamilies
@@ -37,7 +38,7 @@
 class OverlayShadeViewModel
 @AssistedInject
 constructor(private val sceneInteractor: SceneInteractor, shadeInteractor: ShadeInteractor) :
-    SysUiViewModel() {
+    SysUiViewModel, ExclusiveActivatable() {
     private val _backgroundScene = MutableStateFlow(Scenes.Lockscreen)
     /** The scene to show in the background when the overlay shade is open. */
     val backgroundScene: StateFlow<SceneKey> = _backgroundScene.asStateFlow()
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt
index f0e9d41..edfe79a 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt
@@ -24,6 +24,7 @@
 import android.os.UserHandle
 import android.provider.Settings
 import com.android.systemui.broadcast.BroadcastDispatcher
+import com.android.systemui.lifecycle.ExclusiveActivatable
 import com.android.systemui.lifecycle.SysUiViewModel
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.privacy.OngoingPrivacyChip
@@ -65,7 +66,7 @@
     private val privacyChipInteractor: PrivacyChipInteractor,
     private val clockInteractor: ShadeHeaderClockInteractor,
     private val broadcastDispatcher: BroadcastDispatcher,
-) : SysUiViewModel() {
+) : SysUiViewModel, ExclusiveActivatable() {
     /** True if there is exactly one mobile connection. */
     val isSingleCarrier: StateFlow<Boolean> = mobileIconsInteractor.isSingleCarrier
 
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneContentViewModel.kt
index fe3bcb5..f0f2a65 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneContentViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneContentViewModel.kt
@@ -20,6 +20,7 @@
 
 import androidx.lifecycle.LifecycleOwner
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
+import com.android.systemui.lifecycle.ExclusiveActivatable
 import com.android.systemui.lifecycle.SysUiViewModel
 import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor
 import com.android.systemui.qs.FooterActionsController
@@ -61,7 +62,7 @@
     private val unfoldTransitionInteractor: UnfoldTransitionInteractor,
     private val deviceEntryInteractor: DeviceEntryInteractor,
     private val sceneInteractor: SceneInteractor,
-) : SysUiViewModel() {
+) : SysUiViewModel, ExclusiveActivatable() {
 
     val shadeMode: StateFlow<ShadeMode> = shadeInteractor.shadeMode
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt
index f9937e5..b2045fe 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt
@@ -23,6 +23,7 @@
 import com.android.compose.animation.scene.SceneKey
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
+import com.android.systemui.lifecycle.ExclusiveActivatable
 import com.android.systemui.lifecycle.SysUiViewModel
 import com.android.systemui.scene.domain.interactor.SceneInteractor
 import com.android.systemui.scene.shared.flag.SceneContainerFlag
@@ -62,7 +63,8 @@
     keyguardInteractor: Lazy<KeyguardInteractor>,
 ) :
     ActivatableFlowDumper by ActivatableFlowDumperImpl(dumpManager, "NotificationScrollViewModel"),
-    SysUiViewModel() {
+    SysUiViewModel,
+    ExclusiveActivatable() {
 
     override suspend fun onActivated(): Nothing {
         activateFlowDumper()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt
index ffa1de7..d891f62 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt
@@ -20,6 +20,7 @@
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.flags.FeatureFlagsClassic
 import com.android.systemui.flags.Flags
+import com.android.systemui.lifecycle.ExclusiveActivatable
 import com.android.systemui.lifecycle.SysUiViewModel
 import com.android.systemui.scene.domain.interactor.SceneInteractor
 import com.android.systemui.scene.shared.flag.SceneContainerFlag
@@ -52,7 +53,8 @@
     featureFlags: FeatureFlagsClassic,
     dumpManager: DumpManager,
 ) :
-    SysUiViewModel(),
+    SysUiViewModel,
+    ExclusiveActivatable(),
     ActivatableFlowDumper by ActivatableFlowDumperImpl(
         dumpManager = dumpManager,
         tag = "NotificationsPlaceholderViewModel",
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/FlowDumper.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/FlowDumper.kt
index ae0061b..727e51f 100644
--- a/packages/SystemUI/src/com/android/systemui/util/kotlin/FlowDumper.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/FlowDumper.kt
@@ -19,7 +19,7 @@
 import android.util.IndentingPrintWriter
 import com.android.systemui.Dumpable
 import com.android.systemui.dump.DumpManager
-import com.android.systemui.lifecycle.BaseActivatable
+import com.android.systemui.lifecycle.ExclusiveActivatable
 import com.android.systemui.lifecycle.SysUiViewModel
 import com.android.systemui.util.asIndenting
 import com.android.systemui.util.printCollection
@@ -189,7 +189,7 @@
 ) : SimpleFlowDumper(), ActivatableFlowDumper {
 
     private val registration =
-        object : BaseActivatable() {
+        object : ExclusiveActivatable() {
             override suspend fun onActivated(): Nothing {
                 try {
                     dumpManager.registerCriticalDumpable(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/lifecycle/SysUiViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/lifecycle/SysUiViewModelTest.kt
index 7d57220..c7acd78 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/lifecycle/SysUiViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/lifecycle/SysUiViewModelTest.kt
@@ -17,14 +17,8 @@
 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
@@ -32,8 +26,6 @@
 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
@@ -157,51 +149,9 @@
 
         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() {
+private class FakeViewModel : SysUiViewModel, ExclusiveActivatable() {
     var isActivated = false
 
     override suspend fun onActivated(): Nothing {
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/lifecycle/FakeActivatable.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/lifecycle/FakeActivatable.kt
index 4c05939..e66a2be 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/lifecycle/FakeActivatable.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/lifecycle/FakeActivatable.kt
@@ -21,7 +21,7 @@
 class FakeActivatable(
     private val onActivation: () -> Unit = {},
     private val onDeactivation: () -> Unit = {},
-) : BaseActivatable() {
+) : ExclusiveActivatable() {
     var activationCount = 0
     var cancellationCount = 0
 
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 90cd8c7..1652462 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
@@ -17,7 +17,6 @@
 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
@@ -29,19 +28,21 @@
     private val onDeactivation: () -> Unit = {},
     private val upstreamFlow: Flow<Boolean> = flowOf(true),
     private val upstreamStateFlow: StateFlow<Boolean> = MutableStateFlow(true).asStateFlow(),
-) : SysUiViewModel() {
+) : SysUiViewModel, ExclusiveActivatable() {
 
     var activationCount = 0
     var cancellationCount = 0
 
-    val stateBackedByFlow: Boolean by hydratedStateOf(initialValue = true, source = upstreamFlow)
-    val stateBackedByStateFlow: Boolean by hydratedStateOf(source = upstreamStateFlow)
+    private val hydrator = Hydrator()
+    val stateBackedByFlow: Boolean by
+        hydrator.hydratedStateOf(initialValue = true, source = upstreamFlow)
+    val stateBackedByStateFlow: Boolean by hydrator.hydratedStateOf(source = upstreamStateFlow)
 
     override suspend fun onActivated(): Nothing {
         activationCount++
         onActivation()
         try {
-            awaitCancellation()
+            hydrator.activate()
         } finally {
             cancellationCount++
             onDeactivation()