From af2113a228af8ddf68b4c482a7bcd88c33c2fb8f Mon Sep 17 00:00:00 2001 From: William Xiao Date: Tue, 28 Jan 2025 15:25:24 -0800 Subject: Resolve circular dependency This resolves circular dependencies between keyguard and commuanl code due to KeyguardUpdateMonitor's dependency on CommunalSceneInteractor. Bug: 378170118 Test: local build Flag: com.android.systemui.glanceable_hub_v2 Change-Id: Iaa9f674374998844286948c323255139121ca296 --- .../SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java | 6 +++--- .../tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java | 3 +-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java index c266a5b47cff..0b66a0ffb711 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java @@ -296,7 +296,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab private final Provider mJavaAdapter; private final Provider mSceneInteractor; private final Provider mAlternateBouncerInteractor; - private final CommunalSceneInteractor mCommunalSceneInteractor; + private final Provider mCommunalSceneInteractor; private final AuthController mAuthController; private final UiEventLogger mUiEventLogger; private final Set mAllowFingerprintOnOccludingActivitiesFromPackage; @@ -2210,7 +2210,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab Provider alternateBouncerInteractor, Provider javaAdapter, Provider sceneInteractor, - CommunalSceneInteractor communalSceneInteractor) { + Provider communalSceneInteractor) { mContext = context; mSubscriptionManager = subscriptionManager; mUserTracker = userTracker; @@ -2543,7 +2543,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab if (glanceableHubV2()) { mJavaAdapter.get().alwaysCollectFlow( - mCommunalSceneInteractor.isCommunalVisible(), + mCommunalSceneInteractor.get().isCommunalVisible(), this::onCommunalShowingChanged ); } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java index 312d2ffd74e4..4110a05170b3 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java @@ -18,7 +18,6 @@ package com.android.keyguard; import static android.app.StatusBarManager.SESSION_KEYGUARD; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; -import static android.hardware.biometrics.BiometricAuthenticator.TYPE_ANY_BIOMETRIC; import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE; import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT; import static android.hardware.biometrics.BiometricFaceConstants.FACE_ERROR_LOCKOUT_PERMANENT; @@ -2717,7 +2716,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { () -> mAlternateBouncerInteractor, () -> mJavaAdapter, () -> mSceneInteractor, - mCommunalSceneInteractor); + () -> mCommunalSceneInteractor); setAlternateBouncerVisibility(false); setPrimaryBouncerVisibility(false); setStrongAuthTracker(KeyguardUpdateMonitorTest.this.mStrongAuthTracker); -- cgit v1.2.3-59-g8ed1b From b3d5a84ac5bf4c3a346a1d266c425053b33346cc Mon Sep 17 00:00:00 2001 From: William Xiao Date: Tue, 21 Jan 2025 16:52:45 -0800 Subject: 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 --- .../communal/ui/compose/CommunalContainer.kt | 3 +- .../data/repository/CommunalRepositoryImplTest.kt | 83 ---------------- .../repository/CommunalSceneRepositoryImplTest.kt | 107 +++++++++++++++++++++ .../data/repository/CommunalSceneRepository.kt | 44 ++++++++- .../domain/interactor/CommunalSceneInteractor.kt | 23 +++++ .../communal/ui/viewmodel/BaseCommunalViewModel.kt | 2 +- .../data/repository/FakeCommunalSceneRepository.kt | 7 +- 7 files changed, 180 insertions(+), 89 deletions(-) delete mode 100644 packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalRepositoryImplTest.kt create mode 100644 packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSceneRepositoryImplTest.kt 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 3ffbabb09710..4a4607b6e8fc 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 @@ -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( 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 deleted file mode 100644 index fd0bf4dae198..000000000000 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalRepositoryImplTest.kt +++ /dev/null @@ -1,83 +0,0 @@ -/* - * 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.data.repository - -import androidx.test.ext.junit.runners.AndroidJUnit4 -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.kosmos.applicationCoroutineScope -import com.android.systemui.kosmos.testScope -import com.android.systemui.scene.shared.model.sceneDataSource -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 - -@SmallTest -@RunWith(AndroidJUnit4::class) -class CommunalRepositoryImplTest : SysuiTestCase() { - - private val kosmos = testKosmos() - private val testScope = kosmos.testScope - private val underTest by lazy { - CommunalSceneRepositoryImpl( - kosmos.applicationCoroutineScope, - kosmos.applicationCoroutineScope, - kosmos.sceneDataSource, - ) - } - - @Test - fun transitionState_idleByDefault() = - testScope.runTest { - val transitionState by collectLastValue(underTest.transitionState) - assertThat(transitionState) - .isEqualTo(ObservableTransitionState.Idle(CommunalScenes.Default)) - } - - @Test - fun transitionState_setTransitionState_returnsNewValue() = - testScope.runTest { - val expectedSceneKey = CommunalScenes.Communal - underTest.setTransitionState(flowOf(ObservableTransitionState.Idle(expectedSceneKey))) - - val transitionState by collectLastValue(underTest.transitionState) - assertThat(transitionState).isEqualTo(ObservableTransitionState.Idle(expectedSceneKey)) - } - - @Test - fun transitionState_setNullTransitionState_returnsDefaultValue() = - testScope.runTest { - // Set a value for the transition state flow. - underTest.setTransitionState( - flowOf(ObservableTransitionState.Idle(CommunalScenes.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(ObservableTransitionState.Idle(CommunalScenes.Default)) - } -} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSceneRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSceneRepositoryImplTest.kt new file mode 100644 index 000000000000..293d32471713 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSceneRepositoryImplTest.kt @@ -0,0 +1,107 @@ +/* + * 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. + * 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.data.repository + +import androidx.test.ext.junit.runners.AndroidJUnit4 +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.keyguard.data.repository.fakeKeyguardRepository +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope +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 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 CommunalSceneRepositoryImplTest : SysuiTestCase() { + private val kosmos = testKosmos().useUnconfinedTestDispatcher() + + private val delegator = mock {} + + private val Kosmos.underTest by + Kosmos.Fixture { + CommunalSceneRepositoryImpl( + applicationScope = applicationCoroutineScope, + backgroundScope = backgroundScope, + sceneDataSource = delegator, + delegator = delegator, + ) + } + + @Test + fun transitionState_idleByDefault() = + kosmos.runTest { + val transitionState by collectLastValue(underTest.transitionState) + assertThat(transitionState) + .isEqualTo(ObservableTransitionState.Idle(CommunalScenes.Default)) + } + + @Test + fun transitionState_setTransitionState_returnsNewValue() = + kosmos.runTest { + val expectedSceneKey = CommunalScenes.Communal + underTest.setTransitionState(flowOf(ObservableTransitionState.Idle(expectedSceneKey))) + + val transitionState by collectLastValue(underTest.transitionState) + assertThat(transitionState).isEqualTo(ObservableTransitionState.Idle(expectedSceneKey)) + } + + @Test + fun transitionState_setNullTransitionState_returnsDefaultValue() = + kosmos.runTest { + // Set a value for the transition state flow. + underTest.setTransitionState( + flowOf(ObservableTransitionState.Idle(CommunalScenes.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(ObservableTransitionState.Idle(CommunalScenes.Default)) + } + + @Test + fun showHubFromPowerButton() = + kosmos.runTest { + fakeKeyguardRepository.setKeyguardShowing(false) + + underTest.showHubFromPowerButton() + + argumentCaptor().apply { + verify(delegator).setDelegate(capture()) + + assertThat(firstValue.currentScene.value).isEqualTo(CommunalScenes.Communal) + } + } +} 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 643d185f1939..8b6322720118 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,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 = 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?) { _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 = + MutableStateFlow(initialSceneKey).asStateFlow() + + override val currentOverlays: StateFlow> = + MutableStateFlow(emptySet()).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 + } } 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 476493225857..3d9e93036dbc 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 @@ -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) } } 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 099a85926020..49003a735fbd 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 @@ -45,7 +45,7 @@ abstract class BaseCommunalViewModel( val mediaHost: MediaHost, val mediaCarouselController: MediaCarouselController, ) { - val currentScene: Flow = communalSceneInteractor.currentScene + val currentScene: StateFlow = communalSceneInteractor.currentScene /** Used to animate showing or hiding the communal content. */ open val isCommunalContentVisible: Flow = MutableStateFlow(false) 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 b3c1411243c1..3f35bb9f3520 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 @@ -17,8 +17,7 @@ import kotlinx.coroutines.launch /** Fake implementation of [CommunalSceneRepository]. */ class FakeCommunalSceneRepository( private val applicationScope: CoroutineScope, - override val currentScene: MutableStateFlow = - MutableStateFlow(CommunalScenes.Default), + override val currentScene: MutableStateFlow = MutableStateFlow(CommunalScenes.Default), ) : CommunalSceneRepository { override fun changeScene(toScene: SceneKey, transitionKey: TransitionKey?) = @@ -31,6 +30,10 @@ class FakeCommunalSceneRepository( } } + override suspend fun showHubFromPowerButton() { + snapToScene(CommunalScenes.Communal) + } + private val defaultTransitionState = ObservableTransitionState.Idle(CommunalScenes.Default) private val _transitionState = MutableStateFlow?>(null) override val transitionState: StateFlow = -- cgit v1.2.3-59-g8ed1b