Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit cdfbbc11 authored by Alejandro Nijamkin's avatar Alejandro Nijamkin
Browse files

[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
parent 6475b919
Loading
Loading
Loading
Loading
+110 −0
Original line number Diff line number Diff line
@@ -33,7 +33,7 @@ import org.junit.runner.RunWith

@SmallTest
@RunWith(AndroidJUnit4::class)
class BaseActivatableTest : SysuiTestCase() {
class ExclusiveActivatableTest : SysuiTestCase() {

    private val kosmos = testKosmos()
    private val testScope = kosmos.testScope
@@ -43,13 +43,11 @@ class BaseActivatableTest : SysuiTestCase() {
    @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)
        }
@@ -57,20 +55,17 @@ class BaseActivatableTest : SysuiTestCase() {
    @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)
        }
@@ -78,26 +73,22 @@ class BaseActivatableTest : SysuiTestCase() {
    @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)
        }
@@ -105,224 +96,15 @@ class BaseActivatableTest : SysuiTestCase() {
    @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()
        }
}
+84 −0
Original line number Diff line number Diff line
/*
 * 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")
    }
}
+0 −2
Original line number Diff line number Diff line
@@ -26,7 +26,6 @@ import com.android.systemui.flags.EnableSceneContainer
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 @@ class QuickSettingsSceneContentViewModelTest : SysuiTestCase() {
                footerActionsController = footerActionsController,
                mediaCarouselInteractor = kosmos.mediaCarouselInteractor,
            )
        underTest.activateIn(testScope)
    }

    @Test
+0 −4
Original line number Diff line number Diff line
@@ -55,7 +55,6 @@ class SceneActionsViewModelTest : SysuiTestCase() {
        testScope.runTest {
            val actions by collectLastValue(underTest.actions)

            assertThat(underTest.isActive).isFalse()
            assertThat(actions).isEmpty()
        }

@@ -66,7 +65,6 @@ class SceneActionsViewModelTest : SysuiTestCase() {
            underTest.activateIn(testScope)
            runCurrent()

            assertThat(underTest.isActive).isTrue()
            assertThat(actions).isEmpty()
        }

@@ -76,7 +74,6 @@ class SceneActionsViewModelTest : SysuiTestCase() {
            val actions by collectLastValue(underTest.actions)
            underTest.activateIn(testScope)
            runCurrent()
            assertThat(underTest.isActive).isTrue()

            val expected1 =
                mapOf(
@@ -116,7 +113,6 @@ class SceneActionsViewModelTest : SysuiTestCase() {

            job.cancel()
            runCurrent()
            assertThat(underTest.isActive).isFalse()
            assertThat(actions).isEmpty()
        }

+2 −1
Original line number Diff line number Diff line
@@ -21,6 +21,7 @@ import com.android.app.tracing.coroutines.flow.collectLatest
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 @@ sealed class AuthMethodBouncerViewModel(
     * being able to attempt to unlock the device.
     */
    val isInputEnabled: StateFlow<Boolean>,
) : SysUiViewModel() {
) : SysUiViewModel, ExclusiveActivatable() {

    private val _animateFailure = MutableStateFlow(false)
    /**
Loading