Merge "Automatically navigate between communal scenes." into main
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt
new file mode 100644
index 0000000..a8fe16b
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt
@@ -0,0 +1,252 @@
+/*
+ * Copyright (C) 2024 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
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.communal.domain.interactor.communalInteractor
+import com.android.systemui.communal.shared.model.CommunalSceneKey
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.dock.DockManager
+import com.android.systemui.dock.dockManager
+import com.android.systemui.dock.fakeDockManager
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceTimeBy
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class CommunalSceneStartableTest : SysuiTestCase() {
+ private val kosmos = testKosmos()
+
+ private lateinit var underTest: CommunalSceneStartable
+
+ @Before
+ fun setUp() =
+ with(kosmos) {
+ underTest =
+ CommunalSceneStartable(
+ dockManager = dockManager,
+ communalInteractor = communalInteractor,
+ keyguardTransitionInteractor = keyguardTransitionInteractor,
+ applicationScope = applicationCoroutineScope,
+ bgScope = applicationCoroutineScope,
+ )
+ .apply { start() }
+ }
+
+ @Test
+ fun keyguardGoesAway_forceBlankScene() =
+ with(kosmos) {
+ testScope.runTest {
+ val scene by collectLastValue(communalInteractor.desiredScene)
+
+ communalInteractor.onSceneChanged(CommunalSceneKey.Communal)
+ assertThat(scene).isEqualTo(CommunalSceneKey.Communal)
+
+ fakeKeyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.PRIMARY_BOUNCER,
+ to = KeyguardState.GONE,
+ testScope = this
+ )
+
+ assertThat(scene).isEqualTo(CommunalSceneKey.Blank)
+ }
+ }
+
+ @Test
+ fun deviceDreaming_forceBlankScene() =
+ with(kosmos) {
+ testScope.runTest {
+ val scene by collectLastValue(communalInteractor.desiredScene)
+
+ communalInteractor.onSceneChanged(CommunalSceneKey.Communal)
+ assertThat(scene).isEqualTo(CommunalSceneKey.Communal)
+
+ fakeKeyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.GLANCEABLE_HUB,
+ to = KeyguardState.DREAMING,
+ testScope = this
+ )
+
+ assertThat(scene).isEqualTo(CommunalSceneKey.Blank)
+ }
+ }
+
+ @Test
+ fun deviceDocked_forceCommunalScene() =
+ with(kosmos) {
+ testScope.runTest {
+ val scene by collectLastValue(communalInteractor.desiredScene)
+ assertThat(scene).isEqualTo(CommunalSceneKey.Blank)
+
+ updateDocked(true)
+ fakeKeyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.GONE,
+ to = KeyguardState.LOCKSCREEN,
+ testScope = this
+ )
+ assertThat(scene).isEqualTo(CommunalSceneKey.Communal)
+
+ fakeKeyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.GLANCEABLE_HUB,
+ to = KeyguardState.DREAMING,
+ testScope = this
+ )
+ assertThat(scene).isEqualTo(CommunalSceneKey.Blank)
+ }
+ }
+
+ @Test
+ fun deviceDocked_doesNotForceCommunalIfTransitioningFromCommunal() =
+ with(kosmos) {
+ testScope.runTest {
+ val scene by collectLastValue(communalInteractor.desiredScene)
+ assertThat(scene).isEqualTo(CommunalSceneKey.Blank)
+
+ updateDocked(true)
+ fakeKeyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.GLANCEABLE_HUB,
+ to = KeyguardState.LOCKSCREEN,
+ testScope = this
+ )
+ assertThat(scene).isEqualTo(CommunalSceneKey.Blank)
+ }
+ }
+
+ @Test
+ fun deviceAsleep_forceBlankSceneAfterTimeout() =
+ with(kosmos) {
+ testScope.runTest {
+ val scene by collectLastValue(communalInteractor.desiredScene)
+ communalInteractor.onSceneChanged(CommunalSceneKey.Communal)
+ assertThat(scene).isEqualTo(CommunalSceneKey.Communal)
+
+ fakeKeyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.GLANCEABLE_HUB,
+ to = KeyguardState.OFF,
+ testScope = this
+ )
+ assertThat(scene).isEqualTo(CommunalSceneKey.Communal)
+
+ advanceTimeBy(CommunalSceneStartable.AWAKE_DEBOUNCE_DELAY)
+
+ assertThat(scene).isEqualTo(CommunalSceneKey.Blank)
+ }
+ }
+
+ @Test
+ fun deviceAsleep_wakesUpBeforeTimeout_noChangeInScene() =
+ with(kosmos) {
+ testScope.runTest {
+ val scene by collectLastValue(communalInteractor.desiredScene)
+ communalInteractor.onSceneChanged(CommunalSceneKey.Communal)
+ assertThat(scene).isEqualTo(CommunalSceneKey.Communal)
+
+ fakeKeyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.GLANCEABLE_HUB,
+ to = KeyguardState.OFF,
+ testScope = this
+ )
+ assertThat(scene).isEqualTo(CommunalSceneKey.Communal)
+ advanceTimeBy(CommunalSceneStartable.AWAKE_DEBOUNCE_DELAY / 2)
+ assertThat(scene).isEqualTo(CommunalSceneKey.Communal)
+
+ fakeKeyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.OFF,
+ to = KeyguardState.GLANCEABLE_HUB,
+ testScope = this
+ )
+
+ advanceTimeBy(CommunalSceneStartable.AWAKE_DEBOUNCE_DELAY)
+ assertThat(scene).isEqualTo(CommunalSceneKey.Communal)
+ }
+ }
+
+ @Test
+ fun dockingOnLockscreen_forcesCommunal() =
+ with(kosmos) {
+ testScope.runTest {
+ communalInteractor.onSceneChanged(CommunalSceneKey.Blank)
+ val scene by collectLastValue(communalInteractor.desiredScene)
+
+ // device is docked while on the lockscreen
+ fakeKeyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.GLANCEABLE_HUB,
+ to = KeyguardState.LOCKSCREEN,
+ testScope = this
+ )
+ updateDocked(true)
+
+ assertThat(scene).isEqualTo(CommunalSceneKey.Blank)
+ advanceTimeBy(CommunalSceneStartable.DOCK_DEBOUNCE_DELAY)
+ assertThat(scene).isEqualTo(CommunalSceneKey.Communal)
+ }
+ }
+
+ @Test
+ fun dockingOnLockscreen_doesNotForceCommunalIfDreamStarts() =
+ with(kosmos) {
+ testScope.runTest {
+ communalInteractor.onSceneChanged(CommunalSceneKey.Blank)
+ val scene by collectLastValue(communalInteractor.desiredScene)
+
+ // device is docked while on the lockscreen
+ fakeKeyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.GLANCEABLE_HUB,
+ to = KeyguardState.LOCKSCREEN,
+ testScope = this
+ )
+ updateDocked(true)
+
+ assertThat(scene).isEqualTo(CommunalSceneKey.Blank)
+ advanceTimeBy(CommunalSceneStartable.DOCK_DEBOUNCE_DELAY / 2)
+ assertThat(scene).isEqualTo(CommunalSceneKey.Blank)
+
+ // dream starts shortly after docking
+ fakeKeyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.DREAMING,
+ testScope = this
+ )
+ advanceTimeBy(CommunalSceneStartable.DOCK_DEBOUNCE_DELAY)
+ assertThat(scene).isEqualTo(CommunalSceneKey.Blank)
+ }
+ }
+
+ private fun TestScope.updateDocked(docked: Boolean) =
+ with(kosmos) {
+ runCurrent()
+ fakeDockManager.setIsDocked(docked)
+ fakeDockManager.setDockEvent(DockManager.STATE_DOCKED)
+ runCurrent()
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dock/DockManagerFakeKosmos.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dock/DockManagerFakeKosmos.kt
new file mode 100644
index 0000000..06275fa
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dock/DockManagerFakeKosmos.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2024 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.dock
+
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.dockManager: DockManager by Kosmos.Fixture { fakeDockManager }
+val Kosmos.fakeDockManager: DockManagerFake by Kosmos.Fixture { DockManagerFake() }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt
new file mode 100644
index 0000000..f7ba5a4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2024 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
+
+import com.android.systemui.CoreStartable
+import com.android.systemui.communal.domain.interactor.CommunalInteractor
+import com.android.systemui.communal.shared.model.CommunalSceneKey
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dock.DockManager
+import com.android.systemui.dock.retrieveIsDocked
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.util.kotlin.sample
+import javax.inject.Inject
+import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.FlowPreview
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.debounce
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.onEach
+
+/**
+ * A [CoreStartable] responsible for automatically navigating between communal scenes when certain
+ * conditions are met.
+ */
+@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
+@SysUISingleton
+class CommunalSceneStartable
+@Inject
+constructor(
+ private val dockManager: DockManager,
+ private val communalInteractor: CommunalInteractor,
+ private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
+ @Application private val applicationScope: CoroutineScope,
+ @Background private val bgScope: CoroutineScope,
+) : CoreStartable {
+ override fun start() {
+ // Handle automatically switching based on keyguard state.
+ keyguardTransitionInteractor.startedKeyguardTransitionStep
+ .mapLatest(::determineSceneAfterTransition)
+ .filterNotNull()
+ // TODO(b/322787129): Also set a custom transition animation here to avoid the regular
+ // slide-in animation when setting the scene programmatically
+ .onEach { nextScene -> communalInteractor.onSceneChanged(nextScene) }
+ .launchIn(applicationScope)
+
+ // Handle automatically switching to communal when docked.
+ dockManager
+ .retrieveIsDocked()
+ // Allow some time after docking to ensure the dream doesn't start. If the dream
+ // starts, then we don't want to automatically transition to glanceable hub.
+ .debounce(DOCK_DEBOUNCE_DELAY)
+ .sample(keyguardTransitionInteractor.startedKeyguardState, ::Pair)
+ .onEach { (docked, lastStartedState) ->
+ if (docked && lastStartedState == KeyguardState.LOCKSCREEN) {
+ communalInteractor.onSceneChanged(CommunalSceneKey.Communal)
+ }
+ }
+ .launchIn(bgScope)
+ }
+
+ private suspend fun determineSceneAfterTransition(
+ lastStartedTransition: TransitionStep,
+ ): CommunalSceneKey? {
+ val to = lastStartedTransition.to
+ val from = lastStartedTransition.from
+ val docked = dockManager.isDocked
+
+ return when {
+ to == KeyguardState.DREAMING -> CommunalSceneKey.Blank
+ docked && to == KeyguardState.LOCKSCREEN && from != KeyguardState.GLANCEABLE_HUB -> {
+ CommunalSceneKey.Communal
+ }
+ to == KeyguardState.GONE -> CommunalSceneKey.Blank
+ !docked && !KeyguardState.deviceIsAwakeInState(to) -> {
+ // If the user taps the screen and wakes the device within this timeout, we don't
+ // want to dismiss the hub
+ delay(AWAKE_DEBOUNCE_DELAY)
+ CommunalSceneKey.Blank
+ }
+ else -> null
+ }
+ }
+
+ companion object {
+ val AWAKE_DEBOUNCE_DELAY = 5.seconds
+ val DOCK_DEBOUNCE_DELAY = 1.seconds
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt
index ad1327e..54c709d 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt
@@ -29,6 +29,7 @@
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
import com.android.internal.logging.UiEventLogger
import com.android.systemui.communal.shared.log.CommunalUiEvent
+import com.android.systemui.communal.shared.model.CommunalSceneKey
import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel
import com.android.systemui.compose.ComposeFacade.setCommunalEditWidgetActivityContent
import javax.inject.Inject
@@ -126,6 +127,7 @@
},
onEditDone = {
try {
+ communalViewModel.onSceneChanged(CommunalSceneKey.Communal)
checkNotNull(windowManagerService).lockNow(/* options */ null)
finish()
} catch (e: RemoteException) {
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
index 95233f7..8170879 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
@@ -24,6 +24,7 @@
import com.android.systemui.back.domain.interactor.BackActionInteractor
import com.android.systemui.biometrics.BiometricNotificationService
import com.android.systemui.clipboardoverlay.ClipboardListener
+import com.android.systemui.communal.CommunalSceneStartable
import com.android.systemui.communal.log.CommunalLoggerStartable
import com.android.systemui.communal.widgets.CommunalAppWidgetHostStartable
import com.android.systemui.controls.dagger.StartControlsStartableModule
@@ -328,6 +329,11 @@
@Binds
@IntoMap
+ @ClassKey(CommunalSceneStartable::class)
+ abstract fun bindCommunalSceneStartable(impl: CommunalSceneStartable): CoreStartable
+
+ @Binds
+ @IntoMap
@ClassKey(CommunalAppWidgetHostStartable::class)
abstract fun bindCommunalAppWidgetHostStartable(
impl: CommunalAppWidgetHostStartable