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

Commit 6674924c authored by Ale Nijamkin's avatar Ale Nijamkin
Browse files

[flexiglass] rememberViewModel uses lifecycle

Defaults to STARTED, which is more than CREATED and less than RESUMED.

Without passing anything in, this means that content that's always
composed (which stays in CREATED while invisible and switches to RESUMED
when visible), will only activate view-models once visible, by default.

The caller of rememberViewModel can override with CREATED (so
even invisible content activates the view-model), as needed.

Note the quasi-telescoping approach where we now have several flavours
of rememberViewModel, each one taking a combination of minActiveState
and key. This makes sure that this change is forward compatible (as only
some callers currently pass a key but most don't) while also freeing new
callers to pass only minActiveState or both - all without confusing the
compiler to mistake minActiveState for the key.

Fix: 432549544
Test: manually verified that restarting the phone or crashing and
restarting System UI no longer snaps to QS, even if it's always composed
Test: manually verified that QS scene and overlay are both interactive
(respond to touch) when alwaysCompose=true for them (see followup CL)
Flag: com.android.systemui.scene_container

Change-Id: I8c5e524e3bb110cb477f255601268c40d997e9ce
parent f6a7b72c
Loading
Loading
Loading
Loading
+79 −1
Original line number Diff line number Diff line
@@ -17,15 +17,21 @@
package com.android.systemui.lifecycle

import android.view.View
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.ui.viewmodel.FakeSysUiViewModel
import com.android.systemui.util.Assert
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runCurrent
@@ -79,7 +85,7 @@ class SysUiViewModelTest : SysuiTestCase() {
            // return Unit instead of FakeSysUiViewModel. It might be an issue with the compose
            // compiler.
            val unused: FakeSysUiViewModel =
                rememberViewModel("test", key) {
                rememberViewModel("test", key = key) {
                    when (key) {
                        1 ->
                            FakeSysUiViewModel(
@@ -109,6 +115,78 @@ class SysUiViewModelTest : SysuiTestCase() {
        assertThat(isActive2).isFalse()
    }

    @Test
    fun rememberActivated_minActiveState_CREATED() {
        assertActivationThroughAllLifecycleStates(Lifecycle.State.CREATED)
    }

    @Test
    fun rememberActivated_minActiveState_STARTED() {
        assertActivationThroughAllLifecycleStates(Lifecycle.State.STARTED)
    }

    @Test
    fun rememberActivated_minActiveState_RESUMED() {
        assertActivationThroughAllLifecycleStates(Lifecycle.State.RESUMED)
    }

    private fun assertActivationThroughAllLifecycleStates(minActiveState: Lifecycle.State) {
        var isActive = false
        val lifecycleOwner =
            composeRule.runOnUiThread {
                object : LifecycleOwner {
                    override val lifecycle = LifecycleRegistry(this)

                    init {
                        lifecycle.currentState = Lifecycle.State.CREATED
                    }
                }
            }
        composeRule.setContent {
            CompositionLocalProvider(LocalLifecycleOwner provides lifecycleOwner) {
                // Need to explicitly state the type to avoid a weird issue where the factory seems
                // to return Unit instead of FakeSysUiViewModel. It might be an issue with the
                // compose compiler.
                val unused: FakeSysUiViewModel =
                    rememberViewModel(traceName = "test", minActiveState = minActiveState) {
                        FakeSysUiViewModel(
                            onActivation = { isActive = true },
                            onDeactivation = { isActive = false },
                        )
                    }
            }
        }

        // Increase state, step-by-step, all the way to RESUMED, the maximum state and then, reverse
        // course and decrease the state, step-by-step, all the way back down to CREATED. Lastly,
        // move to DESTROYED to finish up.
        //
        // In each step along the way, verify that our Activatable is active or not, based on the
        // minActiveState that we received. The Activatable should be active only if the current\
        // lifecycle state is equal to or "greater" than the minActiveState.
        listOf(
                Lifecycle.State.CREATED,
                Lifecycle.State.STARTED,
                Lifecycle.State.RESUMED,
                Lifecycle.State.STARTED,
                Lifecycle.State.CREATED,
                Lifecycle.State.DESTROYED,
            )
            .forEachIndexed { index, lifecycleState ->
                composeRule.runOnUiThread { lifecycleOwner.lifecycle.currentState = lifecycleState }
                composeRule.waitForIdle()
                val expectedIsActive = lifecycleState.isAtLeast(minActiveState)
                assertWithMessage(
                        "isActive=$isActive but expected to be $expectedIsActive when" +
                            " lifecycleState=$lifecycleState because $lifecycleState is" +
                            " ${if (expectedIsActive) "equal to or greater" else "less"} than" +
                            " minActiveState=$minActiveState (iteration #$index)"
                    )
                    .that(isActive)
                    .isEqualTo(expectedIsActive)
            }
    }

    @Test
    fun rememberActivated_leavingTheComposition() {
        val keepAliveMutable = mutableStateOf(true)
+15 −3
Original line number Diff line number Diff line
@@ -18,10 +18,11 @@ package com.android.systemui.lifecycle

import android.view.View
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.lifecycle.Lifecycle
import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.app.tracing.coroutines.traceCoroutine
import com.android.compose.lifecycle.LaunchedEffectWithLifecycle
import kotlinx.coroutines.CoroutineScope

/**
@@ -34,12 +35,23 @@ import kotlinx.coroutines.CoroutineScope
 * that's unique enough and easy enough to find in code search; this should help correlate
 * performance findings with actual code. One recommendation: prefer whole string literals instead
 * of some complex concatenation or templating scheme.
 *
 * The remembered view-model is activated every time the [minActiveState] is reached and deactivated
 * each time the lifecycle state falls "below" the [minActiveState]. This can be used to have more
 * granular control over when exactly a view-model becomes active.
 */
@Composable
fun <T> rememberViewModel(traceName: String, key: Any = Unit, factory: () -> T): T {
fun <T> rememberViewModel(
    traceName: String,
    minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
    key: Any = Unit,
    factory: () -> T,
): T {
    val instance = remember(key) { factory() }
    if (instance is Activatable) {
        LaunchedEffect(instance) { traceCoroutine(traceName) { instance.activate() } }
        LaunchedEffectWithLifecycle(key1 = instance, minActiveState = minActiveState) {
            traceCoroutine(traceName) { instance.activate() }
        }
    }
    return instance
}
+1 −1
Original line number Diff line number Diff line
@@ -86,7 +86,7 @@ constructor(

        val context = LocalContext.current
        val textFeedbackViewModel =
            rememberViewModel(traceName = "InfiniteGridLayout.TileGrid", context) {
            rememberViewModel(traceName = "InfiniteGridLayout.TileGrid", key = context) {
                textFeedbackContentViewModelFactory.create(context)
            }

+1 −1
Original line number Diff line number Diff line
@@ -181,7 +181,7 @@ private fun ToolbarTextFeedback(
    Box(modifier = modifier) {
        val context = LocalContext.current
        val viewModel =
            rememberViewModel("Toolbar.TextFeedbackViewModel", context) {
            rememberViewModel("Toolbar.TextFeedbackViewModel", key = context) {
                viewModelFactory.create(context)
            }
        val hasTextFeedback = viewModel.textFeedback !is TextFeedbackViewModel.NoFeedback