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

Commit b3d5a84a authored by William Xiao's avatar William Xiao Committed by Bryce Lee
Browse files

Allow showing hub immediately on view load

This change makes it possible to show the hub immediately upon load
from a state where keyguard is not showing and the hub container view
is not loaded. This is needed to support pressing power button to show
the hub from launcher (while postured & charging).

Bug: 378170118
Test: atest CommunalSceneRepositoryImplTest
Flag: com.android.systemui.glanceable_hub_v2
Change-Id: I5a85e15270af3efebd1b4157707f03700d4c4f9d
parent af2113a2
Loading
Loading
Loading
Loading
+1 −2
Original line number Diff line number Diff line
@@ -159,8 +159,7 @@ fun CommunalContainer(
    content: CommunalContent,
) {
    val coroutineScope = rememberCoroutineScope()
    val currentSceneKey: SceneKey by
        viewModel.currentScene.collectAsStateWithLifecycle(CommunalScenes.Blank)
    val currentSceneKey: SceneKey by viewModel.currentScene.collectAsStateWithLifecycle()
    val touchesAllowed by viewModel.touchesAllowed.collectAsStateWithLifecycle()
    val backgroundType by
        viewModel.communalBackground.collectAsStateWithLifecycle(
+42 −18
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 The Android Open Source Project
 * Copyright (C) 2025 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.
@@ -21,34 +21,44 @@ import androidx.test.filters.SmallTest
import com.android.compose.animation.scene.ObservableTransitionState
import com.android.systemui.SysuiTestCase
import com.android.systemui.communal.shared.model.CommunalScenes
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.kosmos.testScope
import com.android.systemui.scene.shared.model.sceneDataSource
import com.android.systemui.kosmos.backgroundScope
import com.android.systemui.kosmos.collectLastValue
import com.android.systemui.kosmos.runTest
import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.scene.shared.model.SceneDataSource
import com.android.systemui.scene.shared.model.SceneDataSourceDelegator
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify

@SmallTest
@RunWith(AndroidJUnit4::class)
class CommunalRepositoryImplTest : SysuiTestCase() {
class CommunalSceneRepositoryImplTest : SysuiTestCase() {
    private val kosmos = testKosmos().useUnconfinedTestDispatcher()

    private val kosmos = testKosmos()
    private val testScope = kosmos.testScope
    private val underTest by lazy {
    private val delegator = mock<SceneDataSourceDelegator> {}

    private val Kosmos.underTest by
        Kosmos.Fixture {
            CommunalSceneRepositoryImpl(
            kosmos.applicationCoroutineScope,
            kosmos.applicationCoroutineScope,
            kosmos.sceneDataSource,
                applicationScope = applicationCoroutineScope,
                backgroundScope = backgroundScope,
                sceneDataSource = delegator,
                delegator = delegator,
            )
        }

    @Test
    fun transitionState_idleByDefault() =
        testScope.runTest {
        kosmos.runTest {
            val transitionState by collectLastValue(underTest.transitionState)
            assertThat(transitionState)
                .isEqualTo(ObservableTransitionState.Idle(CommunalScenes.Default))
@@ -56,7 +66,7 @@ class CommunalRepositoryImplTest : SysuiTestCase() {

    @Test
    fun transitionState_setTransitionState_returnsNewValue() =
        testScope.runTest {
        kosmos.runTest {
            val expectedSceneKey = CommunalScenes.Communal
            underTest.setTransitionState(flowOf(ObservableTransitionState.Idle(expectedSceneKey)))

@@ -66,7 +76,7 @@ class CommunalRepositoryImplTest : SysuiTestCase() {

    @Test
    fun transitionState_setNullTransitionState_returnsDefaultValue() =
        testScope.runTest {
        kosmos.runTest {
            // Set a value for the transition state flow.
            underTest.setTransitionState(
                flowOf(ObservableTransitionState.Idle(CommunalScenes.Communal))
@@ -80,4 +90,18 @@ class CommunalRepositoryImplTest : SysuiTestCase() {
            assertThat(transitionState)
                .isEqualTo(ObservableTransitionState.Idle(CommunalScenes.Default))
        }

    @Test
    fun showHubFromPowerButton() =
        kosmos.runTest {
            fakeKeyguardRepository.setKeyguardShowing(false)

            underTest.showHubFromPowerButton()

            argumentCaptor<SceneDataSource>().apply {
                verify(delegator).setDelegate(capture())

                assertThat(firstValue.currentScene.value).isEqualTo(CommunalScenes.Communal)
            }
        }
}
+43 −1
Original line number Diff line number Diff line
@@ -16,7 +16,9 @@

package com.android.systemui.communal.data.repository

import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.compose.animation.scene.ObservableTransitionState
import com.android.compose.animation.scene.OverlayKey
import com.android.compose.animation.scene.SceneKey
import com.android.compose.animation.scene.TransitionKey
import com.android.systemui.communal.dagger.Communal
@@ -25,16 +27,17 @@ import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.scene.shared.model.SceneDataSource
import com.android.systemui.scene.shared.model.SceneDataSourceDelegator
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.stateIn
import com.android.app.tracing.coroutines.launchTraced as launch

/** Encapsulates the state of communal mode. */
interface CommunalSceneRepository {
@@ -52,6 +55,9 @@ interface CommunalSceneRepository {
    /** Immediately snaps to the desired scene. */
    fun snapToScene(toScene: SceneKey)

    /** Shows the hub from a power button press. */
    suspend fun showHubFromPowerButton()

    /**
     * Updates the transition state of the hub [SceneTransitionLayout].
     *
@@ -67,6 +73,7 @@ constructor(
    @Application private val applicationScope: CoroutineScope,
    @Background backgroundScope: CoroutineScope,
    @Communal private val sceneDataSource: SceneDataSource,
    @Communal private val delegator: SceneDataSourceDelegator,
) : CommunalSceneRepository {

    override val currentScene: StateFlow<SceneKey> = sceneDataSource.currentScene
@@ -98,6 +105,18 @@ constructor(
        }
    }

    override suspend fun showHubFromPowerButton() {
        // If keyguard is not showing yet, the hub view is not ready and the
        // [SceneDataSourceDelegator] will still be using the default [NoOpSceneDataSource]
        // and initial key, which is Blank. This means that when the hub container loads, it
        // will default to not showing the hub. Attempting to set the scene in this state
        // is simply ignored by the [NoOpSceneDataSource]. Instead, we temporarily override
        // it with a new one that defaults to Communal. This delegate will be overwritten
        // once the [CommunalContainer] loads.
        // TODO(b/392969914): show the hub first instead of forcing the scene.
        delegator.setDelegate(NoOpSceneDataSource(CommunalScenes.Communal))
    }

    /**
     * Updates the transition state of the hub [SceneTransitionLayout].
     *
@@ -106,4 +125,27 @@ constructor(
    override fun setTransitionState(transitionState: Flow<ObservableTransitionState>?) {
        _transitionState.value = transitionState
    }

    /** Noop implementation of a scene data source that always returns the initial [SceneKey]. */
    private class NoOpSceneDataSource(initialSceneKey: SceneKey) : SceneDataSource {
        override val currentScene: StateFlow<SceneKey> =
            MutableStateFlow(initialSceneKey).asStateFlow()

        override val currentOverlays: StateFlow<Set<OverlayKey>> =
            MutableStateFlow(emptySet<OverlayKey>()).asStateFlow()

        override fun changeScene(toScene: SceneKey, transitionKey: TransitionKey?) = Unit

        override fun snapToScene(toScene: SceneKey) = Unit

        override fun showOverlay(overlay: OverlayKey, transitionKey: TransitionKey?) = Unit

        override fun hideOverlay(overlay: OverlayKey, transitionKey: TransitionKey?) = Unit

        override fun replaceOverlay(
            from: OverlayKey,
            to: OverlayKey,
            transitionKey: TransitionKey?,
        ) = Unit
    }
}
+23 −0
Original line number Diff line number Diff line
@@ -148,6 +148,29 @@ constructor(
        }
    }

    fun showHubFromPowerButton() {
        val loggingReason = "showing hub from power button"
        applicationScope.launch("$TAG#showHubFromPowerButton") {
            if (SceneContainerFlag.isEnabled) {
                sceneInteractor.changeScene(
                    toScene = CommunalScenes.Communal.toSceneContainerSceneKey(),
                    loggingReason = loggingReason,
                )
                return@launch
            }

            if (currentScene.value == CommunalScenes.Communal) return@launch
            logger.logSceneChangeRequested(
                from = currentScene.value,
                to = CommunalScenes.Communal,
                reason = loggingReason,
                isInstant = true,
            )
            notifyListeners(CommunalScenes.Communal, null)
            repository.showHubFromPowerButton()
        }
    }

    private fun notifyListeners(newScene: SceneKey, keyguardState: KeyguardState?) {
        onSceneAboutToChangeListener.forEach { it.onSceneAboutToChange(newScene, keyguardState) }
    }
+1 −1
Original line number Diff line number Diff line
@@ -45,7 +45,7 @@ abstract class BaseCommunalViewModel(
    val mediaHost: MediaHost,
    val mediaCarouselController: MediaCarouselController,
) {
    val currentScene: Flow<SceneKey> = communalSceneInteractor.currentScene
    val currentScene: StateFlow<SceneKey> = communalSceneInteractor.currentScene

    /** Used to animate showing or hiding the communal content. */
    open val isCommunalContentVisible: Flow<Boolean> = MutableStateFlow(false)
Loading