diff options
| author | 2023-12-11 22:59:29 +0000 | |
|---|---|---|
| committer | 2023-12-11 22:59:29 +0000 | |
| commit | 054eadd9cefae7312c9444d2c7b3fd1579398df7 (patch) | |
| tree | 94145d2553851fba50d32f445dfb151498217550 | |
| parent | 7c5b2bf2d52dd6cfb2f6f737ade961de4c4f4155 (diff) | |
| parent | a568f26ae082a384e023581d2f42d179029500ae (diff) | |
Merge "Expose glanceable hub transition state" into main
8 files changed, 249 insertions, 31 deletions
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt index 185a06c70f9e..d83f3aae1ace 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt @@ -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 { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalRepositoryImplTest.kt index 7196de6608cb..65176e1c5c0d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalRepositoryImplTest.kt @@ -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,29 +43,30 @@ 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( - featureFlagsClassic, - sceneContainerFlags, - sceneContainerRepository, - ) + underTest = createRepositoryImpl(false) + } + + private fun createRepositoryImpl(sceneContainerEnabled: Boolean): CommunalRepositoryImpl { + return CommunalRepositoryImpl( + testScope.backgroundScope, + featureFlagsClassic, + FakeSceneContainerFlags(enabled = sceneContainerEnabled), + sceneContainerRepository, + ) } @Test @@ -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)) + } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepository.kt index 3119b9e98bac..1f4be4060223 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepository.kt @@ -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 } diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt index e630fd4e3c54..1a2a4253e761 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt @@ -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]. diff --git a/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalSceneKey.kt b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalSceneKey.kt index 2be909c8e6d0..c68dd4ff271c 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalSceneKey.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalSceneKey.kt @@ -29,4 +29,8 @@ sealed class CommunalSceneKey( override fun toString(): String { return loggingName } + + companion object { + val DEFAULT = Blank + } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/shared/model/ObservableCommunalTransitionState.kt b/packages/SystemUI/src/com/android/systemui/communal/shared/model/ObservableCommunalTransitionState.kt new file mode 100644 index 000000000000..d834715768c9 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/shared/model/ObservableCommunalTransitionState.kt @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2023 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.communal.shared.model + +import kotlinx.coroutines.flow.Flow + +/** + * This is a fork of the `com.android.compose.animation.scene.ObservableTransitionState` class. + * + * TODO(b/315490861): remove this fork, once we can compile Compose into System UI. + */ +sealed class ObservableCommunalTransitionState { + /** No transition/animation is currently running. */ + data class Idle(val scene: CommunalSceneKey) : ObservableCommunalTransitionState() + + /** There is a transition animating between two scenes. */ + data class Transition( + val fromScene: CommunalSceneKey, + val toScene: CommunalSceneKey, + val progress: Flow<Float>, + + /** + * Whether the transition was originally triggered by user input rather than being + * programmatic. If this value is initially true, it will remain true until the transition + * fully completes, even if the user input that triggered the transition has ended. Any + * sub-transitions launched by this one will inherit this value. For example, if the user + * drags a pointer but does not exceed the threshold required to transition to another + * scene, this value will remain true after the pointer is no longer touching the screen and + * will be true in any transition created to animate back to the original position. + */ + val isInitiatedByUserInput: Boolean, + + /** + * Whether user input is currently driving the transition. For example, if a user is + * dragging a pointer, this emits true. Once they lift their finger, this emits false while + * the transition completes/settles. + */ + val isUserInputOngoing: Flow<Boolean>, + ) : ObservableCommunalTransitionState() +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt index 333fc194b288..708f137017ca 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt @@ -22,6 +22,7 @@ import android.view.MotionEvent import com.android.systemui.communal.domain.interactor.CommunalInteractor import com.android.systemui.communal.domain.model.CommunalContentModel import com.android.systemui.communal.shared.model.CommunalSceneKey +import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState import com.android.systemui.media.controls.ui.MediaHost import com.android.systemui.shade.ShadeViewController import javax.inject.Provider @@ -43,6 +44,15 @@ abstract class BaseCommunalViewModel( communalInteractor.onSceneChanged(scene) } + /** + * 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>?) { + communalInteractor.setTransitionState(transitionState) + } + // TODO(b/308813166): remove once CommunalContainer is moved lower in z-order and doesn't block // touches anymore. /** Called when a touch is received outside the edge swipe area when hub mode is closed. */ diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalRepository.kt index 2cb17b5badc4..c85c27e277b4 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalRepository.kt @@ -1,19 +1,47 @@ package com.android.systemui.communal.data.repository import com.android.systemui.communal.shared.model.CommunalSceneKey +import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState +import com.android.systemui.dagger.qualifiers.Background +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.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.test.TestScope /** Fake implementation of [CommunalRepository]. */ +@OptIn(ExperimentalCoroutinesApi::class) class FakeCommunalRepository( + @Background applicationScope: CoroutineScope = TestScope(), override var isCommunalEnabled: Boolean = false, override val desiredScene: MutableStateFlow<CommunalSceneKey> = - MutableStateFlow(CommunalSceneKey.Blank) + MutableStateFlow(CommunalSceneKey.DEFAULT), ) : CommunalRepository { override fun setDesiredScene(desiredScene: CommunalSceneKey) { this.desiredScene.value = desiredScene } + 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 = applicationScope, + started = SharingStarted.Eagerly, + initialValue = defaultTransitionState, + ) + + override fun setTransitionState(transitionState: Flow<ObservableCommunalTransitionState>?) { + _transitionState.value = transitionState + } + fun setIsCommunalEnabled(value: Boolean) { isCommunalEnabled = value } |