Fix hub timeout not working if dream restarts underneath hub after timeout
While we do expect the dream to be running underneath the hub when
docked, this is not the case when the dream app is being updated.
In these cases, the dream may be gone for longer than the device's
screen timeout.
This CL adjusts our timeout logic so that we start the timeout if the
dream restarts underneath the hub and no timeout is running.
Bug: 335862476
Fix: 335862476
Test: atest CommunalSceneStartableTest
Flag: ACONFIG com.android.systemui.communal_hub TEAMFOOD
Change-Id: I7175fd2023bf86537f1d23e4dcaff9d28ea86c5d
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt
index b4b812d..0ab0959 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt
@@ -265,7 +265,7 @@
with(kosmos) {
testScope.runTest {
// Device is dreaming and on communal.
- fakeKeyguardRepository.setDreaming(true)
+ updateDreaming(true)
communalInteractor.changeScene(CommunalScenes.Communal)
val scene by collectLastValue(communalInteractor.desiredScene)
@@ -282,7 +282,7 @@
with(kosmos) {
testScope.runTest {
// Device is not dreaming and on communal.
- fakeKeyguardRepository.setDreaming(false)
+ updateDreaming(false)
communalInteractor.changeScene(CommunalScenes.Communal)
// Scene stays as Communal
@@ -297,7 +297,7 @@
with(kosmos) {
testScope.runTest {
// Device is dreaming and on communal.
- fakeKeyguardRepository.setDreaming(true)
+ updateDreaming(true)
communalInteractor.changeScene(CommunalScenes.Communal)
val scene by collectLastValue(communalInteractor.desiredScene)
@@ -309,7 +309,7 @@
// Dream stops, timeout is cancelled and device stays on hub, because the regular
// screen timeout will take effect at this point.
- fakeKeyguardRepository.setDreaming(false)
+ updateDreaming(false)
advanceTimeBy((SCREEN_TIMEOUT / 2).milliseconds)
assertThat(scene).isEqualTo(CommunalScenes.Communal)
}
@@ -320,7 +320,7 @@
with(kosmos) {
testScope.runTest {
// Device is on communal, but not dreaming.
- fakeKeyguardRepository.setDreaming(false)
+ updateDreaming(false)
communalInteractor.changeScene(CommunalScenes.Communal)
val scene by collectLastValue(communalInteractor.desiredScene)
@@ -328,7 +328,7 @@
// Wait a bit, but not long enough to timeout, then start dreaming.
advanceTimeBy((SCREEN_TIMEOUT / 2).milliseconds)
- fakeKeyguardRepository.setDreaming(true)
+ updateDreaming(true)
assertThat(scene).isEqualTo(CommunalScenes.Communal)
// Device times out after one screen timeout interval, dream doesn't reset timeout.
@@ -338,11 +338,31 @@
}
@Test
+ fun hubTimeout_dreamAfterInitialTimeout_goesToBlank() =
+ with(kosmos) {
+ testScope.runTest {
+ // Device is on communal.
+ communalInteractor.changeScene(CommunalScenes.Communal)
+
+ // Device stays on the hub after the timeout since we're not dreaming.
+ advanceTimeBy(SCREEN_TIMEOUT.milliseconds * 2)
+ val scene by collectLastValue(communalInteractor.desiredScene)
+ assertThat(scene).isEqualTo(CommunalScenes.Communal)
+
+ // Start dreaming.
+ updateDreaming(true)
+
+ // Hub times out immediately.
+ assertThat(scene).isEqualTo(CommunalScenes.Blank)
+ }
+ }
+
+ @Test
fun hubTimeout_userActivityTriggered_resetsTimeout() =
with(kosmos) {
testScope.runTest {
// Device is dreaming and on communal.
- fakeKeyguardRepository.setDreaming(true)
+ updateDreaming(true)
communalInteractor.changeScene(CommunalScenes.Communal)
val scene by collectLastValue(communalInteractor.desiredScene)
@@ -371,7 +391,7 @@
fakeSettings.putInt(Settings.System.SCREEN_OFF_TIMEOUT, SCREEN_TIMEOUT * 2)
// Device is dreaming and on communal.
- fakeKeyguardRepository.setDreaming(true)
+ updateDreaming(true)
communalInteractor.changeScene(CommunalScenes.Communal)
val scene by collectLastValue(communalInteractor.desiredScene)
@@ -395,6 +415,12 @@
runCurrent()
}
+ private fun TestScope.updateDreaming(dreaming: Boolean) =
+ with(kosmos) {
+ fakeKeyguardRepository.setDreaming(dreaming)
+ runCurrent()
+ }
+
private suspend fun TestScope.enableCommunal() =
with(kosmos) {
setCommunalAvailable(true)
diff --git a/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt
index f437032..28f2ef2 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt
@@ -43,6 +43,7 @@
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
@@ -74,6 +75,10 @@
) : CoreStartable {
private var screenTimeout: Int = DEFAULT_SCREEN_TIMEOUT
+ private var timeoutJob: Job? = null
+
+ private var isDreaming: Boolean = false
+
override fun start() {
// Handle automatically switching based on keyguard state.
keyguardTransitionInteractor.startedKeyguardTransitionStep
@@ -113,31 +118,35 @@
}
.launchIn(bgScope)
- // Handle timing out back to the dream.
+ // The hub mode timeout should start as soon as the user enters hub mode. At the end of the
+ // timer, if the device is dreaming, hub mode should closed and reveal the dream. If the
+ // dream is not running, nothing will happen. However if the dream starts again underneath
+ // hub mode after the initial timeout expires, such as if the device is docked or the dream
+ // app is updated by the Play store, a new timeout should be started.
bgScope.launch {
combine(
communalInteractor.desiredScene,
// Emit a value on start so the combine starts.
communalInteractor.userActivity.emitOnStart()
) { scene, _ ->
- // Time out should run whenever we're dreaming and the hub is open, even if not
- // docked.
+ // Only timeout if we're on the hub is open.
scene == CommunalScenes.Communal
}
- // mapLatest cancels the previous action block when new values arrive, so any
- // already running timeout gets cancelled when conditions change or user interaction
- // is detected.
- .mapLatest { shouldTimeout ->
- if (!shouldTimeout) {
- return@mapLatest false
+ .collectLatest { shouldTimeout ->
+ cancelHubTimeout()
+ if (shouldTimeout) {
+ startHubTimeout()
}
-
- delay(screenTimeout.milliseconds)
- true
}
- .sample(keyguardInteractor.isDreaming, ::Pair)
- .collect { (shouldTimeout, isDreaming) ->
- if (isDreaming && shouldTimeout) {
+ }
+ bgScope.launch {
+ keyguardInteractor.isDreaming
+ .sample(communalInteractor.desiredScene, ::Pair)
+ .collectLatest { (isDreaming, scene) ->
+ this@CommunalSceneStartable.isDreaming = isDreaming
+ if (scene == CommunalScenes.Communal && isDreaming && timeoutJob == null) {
+ // If dreaming starts after timeout has expired, ex. if dream restarts under
+ // the hub, just close the hub immediately.
communalInteractor.changeScene(CommunalScenes.Blank)
}
}
@@ -154,6 +163,24 @@
}
}
+ private fun cancelHubTimeout() {
+ timeoutJob?.cancel()
+ timeoutJob = null
+ }
+
+ private fun startHubTimeout() {
+ if (timeoutJob == null) {
+ timeoutJob =
+ bgScope.launch {
+ delay(screenTimeout.milliseconds)
+ if (isDreaming) {
+ communalInteractor.changeScene(CommunalScenes.Blank)
+ }
+ timeoutJob = null
+ }
+ }
+ }
+
private suspend fun determineSceneAfterTransition(
lastStartedTransition: TransitionStep,
): SceneKey? {