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

Commit a568f26a authored by William Xiao's avatar William Xiao
Browse files

Expose glanceable hub transition state

This CL exposes the full transition state of the glanceable hub's
SceneTransitionLayout so that other pieces of SysUI can use it for
transitions.

Bug: 315203819
Bug: 315490861
Fixed: 315203819
Test: atest CommunalRepositoryImplTest
Flag: ACONFIG com.android.systemui.communal_hub DEVELOPMENT
Change-Id: Ic496a40657ec6cd7e7c548d703de2c8be01d2788
parent e2b283d9
Loading
Loading
Loading
Loading
+41 −4
Original line number Diff line number Diff line
@@ -14,6 +14,7 @@ import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -28,15 +29,20 @@ import androidx.compose.ui.unit.dp
import com.android.compose.animation.scene.Edge
import com.android.compose.animation.scene.ElementKey
import com.android.compose.animation.scene.FixedSizeEdgeDetector
import com.android.compose.animation.scene.ObservableTransitionState
import com.android.compose.animation.scene.SceneKey
import com.android.compose.animation.scene.SceneScope
import com.android.compose.animation.scene.SceneTransitionLayout
import com.android.compose.animation.scene.SceneTransitionLayoutState
import com.android.compose.animation.scene.Swipe
import com.android.compose.animation.scene.SwipeDirection
import com.android.compose.animation.scene.observableTransitionState
import com.android.compose.animation.scene.transitions
import com.android.systemui.communal.shared.model.CommunalSceneKey
import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState
import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.transform

object Communal {
@@ -60,7 +66,7 @@ val sceneTransitions = transitions {
 * This is a temporary container to allow the communal UI to use [SceneTransitionLayout] for gesture
 * handling and transitions before the full Flexiglass layout is ready.
 */
@OptIn(ExperimentalComposeUiApi::class)
@OptIn(ExperimentalComposeUiApi::class, ExperimentalCoroutinesApi::class)
@Composable
fun CommunalContainer(
    modifier: Modifier = Modifier,
@@ -81,6 +87,15 @@ fun CommunalContainer(
        return
    }

    // This effect exposes the SceneTransitionLayout's observable transition state to the rest of
    // the system, and unsets it when the view is disposed to avoid a memory leak.
    DisposableEffect(viewModel, sceneTransitionLayoutState) {
        viewModel.setTransitionState(
            sceneTransitionLayoutState.observableTransitionState().map { it.toModel() }
        )
        onDispose { viewModel.setTransitionState(null) }
    }

    Box(modifier = modifier.fillMaxSize()) {
        SceneTransitionLayout(
            modifier = Modifier.fillMaxSize(),
@@ -171,18 +186,40 @@ private fun SceneScope.CommunalScene(
    Box(modifier.element(Communal.Elements.Content)) { CommunalHub(viewModel = viewModel) }
}

// TODO(b/293899074): Remove these conversions once Compose can be used throughout SysUI.
// TODO(b/315490861): Remove these conversions once Compose can be used throughout SysUI.
object TransitionSceneKey {
    val Blank = CommunalSceneKey.Blank.toTransitionSceneKey()
    val Communal = CommunalSceneKey.Communal.toTransitionSceneKey()
}

// TODO(b/315490861): Remove these conversions once Compose can be used throughout SysUI.
fun SceneKey.toCommunalSceneKey(): CommunalSceneKey {
    return this.identity as CommunalSceneKey
}

// TODO(b/315490861): Remove these conversions once Compose can be used throughout SysUI.
fun CommunalSceneKey.toTransitionSceneKey(): SceneKey {
    return SceneKey(name = toString(), identity = this)
}

fun SceneKey.toCommunalSceneKey(): CommunalSceneKey {
    return this.identity as CommunalSceneKey
/**
 * Converts between the [SceneTransitionLayout] state class and our forked data class that can be
 * used throughout SysUI.
 */
// TODO(b/315490861): Remove these conversions once Compose can be used throughout SysUI.
fun ObservableTransitionState.toModel(): ObservableCommunalTransitionState {
    return when (this) {
        is ObservableTransitionState.Idle ->
            ObservableCommunalTransitionState.Idle(scene.toCommunalSceneKey())
        is ObservableTransitionState.Transition ->
            ObservableCommunalTransitionState.Transition(
                fromScene = fromScene.toCommunalSceneKey(),
                toScene = toScene.toCommunalSceneKey(),
                progress = progress,
                isInitiatedByUserInput = isInitiatedByUserInput,
                isUserInputOngoing = isUserInputOngoing,
            )
    }
}

object ContainerDimensions {
+55 −25
Original line number Diff line number Diff line
@@ -20,6 +20,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.communal.shared.model.CommunalSceneKey
import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.flags.FakeFeatureFlagsClassic
import com.android.systemui.flags.Flags
@@ -29,6 +30,8 @@ import com.android.systemui.scene.shared.flag.FakeSceneContainerFlags
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.shared.model.SceneModel
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Before
@@ -40,27 +43,28 @@ import org.junit.runner.RunWith
class CommunalRepositoryImplTest : SysuiTestCase() {
    private lateinit var underTest: CommunalRepositoryImpl

    private lateinit var testScope: TestScope
    private val testDispatcher = StandardTestDispatcher()
    private val testScope = TestScope(testDispatcher)

    private lateinit var featureFlagsClassic: FakeFeatureFlagsClassic
    private lateinit var sceneContainerFlags: FakeSceneContainerFlags
    private lateinit var sceneContainerRepository: SceneContainerRepository

    @Before
    fun setUp() {
        testScope = TestScope()

        val sceneTestUtils = SceneTestUtils(this)
        sceneContainerFlags = FakeSceneContainerFlags(enabled = false)
        sceneContainerRepository = sceneTestUtils.fakeSceneContainerRepository()
        featureFlagsClassic = FakeFeatureFlagsClassic()

        featureFlagsClassic.set(Flags.COMMUNAL_SERVICE_ENABLED, true)

        underTest =
            CommunalRepositoryImpl(
        underTest = createRepositoryImpl(false)
    }

    private fun createRepositoryImpl(sceneContainerEnabled: Boolean): CommunalRepositoryImpl {
        return CommunalRepositoryImpl(
            testScope.backgroundScope,
            featureFlagsClassic,
                sceneContainerFlags,
            FakeSceneContainerFlags(enabled = sceneContainerEnabled),
            sceneContainerRepository,
        )
    }
@@ -86,13 +90,7 @@ class CommunalRepositoryImplTest : SysuiTestCase() {
    @Test
    fun isCommunalShowing_sceneContainerEnabled_onCommunalScene_true() =
        testScope.runTest {
            sceneContainerFlags = FakeSceneContainerFlags(enabled = true)
            underTest =
                CommunalRepositoryImpl(
                    featureFlagsClassic,
                    sceneContainerFlags,
                    sceneContainerRepository,
                )
            underTest = createRepositoryImpl(true)

            sceneContainerRepository.setDesiredScene(SceneModel(key = SceneKey.Communal))

@@ -103,17 +101,49 @@ class CommunalRepositoryImplTest : SysuiTestCase() {
    @Test
    fun isCommunalShowing_sceneContainerEnabled_onLockscreenScene_false() =
        testScope.runTest {
            sceneContainerFlags = FakeSceneContainerFlags(enabled = true)
            underTest =
                CommunalRepositoryImpl(
                    featureFlagsClassic,
                    sceneContainerFlags,
                    sceneContainerRepository,
                )
            underTest = createRepositoryImpl(true)

            sceneContainerRepository.setDesiredScene(SceneModel(key = SceneKey.Lockscreen))

            val isCommunalHubShowing by collectLastValue(underTest.isCommunalHubShowing)
            assertThat(isCommunalHubShowing).isFalse()
        }

    @Test
    fun transitionState_idleByDefault() =
        testScope.runTest {
            val transitionState by collectLastValue(underTest.transitionState)
            assertThat(transitionState)
                .isEqualTo(ObservableCommunalTransitionState.Idle(CommunalSceneKey.DEFAULT))
        }

    @Test
    fun transitionState_setTransitionState_returnsNewValue() =
        testScope.runTest {
            val expectedSceneKey = CommunalSceneKey.Communal
            underTest.setTransitionState(
                flowOf(ObservableCommunalTransitionState.Idle(expectedSceneKey))
            )

            val transitionState by collectLastValue(underTest.transitionState)
            assertThat(transitionState)
                .isEqualTo(ObservableCommunalTransitionState.Idle(expectedSceneKey))
        }

    @Test
    fun transitionState_setNullTransitionState_returnsDefaultValue() =
        testScope.runTest {
            // Set a value for the transition state flow.
            underTest.setTransitionState(
                flowOf(ObservableCommunalTransitionState.Idle(CommunalSceneKey.Communal))
            )

            // Set the transition state flow back to null.
            underTest.setTransitionState(null)

            // Flow returns default scene key.
            val transitionState by collectLastValue(underTest.transitionState)
            assertThat(transitionState)
                .isEqualTo(ObservableCommunalTransitionState.Idle(CommunalSceneKey.DEFAULT))
        }
}
+42 −1
Original line number Diff line number Diff line
@@ -18,18 +18,26 @@ package com.android.systemui.communal.data.repository

import com.android.systemui.Flags.communalHub
import com.android.systemui.communal.shared.model.CommunalSceneKey
import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.flags.FeatureFlagsClassic
import com.android.systemui.flags.Flags
import com.android.systemui.scene.data.repository.SceneContainerRepository
import com.android.systemui.scene.shared.flag.SceneContainerFlags
import com.android.systemui.scene.shared.model.SceneKey
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
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.map
import kotlinx.coroutines.flow.stateIn

/** Encapsulates the state of communal mode. */
interface CommunalRepository {
@@ -45,14 +53,26 @@ interface CommunalRepository {
     */
    val desiredScene: StateFlow<CommunalSceneKey>

    /** Exposes the transition state of the communal [SceneTransitionLayout]. */
    val transitionState: StateFlow<ObservableCommunalTransitionState>

    /** Updates the requested scene. */
    fun setDesiredScene(desiredScene: CommunalSceneKey)

    /**
     * Updates the transition state of the hub [SceneTransitionLayout].
     *
     * Note that you must call is with `null` when the UI is done or risk a memory leak.
     */
    fun setTransitionState(transitionState: Flow<ObservableCommunalTransitionState>?)
}

@OptIn(ExperimentalCoroutinesApi::class)
@SysUISingleton
class CommunalRepositoryImpl
@Inject
constructor(
    @Background backgroundScope: CoroutineScope,
    private val featureFlagsClassic: FeatureFlagsClassic,
    sceneContainerFlags: SceneContainerFlags,
    sceneContainerRepository: SceneContainerRepository,
@@ -61,13 +81,34 @@ constructor(
        get() = featureFlagsClassic.isEnabled(Flags.COMMUNAL_SERVICE_ENABLED) && communalHub()

    private val _desiredScene: MutableStateFlow<CommunalSceneKey> =
        MutableStateFlow(CommunalSceneKey.Blank)
        MutableStateFlow(CommunalSceneKey.DEFAULT)
    override val desiredScene: StateFlow<CommunalSceneKey> = _desiredScene.asStateFlow()

    private val defaultTransitionState =
        ObservableCommunalTransitionState.Idle(CommunalSceneKey.DEFAULT)
    private val _transitionState = MutableStateFlow<Flow<ObservableCommunalTransitionState>?>(null)
    override val transitionState: StateFlow<ObservableCommunalTransitionState> =
        _transitionState
            .flatMapLatest { innerFlowOrNull -> innerFlowOrNull ?: flowOf(defaultTransitionState) }
            .stateIn(
                scope = backgroundScope,
                started = SharingStarted.Lazily,
                initialValue = defaultTransitionState,
            )

    override fun setDesiredScene(desiredScene: CommunalSceneKey) {
        _desiredScene.value = desiredScene
    }

    /**
     * Updates the transition state of the hub [SceneTransitionLayout].
     *
     * Note that you must call is with `null` when the UI is done or risk a memory leak.
     */
    override fun setTransitionState(transitionState: Flow<ObservableCommunalTransitionState>?) {
        _transitionState.value = transitionState
    }

    override val isCommunalHubShowing: Flow<Boolean> =
        if (sceneContainerFlags.isEnabled()) {
            sceneContainerRepository.desiredScene.map { scene -> scene.key == SceneKey.Communal }
+14 −0
Original line number Diff line number Diff line
@@ -25,6 +25,7 @@ import com.android.systemui.communal.data.repository.CommunalWidgetRepository
import com.android.systemui.communal.domain.model.CommunalContentModel
import com.android.systemui.communal.shared.model.CommunalContentSize
import com.android.systemui.communal.shared.model.CommunalSceneKey
import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState
import com.android.systemui.communal.widgets.EditWidgetsActivityStarter
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
@@ -62,6 +63,19 @@ constructor(
     */
    val desiredScene: StateFlow<CommunalSceneKey> = communalRepository.desiredScene

    /** Transition state of the hub mode. */
    val transitionState: StateFlow<ObservableCommunalTransitionState> =
        communalRepository.transitionState

    /**
     * Updates the transition state of the hub [SceneTransitionLayout].
     *
     * Note that you must call is with `null` when the UI is done or risk a memory leak.
     */
    fun setTransitionState(transitionState: Flow<ObservableCommunalTransitionState>?) {
        communalRepository.setTransitionState(transitionState)
    }

    /**
     * Flow that emits a boolean if the communal UI is showing, ie. the [desiredScene] is the
     * [CommunalSceneKey.Communal].
+4 −0
Original line number Diff line number Diff line
@@ -29,4 +29,8 @@ sealed class CommunalSceneKey(
    override fun toString(): String {
        return loggingName
    }

    companion object {
        val DEFAULT = Blank
    }
}
Loading