diff options
author | 2025-03-21 09:39:59 -0700 | |
---|---|---|
committer | 2025-03-21 09:39:59 -0700 | |
commit | 90cead419db6089bbcdd7cbde787781bd088ceec (patch) | |
tree | f22139641ea64c8ed44a2d292c1d274fe65658ff | |
parent | 2c4b90aaa5d4a32e67ac9fc353e8bb4571f00c53 (diff) | |
parent | d142cf112d956e7a011532239bcf687636466300 (diff) |
Merge "Fix flickering when swiping back to keyguard from hub in landscape" into main
13 files changed, 549 insertions, 15 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 2b8fe39c4870..16a8f987ba83 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 @@ -1,5 +1,6 @@ package com.android.systemui.communal.ui.compose +import android.content.res.Configuration import androidx.compose.animation.core.CubicBezierEasing import androidx.compose.animation.core.RepeatMode import androidx.compose.animation.core.animateFloat @@ -15,6 +16,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -26,6 +28,7 @@ import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.BlendMode import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.semantics.disabled @@ -55,6 +58,7 @@ import com.android.systemui.communal.ui.compose.Dimensions.Companion.SlideOffset import com.android.systemui.communal.ui.compose.extensions.allowGestures import com.android.systemui.communal.ui.viewmodel.CommunalViewModel import com.android.systemui.communal.util.CommunalColors +import com.android.systemui.keyguard.domain.interactor.FromGlanceableHubTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION import com.android.systemui.keyguard.domain.interactor.FromPrimaryBouncerTransitionInteractor.Companion.TO_GONE_DURATION import com.android.systemui.scene.shared.model.SceneDataSourceDelegator import com.android.systemui.scene.ui.composable.SceneTransitionLayoutDataSource @@ -98,6 +102,17 @@ val sceneTransitionsV2 = transitions { spec = tween(durationMillis = TO_GONE_DURATION.toInt(DurationUnit.MILLISECONDS)) fade(AllElements) } + to(CommunalScenes.Blank, key = CommunalTransitionKeys.SwipeInLandscape) { + spec = tween(durationMillis = TO_LOCKSCREEN_DURATION.toInt(DurationUnit.MILLISECONDS)) + translate(Communal.Elements.Grid, Edge.End) + timestampRange(endMillis = 167) { + fade(Communal.Elements.Grid) + fade(Communal.Elements.IndicationArea) + fade(Communal.Elements.LockIcon) + fade(Communal.Elements.StatusBar) + } + timestampRange(startMillis = 167, endMillis = 500) { fade(Communal.Elements.Scrim) } + } to(CommunalScenes.Blank, key = CommunalTransitionKeys.Swipe) { spec = tween(durationMillis = TransitionDuration.TO_GLANCEABLE_HUB_DURATION_MS) translate(Communal.Elements.Grid, Edge.End) @@ -214,6 +229,9 @@ fun CommunalContainer( val blurRadius = with(LocalDensity.current) { viewModel.blurRadiusPx.toDp() } + val swipeFromHubInLandscape by + viewModel.swipeFromHubInLandscape.collectAsStateWithLifecycle(false) + SceneTransitionLayout( state = state, modifier = modifier.fillMaxSize().thenIf(isUiBlurred) { Modifier.blur(blurRadius) }, @@ -241,7 +259,14 @@ fun CommunalContainer( userActions = mapOf( Swipe.End to - UserActionResult(CommunalScenes.Blank, CommunalTransitionKeys.Swipe) + UserActionResult( + CommunalScenes.Blank, + if (swipeFromHubInLandscape) { + CommunalTransitionKeys.SwipeInLandscape + } else { + CommunalTransitionKeys.Swipe + }, + ) ), ) { CommunalScene( @@ -258,6 +283,20 @@ fun CommunalContainer( Box(modifier = Modifier.fillMaxSize().allowGestures(touchesAllowed)) } +/** Listens to orientation changes on communal scene and reset when scene is disposed. */ +@Composable +fun ObserveOrientationChange(viewModel: CommunalViewModel) { + val configuration = LocalConfiguration.current + + LaunchedEffect(configuration.orientation) { + viewModel.onOrientationChange(configuration.orientation) + } + + DisposableEffect(Unit) { + onDispose { viewModel.onOrientationChange(Configuration.ORIENTATION_UNDEFINED) } + } +} + /** Scene containing the glanceable hub UI. */ @Composable fun ContentScope.CommunalScene( @@ -269,6 +308,8 @@ fun ContentScope.CommunalScene( ) { val isFocusable by viewModel.isFocusable.collectAsStateWithLifecycle(initialValue = false) + // Observe screen rotation while Communal Scene is active. + ObserveOrientationChange(viewModel) Box( modifier = Modifier.element(Communal.Elements.Scrim) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractorTest.kt index dc21f0692c9e..7bdac476641b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractorTest.kt @@ -16,6 +16,8 @@ package com.android.systemui.communal.domain.interactor +import android.content.res.Configuration.ORIENTATION_LANDSCAPE +import android.content.res.Configuration.ORIENTATION_PORTRAIT import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.FlagsParameterization @@ -33,11 +35,15 @@ import com.android.systemui.flags.andSceneContainer import com.android.systemui.kosmos.testScope import com.android.systemui.scene.initialSceneKey import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.statusbar.policy.KeyguardStateController +import com.android.systemui.statusbar.policy.keyguardStateController import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.runner.RunWith @@ -46,9 +52,11 @@ import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever import platform.test.runner.parameterized.ParameterizedAndroidJunit4 import platform.test.runner.parameterized.Parameters +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(ParameterizedAndroidJunit4::class) class CommunalSceneInteractorTest(flags: FlagsParameterization) : SysuiTestCase() { @@ -70,6 +78,7 @@ class CommunalSceneInteractorTest(flags: FlagsParameterization) : SysuiTestCase( private val repository = kosmos.communalSceneRepository private val underTest by lazy { kosmos.communalSceneInteractor } + private val keyguardStateController: KeyguardStateController = kosmos.keyguardStateController @DisableFlags(FLAG_SCENE_CONTAINER) @Test @@ -551,4 +560,57 @@ class CommunalSceneInteractorTest(flags: FlagsParameterization) : SysuiTestCase( transitionState.value = ObservableTransitionState.Idle(Scenes.Lockscreen) assertThat(isCommunalVisible).isEqualTo(false) } + + @Test + fun willRotateToPortrait_whenKeyguardRotationNotAllowed() = + testScope.runTest { + whenever(keyguardStateController.isKeyguardScreenRotationAllowed()).thenReturn(false) + val willRotateToPortrait by collectLastValue(underTest.willRotateToPortrait) + + repository.setCommunalContainerOrientation(ORIENTATION_LANDSCAPE) + runCurrent() + + assertThat(willRotateToPortrait).isEqualTo(true) + + repository.setCommunalContainerOrientation(ORIENTATION_PORTRAIT) + runCurrent() + + assertThat(willRotateToPortrait).isEqualTo(false) + } + + @Test + fun willRotateToPortrait_isFalse_whenKeyguardRotationIsAllowed() = + testScope.runTest { + whenever(keyguardStateController.isKeyguardScreenRotationAllowed()).thenReturn(true) + val willRotateToPortrait by collectLastValue(underTest.willRotateToPortrait) + + repository.setCommunalContainerOrientation(ORIENTATION_LANDSCAPE) + runCurrent() + + assertThat(willRotateToPortrait).isEqualTo(false) + + repository.setCommunalContainerOrientation(ORIENTATION_PORTRAIT) + runCurrent() + + assertThat(willRotateToPortrait).isEqualTo(false) + } + + @Test + fun rotatedToPortrait() = + testScope.runTest { + val rotatedToPortrait by collectLastValue(underTest.rotatedToPortrait) + assertThat(rotatedToPortrait).isEqualTo(false) + + repository.setCommunalContainerOrientation(ORIENTATION_PORTRAIT) + runCurrent() + assertThat(rotatedToPortrait).isEqualTo(false) + + repository.setCommunalContainerOrientation(ORIENTATION_LANDSCAPE) + runCurrent() + assertThat(rotatedToPortrait).isEqualTo(false) + + repository.setCommunalContainerOrientation(ORIENTATION_PORTRAIT) + runCurrent() + assertThat(rotatedToPortrait).isEqualTo(true) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModelTest.kt index 3ab920a46084..cdd093a410df 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModelTest.kt @@ -17,11 +17,20 @@ package com.android.systemui.keyguard.ui.viewmodel import android.content.res.Configuration +import android.content.res.mainResources +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.FlagsParameterization import android.util.LayoutDirection -import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.compose.animation.scene.ObservableTransitionState +import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_V2 import com.android.systemui.SysuiTestCase import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository +import com.android.systemui.communal.data.repository.communalSceneRepository +import com.android.systemui.communal.domain.interactor.communalSceneInteractor +import com.android.systemui.communal.domain.interactor.setCommunalV2ConfigEnabled +import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.flags.DisableSceneContainer import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository import com.android.systemui.keyguard.shared.model.KeyguardState @@ -29,30 +38,53 @@ import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.keyguard.ui.transitions.blurConfig import com.android.systemui.kosmos.collectValues +import com.android.systemui.kosmos.runCurrent import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.testScope import com.android.systemui.res.R +import com.android.systemui.statusbar.policy.KeyguardStateController +import com.android.systemui.statusbar.policy.keyguardStateController import com.android.systemui.testKosmos import com.google.common.collect.Range import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flowOf import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.mock import org.mockito.kotlin.whenever +import platform.test.runner.parameterized.ParameterizedAndroidJunit4 +import platform.test.runner.parameterized.Parameters @SmallTest -@RunWith(AndroidJUnit4::class) -class GlanceableHubToLockscreenTransitionViewModelTest : SysuiTestCase() { - val kosmos = testKosmos() +@RunWith(ParameterizedAndroidJunit4::class) +class GlanceableHubToLockscreenTransitionViewModelTest(flags: FlagsParameterization) : + SysuiTestCase() { + val kosmos = testKosmos().apply { mainResources = mContext.orCreateTestableResources.resources } val testScope = kosmos.testScope val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository val configurationRepository = kosmos.fakeConfigurationRepository + val keyguardStateController: KeyguardStateController = kosmos.keyguardStateController val underTest by lazy { kosmos.glanceableHubToLockscreenTransitionViewModel } + companion object { + @JvmStatic + @Parameters(name = "{0}") + fun getParams(): List<FlagsParameterization> { + return FlagsParameterization.allCombinationsOf(FLAG_GLANCEABLE_HUB_V2) + } + } + + init { + mSetFlagsRule.setFlagsParameterization(flags) + } + @Test fun lockscreenFadeIn() = kosmos.runTest { + communalSceneInteractor.changeScene(CommunalScenes.Communal, "test") + val values by collectValues(underTest.keyguardAlpha) assertThat(values).isEmpty() @@ -79,6 +111,116 @@ class GlanceableHubToLockscreenTransitionViewModelTest : SysuiTestCase() { } @Test + @EnableFlags(FLAG_GLANCEABLE_HUB_V2) + fun lockscreenFadeIn_fromHubInLandscape() = + kosmos.runTest { + kosmos.setCommunalV2ConfigEnabled(true) + whenever(keyguardStateController.isKeyguardScreenRotationAllowed).thenReturn(false) + communalSceneInteractor.changeScene(CommunalScenes.Communal, "test") + communalSceneRepository.setCommunalContainerOrientation( + Configuration.ORIENTATION_LANDSCAPE + ) + + val values by collectValues(underTest.keyguardAlpha) + assertThat(values).isEmpty() + + // Exit hub to lockscreen + val progress = MutableStateFlow(0f) + val transitionState = + MutableStateFlow( + ObservableTransitionState.Transition( + fromScene = CommunalScenes.Communal, + toScene = CommunalScenes.Blank, + currentScene = flowOf(CommunalScenes.Blank), + progress = progress, + isInitiatedByUserInput = false, + isUserInputOngoing = flowOf(false), + ) + ) + communalSceneInteractor.setTransitionState(transitionState) + progress.value = .2f + + // Still in landscape + keyguardTransitionRepository.sendTransitionSteps( + listOf( + step(0f, TransitionState.STARTED), + step(0.1f), + // start here.. + step(0.5f), + ), + testScope, + ) + + // Communal container is rotated to portrait + communalSceneRepository.setCommunalContainerOrientation( + Configuration.ORIENTATION_PORTRAIT + ) + runCurrent() + + keyguardTransitionRepository.sendTransitionSteps( + listOf( + step(0.6f), + step(0.7f), + // should stop here.. + step(0.8f), + step(1f), + ), + testScope, + ) + // Scene transition finished. + progress.value = 1f + keyguardTransitionRepository.sendTransitionSteps( + listOf(step(1f, TransitionState.FINISHED)), + testScope, + ) + + assertThat(values).hasSize(4) + // onStart + assertThat(values[0]).isEqualTo(0f) + assertThat(values[1]).isEqualTo(0f) + assertThat(values[2]).isEqualTo(1f) + // onFinish + assertThat(values[3]).isEqualTo(1f) + } + + @Test + @DisableFlags(FLAG_GLANCEABLE_HUB_V2) + fun lockscreenFadeIn_v2FlagDisabledAndFromHubInLandscape() = + kosmos.runTest { + whenever(keyguardStateController.isKeyguardScreenRotationAllowed).thenReturn(false) + communalSceneInteractor.changeScene(CommunalScenes.Communal, "test") + // Rotation is not enabled so communal container is in portrait. + communalSceneRepository.setCommunalContainerOrientation( + Configuration.ORIENTATION_PORTRAIT + ) + + val values by collectValues(underTest.keyguardAlpha) + assertThat(values).isEmpty() + + // Exit hub to lockscreen + keyguardTransitionRepository.sendTransitionSteps( + listOf( + step(0f, TransitionState.STARTED), + // Should start running here... + step(0.1f), + step(0.2f), + step(0.3f), + step(0.4f), + // ...up to here + step(0.5f), + step(0.6f), + step(0.7f), + step(0.8f), + step(1f), + ), + testScope, + ) + + assertThat(values).hasSize(4) + values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) } + } + + @Test fun lockscreenTranslationX() = kosmos.runTest { val config: Configuration = mock() @@ -89,6 +231,8 @@ class GlanceableHubToLockscreenTransitionViewModelTest : SysuiTestCase() { R.dimen.hub_to_lockscreen_transition_lockscreen_translation_x, 100, ) + communalSceneInteractor.changeScene(CommunalScenes.Communal, "test") + val values by collectValues(underTest.keyguardTranslationX) assertThat(values).isEmpty() @@ -108,6 +252,44 @@ class GlanceableHubToLockscreenTransitionViewModelTest : SysuiTestCase() { } @Test + @EnableFlags(FLAG_GLANCEABLE_HUB_V2) + fun lockscreenTranslationX_fromHubInLandscape() = + kosmos.runTest { + kosmos.setCommunalV2ConfigEnabled(true) + val config: Configuration = mock() + whenever(config.layoutDirection).thenReturn(LayoutDirection.LTR) + configurationRepository.onConfigurationChange(config) + + configurationRepository.setDimensionPixelSize( + R.dimen.hub_to_lockscreen_transition_lockscreen_translation_x, + 100, + ) + whenever(keyguardStateController.isKeyguardScreenRotationAllowed).thenReturn(false) + + communalSceneInteractor.changeScene(CommunalScenes.Communal, "test") + communalSceneRepository.setCommunalContainerOrientation( + Configuration.ORIENTATION_LANDSCAPE + ) + + val values by collectValues(underTest.keyguardTranslationX) + assertThat(values).isEmpty() + + keyguardTransitionRepository.sendTransitionSteps( + listOf( + step(0f, TransitionState.STARTED), + step(0.3f), + step(0.5f), + step(0.7f), + step(1f), + step(1f, TransitionState.FINISHED), + ), + testScope, + ) + // no translation-x animation + values.forEach { assertThat(it.value).isEqualTo(0f) } + } + + @Test fun lockscreenTranslationX_resetsAfterCancellation() = kosmos.runTest { val config: Configuration = mock() @@ -118,6 +300,9 @@ class GlanceableHubToLockscreenTransitionViewModelTest : SysuiTestCase() { R.dimen.hub_to_lockscreen_transition_lockscreen_translation_x, 100, ) + + communalSceneInteractor.changeScene(CommunalScenes.Communal, "test") + val values by collectValues(underTest.keyguardTranslationX) assertThat(values).isEmpty() @@ -137,6 +322,42 @@ class GlanceableHubToLockscreenTransitionViewModelTest : SysuiTestCase() { } @Test + @EnableFlags(FLAG_GLANCEABLE_HUB_V2) + fun lockscreenTranslationX_resetsAfterCancellation_fromHubInLandscape() = + kosmos.runTest { + kosmos.setCommunalV2ConfigEnabled(true) + val config: Configuration = mock() + whenever(config.layoutDirection).thenReturn(LayoutDirection.LTR) + configurationRepository.onConfigurationChange(config) + + configurationRepository.setDimensionPixelSize( + R.dimen.hub_to_lockscreen_transition_lockscreen_translation_x, + 100, + ) + whenever(keyguardStateController.isKeyguardScreenRotationAllowed).thenReturn(false) + + communalSceneInteractor.changeScene(CommunalScenes.Communal, "test") + communalSceneRepository.setCommunalContainerOrientation( + Configuration.ORIENTATION_LANDSCAPE + ) + + val values by collectValues(underTest.keyguardTranslationX) + assertThat(values).isEmpty() + + keyguardTransitionRepository.sendTransitionSteps( + listOf( + step(0f, TransitionState.STARTED), + step(0.3f), + step(0.6f), + step(0.9f, TransitionState.CANCELED), + ), + testScope, + ) + // no translation-x animation + values.forEach { assertThat(it.value).isEqualTo(0f) } + } + + @Test @DisableSceneContainer fun blurBecomesMinValueImmediately() = kosmos.runTest { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt index fe213a6ebbf0..71e09d982494 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt @@ -17,12 +17,19 @@ package com.android.systemui.keyguard.ui.viewmodel +import android.content.res.Configuration +import android.content.res.mainResources +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.FlagsParameterization import android.view.View import androidx.test.filters.SmallTest import com.android.compose.animation.scene.ObservableTransitionState +import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_V2 import com.android.systemui.SysuiTestCase import com.android.systemui.communal.data.repository.communalSceneRepository +import com.android.systemui.communal.domain.interactor.communalSceneInteractor +import com.android.systemui.communal.domain.interactor.setCommunalV2ConfigEnabled import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.coroutines.collectLastValue import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository @@ -35,6 +42,9 @@ import com.android.systemui.keyguard.domain.interactor.pulseExpansionInteractor import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep +import com.android.systemui.kosmos.collectLastValue +import com.android.systemui.kosmos.runCurrent +import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.testScope import com.android.systemui.scene.data.repository.Idle import com.android.systemui.scene.data.repository.sceneContainerRepository @@ -48,6 +58,7 @@ import com.android.systemui.statusbar.notification.data.repository.activeNotific import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationsKeyguardInteractor import com.android.systemui.statusbar.phone.dozeParameters import com.android.systemui.statusbar.phone.screenOffAnimationController +import com.android.systemui.statusbar.policy.keyguardStateController import com.android.systemui.testKosmos import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever @@ -69,7 +80,8 @@ import platform.test.runner.parameterized.Parameters @SmallTest @RunWith(ParameterizedAndroidJunit4::class) class KeyguardRootViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { - private val kosmos = testKosmos() + private val kosmos = + testKosmos().apply { mainResources = mContext.orCreateTestableResources.resources } private val testScope = kosmos.testScope private val keyguardTransitionRepository by lazy { kosmos.fakeKeyguardTransitionRepository } private val keyguardRepository by lazy { kosmos.fakeKeyguardRepository } @@ -419,6 +431,7 @@ class KeyguardRootViewModelTest(flags: FlagsParameterization) : SysuiTestCase() } @Test + @DisableFlags(FLAG_GLANCEABLE_HUB_V2) fun alpha_transitionFromHubToLockscreen_isOne() = testScope.runTest { val alpha by collectLastValue(underTest.alpha(viewState)) @@ -439,6 +452,84 @@ class KeyguardRootViewModelTest(flags: FlagsParameterization) : SysuiTestCase() } @Test + @DisableSceneContainer + @EnableFlags(FLAG_GLANCEABLE_HUB_V2) + fun alpha_transitionFromHubToLockscreenInLandscape_isOne() = + kosmos.runTest { + setCommunalV2ConfigEnabled(true) + whenever(keyguardStateController.isKeyguardScreenRotationAllowed).thenReturn(false) + communalSceneInteractor.changeScene(CommunalScenes.Communal, "test") + communalSceneRepository.setCommunalContainerOrientation( + Configuration.ORIENTATION_LANDSCAPE + ) + + val alpha by collectLastValue(underTest.alpha(viewState)) + + // Transition to the glanceable hub and back. + keyguardTransitionRepository.sendTransitionSteps( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GLANCEABLE_HUB, + testScope, + ) + + communalSceneInteractor.changeScene(CommunalScenes.Communal, "test") + runCurrent() + + // Exit hub to lockscreen + val progress = MutableStateFlow(0f) + val transitionState = + MutableStateFlow( + ObservableTransitionState.Transition( + fromScene = CommunalScenes.Communal, + toScene = CommunalScenes.Blank, + currentScene = flowOf(CommunalScenes.Blank), + progress = progress, + isInitiatedByUserInput = false, + isUserInputOngoing = flowOf(false), + ) + ) + communalSceneInteractor.setTransitionState(transitionState) + progress.value = .4f + + keyguardTransitionRepository.sendTransitionSteps( + listOf( + TransitionStep( + from = KeyguardState.GLANCEABLE_HUB, + to = KeyguardState.LOCKSCREEN, + transitionState = TransitionState.STARTED, + value = 0f, + ), + TransitionStep( + from = KeyguardState.GLANCEABLE_HUB, + to = KeyguardState.LOCKSCREEN, + transitionState = TransitionState.RUNNING, + value = 0.4f, + ), + ), + testScope, + ) + + communalSceneRepository.setCommunalContainerOrientation( + Configuration.ORIENTATION_PORTRAIT + ) + runCurrent() + + keyguardTransitionRepository.sendTransitionSteps( + listOf( + TransitionStep( + from = KeyguardState.GLANCEABLE_HUB, + to = KeyguardState.LOCKSCREEN, + transitionState = TransitionState.FINISHED, + value = 1f, + ) + ), + testScope, + ) + + assertThat(alpha).isEqualTo(1.0f) + } + + @Test fun alpha_emitsOnShadeExpansion() = testScope.runTest { val alpha by collectLastValue(underTest.alpha(viewState)) diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneRepository.kt index bf4445ba18db..2b8cf008c0c7 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneRepository.kt @@ -16,6 +16,7 @@ package com.android.systemui.communal.data.repository +import android.content.res.Configuration import com.android.app.tracing.coroutines.launchTraced as launch import com.android.compose.animation.scene.ObservableTransitionState import com.android.compose.animation.scene.OverlayKey @@ -49,6 +50,9 @@ interface CommunalSceneRepository { /** Exposes the transition state of the communal [SceneTransitionLayout]. */ val transitionState: StateFlow<ObservableTransitionState> + /** Current orientation of the communal container. */ + val communalContainerOrientation: StateFlow<Int> + /** Updates the requested scene. */ fun changeScene(toScene: SceneKey, transitionKey: TransitionKey? = null) @@ -64,6 +68,9 @@ interface CommunalSceneRepository { * Note that you must call is with `null` when the UI is done or risk a memory leak. */ fun setTransitionState(transitionState: Flow<ObservableTransitionState>?) + + /** Set the current orientation of the communal container. */ + fun setCommunalContainerOrientation(orientation: Int) } @SysUISingleton @@ -89,6 +96,11 @@ constructor( initialValue = defaultTransitionState, ) + private val _communalContainerOrientation = + MutableStateFlow(Configuration.ORIENTATION_UNDEFINED) + override val communalContainerOrientation: StateFlow<Int> = + _communalContainerOrientation.asStateFlow() + override fun changeScene(toScene: SceneKey, transitionKey: TransitionKey?) { applicationScope.launch { // SceneTransitionLayout state updates must be triggered on the thread the STL was @@ -105,6 +117,10 @@ constructor( } } + override fun setCommunalContainerOrientation(orientation: Int) { + _communalContainerOrientation.value = orientation + } + 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] diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractor.kt index fed99d71fa3b..a112dd25e006 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractor.kt @@ -16,6 +16,7 @@ package com.android.systemui.communal.domain.interactor +import android.content.res.Configuration import com.android.app.tracing.coroutines.launchTraced as launch import com.android.compose.animation.scene.ObservableTransitionState import com.android.compose.animation.scene.SceneKey @@ -32,6 +33,7 @@ import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf import com.android.systemui.util.kotlin.pairwiseBy import javax.inject.Inject @@ -58,6 +60,7 @@ constructor( private val repository: CommunalSceneRepository, private val logger: CommunalSceneLogger, private val sceneInteractor: SceneInteractor, + private val keyguardStateController: KeyguardStateController, ) { private val _isLaunchingWidget = MutableStateFlow(false) @@ -68,6 +71,30 @@ constructor( _isLaunchingWidget.value = launching } + /** + * Whether screen will be rotated to portrait if transitioned out of hub to keyguard screens. + */ + var willRotateToPortrait: Flow<Boolean> = + repository.communalContainerOrientation + .map { + it == Configuration.ORIENTATION_LANDSCAPE && + !keyguardStateController.isKeyguardScreenRotationAllowed() + } + .distinctUntilChanged() + + /** Whether communal container is rotated to portrait. Emits an initial value of false. */ + val rotatedToPortrait: StateFlow<Boolean> = + repository.communalContainerOrientation + .pairwiseBy(initialValue = false) { old, new -> + old == Configuration.ORIENTATION_LANDSCAPE && + new == Configuration.ORIENTATION_PORTRAIT + } + .stateIn(applicationScope, SharingStarted.Eagerly, false) + + fun setCommunalContainerOrientation(orientation: Int) { + repository.setCommunalContainerOrientation(orientation) + } + fun interface OnSceneAboutToChangeListener { /** Notifies that the scene is about to change to [toScene]. */ fun onSceneAboutToChange(toScene: SceneKey, keyguardState: KeyguardState?) diff --git a/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalTransitionKeys.kt b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalTransitionKeys.kt index a84c45732169..49dc59ac0004 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalTransitionKeys.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalTransitionKeys.kt @@ -33,4 +33,6 @@ object CommunalTransitionKeys { val FromEditMode = TransitionKey("FromEditMode") /** Swipes the glanceable hub in/out of view */ val Swipe = TransitionKey("Swipe") + /** Swipes out of glanceable hub in landscape orientation */ + val SwipeInLandscape = TransitionKey("SwipeInLandscape") } diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt index 5a4b0b0e2d24..a6309d1be03d 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt @@ -386,6 +386,11 @@ constructor( } } + val swipeFromHubInLandscape: Flow<Boolean> = communalSceneInteractor.willRotateToPortrait + + fun onOrientationChange(orientation: Int) = + communalSceneInteractor.setCommunalContainerOrientation(orientation) + companion object { const val POPUP_AUTO_HIDE_TIMEOUT_MS = 12000L } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt index 3ad862b761fc..be0cf62b0526 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt @@ -251,6 +251,8 @@ constructor( * Set at 400ms for parity with [FromLockscreenTransitionInteractor] */ val DEFAULT_DURATION = 400.milliseconds + // To lockscreen duration must be at least 500ms to allow for potential screen rotation + // during the transition while the animation begins after 500ms. val TO_LOCKSCREEN_DURATION = 1.seconds val TO_BOUNCER_DURATION = 400.milliseconds val TO_OCCLUDED_DURATION = 450.milliseconds diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt index bcbe66642d11..fd5783ef7f8e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt @@ -19,7 +19,10 @@ package com.android.systemui.keyguard.ui.viewmodel import android.util.LayoutDirection import com.android.app.animation.Interpolators.EMPHASIZED import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor +import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor +import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.dagger.GlanceableHubBlurComponent import com.android.systemui.keyguard.domain.interactor.FromGlanceableHubTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION import com.android.systemui.keyguard.shared.model.Edge @@ -34,21 +37,32 @@ import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.shade.ShadeDisplayAware import javax.inject.Inject import kotlin.time.Duration.Companion.milliseconds +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combineTransform import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn /** * Breaks down GLANCEABLE_HUB->LOCKSCREEN transition into discrete steps for corresponding views to * consume. */ +@OptIn(ExperimentalCoroutinesApi::class) @SysUISingleton class GlanceableHubToLockscreenTransitionViewModel @Inject constructor( + @Application applicationScope: CoroutineScope, @ShadeDisplayAware configurationInteractor: ConfigurationInteractor, animationFlow: KeyguardTransitionAnimationFlow, + communalSceneInteractor: CommunalSceneInteractor, + communalSettingsInteractor: CommunalSettingsInteractor, private val blurFactory: GlanceableHubBlurComponent.Factory, ) : GlanceableHubTransition, DeviceEntryIconTransition { private val transitionAnimation = @@ -59,18 +73,45 @@ constructor( ) .setupWithoutSceneContainer(edge = Edge.create(from = GLANCEABLE_HUB, to = LOCKSCREEN)) + // Whether screen rotation will happen with the transition. Only emit when idle so ongoing + // animation won't be interrupted when orientation is updated during the transition. + private val willRotateToPortraitInTransition: StateFlow<Boolean> = + if (!communalSettingsInteractor.isV2FlagEnabled()) { + flowOf(false) + } else { + communalSceneInteractor.isIdleOnCommunal.combineTransform( + communalSceneInteractor.willRotateToPortrait + ) { isIdle, willRotate -> + if (isIdle) emit(willRotate) + } + } + .stateIn(applicationScope, SharingStarted.Eagerly, false) + override val windowBlurRadius: Flow<Float> = blurFactory.create(transitionAnimation).getBlurProvider().exitBlurRadius val keyguardAlpha: Flow<Float> = - transitionAnimation.sharedFlow( - duration = 167.milliseconds, - startTime = 167.milliseconds, - onStep = { it }, - onFinish = { 1f }, - onCancel = { 0f }, - name = "GLANCEABLE_HUB->LOCKSCREEN: keyguardAlpha", - ) + willRotateToPortraitInTransition.flatMapLatest { willRotate -> + transitionAnimation.sharedFlow( + duration = 167.milliseconds, + // If will rotate, start later to leave time for screen rotation. + startTime = if (willRotate) 500.milliseconds else 167.milliseconds, + onStep = { step -> + if (willRotate) { + if (!communalSceneInteractor.rotatedToPortrait.value) { + 0f + } else { + 1f + } + } else { + step + } + }, + onFinish = { 1f }, + onCancel = { 0f }, + name = "GLANCEABLE_HUB->LOCKSCREEN: keyguardAlpha", + ) + } // Show UMO as long as keyguard is not visible. val showUmo: Flow<Boolean> = keyguardAlpha.map { alpha -> alpha == 0f } @@ -84,7 +125,14 @@ constructor( .flatMapLatest { translatePx: Int -> transitionAnimation.sharedFlowWithState( duration = TO_LOCKSCREEN_DURATION, - onStep = { value -> -translatePx + value * translatePx }, + onStep = { value -> + // do not animate translation-x if screen rotation will happen + if (willRotateToPortraitInTransition.value) { + 0f + } else { + -translatePx + value * translatePx + } + }, interpolator = EMPHASIZED, // Move notifications back to their original position since they can be // accessed from the shade, and also keyguard elements in case the animation diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalSceneRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalSceneRepository.kt index 3f35bb9f3520..38e6c8a0cdea 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalSceneRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalSceneRepository.kt @@ -1,5 +1,6 @@ package com.android.systemui.communal.data.repository +import android.content.res.Configuration import com.android.compose.animation.scene.ObservableTransitionState import com.android.compose.animation.scene.SceneKey import com.android.compose.animation.scene.TransitionKey @@ -9,6 +10,7 @@ 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 @@ -48,4 +50,13 @@ class FakeCommunalSceneRepository( override fun setTransitionState(transitionState: Flow<ObservableTransitionState>?) { _transitionState.value = transitionState } + + private val _communalContainerOrientation = + MutableStateFlow(Configuration.ORIENTATION_UNDEFINED) + override val communalContainerOrientation: StateFlow<Int> = + _communalContainerOrientation.asStateFlow() + + override fun setCommunalContainerOrientation(orientation: Int) { + _communalContainerOrientation.value = orientation + } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractorKosmos.kt index 209d1636e380..8834af581e73 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractorKosmos.kt @@ -21,6 +21,7 @@ import com.android.systemui.communal.shared.log.communalSceneLogger import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.scene.domain.interactor.sceneInteractor +import com.android.systemui.statusbar.policy.keyguardStateController val Kosmos.communalSceneInteractor: CommunalSceneInteractor by Kosmos.Fixture { @@ -29,5 +30,6 @@ val Kosmos.communalSceneInteractor: CommunalSceneInteractor by repository = communalSceneRepository, logger = communalSceneLogger, sceneInteractor = sceneInteractor, + keyguardStateController = keyguardStateController, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModelKosmos.kt index 530981c489e8..02e63a42d87d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModelKosmos.kt @@ -17,15 +17,21 @@ package com.android.systemui.keyguard.ui.viewmodel import com.android.systemui.common.ui.domain.interactor.configurationInteractor +import com.android.systemui.communal.domain.interactor.communalSceneInteractor +import com.android.systemui.communal.domain.interactor.communalSettingsInteractor import com.android.systemui.keyguard.ui.glanceableHubBlurComponentFactory import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.kosmos.applicationCoroutineScope val Kosmos.glanceableHubToLockscreenTransitionViewModel by Fixture { GlanceableHubToLockscreenTransitionViewModel( + applicationScope = applicationCoroutineScope, configurationInteractor = configurationInteractor, animationFlow = keyguardTransitionAnimationFlow, + communalSceneInteractor = communalSceneInteractor, + communalSettingsInteractor = communalSettingsInteractor, blurFactory = glanceableHubBlurComponentFactory, ) } |