Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit e78b2da8 authored by William Xiao's avatar William Xiao
Browse files

Close the hub after a timeout when dreaming

When the device is dreaming, the screen doesn't time out. If the hub is
open, we should close after the configured display timeout. The timeout
is stopped if a user interactions with the device, if the user exits
the hub, or if dreaming ends.

Bug: 323247392
Fixed: 323247392
Test: atest CommunalSceneStartableTest
      also manually verified on device
Flag: ACONFIG com.android.systemui.communal_hub TEAMFOOD
Change-Id: I0ae51c22cf4dd38a9b4032063e9f1b69ce0583f7
parent 253a041a
Loading
Loading
Loading
Loading
+44 −24
Original line number Diff line number Diff line
@@ -85,6 +85,8 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.ColorMatrix
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.input.pointer.motionEventSpy
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.boundsInWindow
@@ -183,27 +185,37 @@ fun CommunalHub(
                            gridCoordinates
                        ) {
                            detectLongPressGesture { offset ->
                            // Deduct both grid offset relative to its container and content offset.
                                // Deduct both grid offset relative to its container and content
                                // offset.
                                val adjustedOffset =
                                    gridCoordinates?.let {
                                        offset - it.positionInWindow() - contentOffset
                                    }
                            val index = adjustedOffset?.let { firstIndexAtOffset(gridState, it) }
                                val index =
                                    adjustedOffset?.let { firstIndexAtOffset(gridState, it) }
                                // Display the button only when the gesture initiates from widgets,
                                // the CTA tile, or an empty area on the screen. UMO/smartspace have
                            // their own long-press handlers. To prevent user confusion, we should
                                // their own long-press handlers. To prevent user confusion, we
                                // should
                                // not display this button.
                                if (
                                    index == null ||
                                        communalContent[index].isWidgetContent() ||
                                    communalContent[index] is CommunalContentModel.CtaTileInViewMode
                                        communalContent[index] is
                                            CommunalContentModel.CtaTileInViewMode
                                ) {
                                    isButtonToEditWidgetsShowing = true
                                }
                            val key = index?.let { keyAtIndexIfEditable(communalContent, index) }
                                val key =
                                    index?.let { keyAtIndexIfEditable(communalContent, index) }
                                viewModel.setSelectedKey(key)
                            }
                        }
                        .onPreviewKeyEvent {
                            onKeyEvent(viewModel)
                            false
                        }
                        .motionEventSpy { onMotionEvent(viewModel) }
                },
    ) {
        CommunalHubLazyGrid(
@@ -311,6 +323,14 @@ fun CommunalHub(
    }
}

private fun onKeyEvent(viewModel: BaseCommunalViewModel) {
    viewModel.signalUserInteraction()
}

private fun onMotionEvent(viewModel: BaseCommunalViewModel) {
    viewModel.signalUserInteraction()
}

@Composable
private fun ScrollOnNewSmartspaceEffect(
    viewModel: BaseCommunalViewModel,
+102 −0
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.systemui.communal

import android.provider.Settings
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
@@ -25,13 +26,17 @@ import com.android.systemui.communal.shared.model.CommunalScenes
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.dock.dockManager
import com.android.systemui.dock.fakeDockManager
import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
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.android.systemui.util.settings.fakeSettings
import com.google.common.truth.Truth.assertThat
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.TestScope
@@ -54,11 +59,15 @@ class CommunalSceneStartableTest : SysuiTestCase() {
    @Before
    fun setUp() {
        with(kosmos) {
            fakeSettings.putInt(Settings.System.SCREEN_OFF_TIMEOUT, SCREEN_TIMEOUT)

            underTest =
                CommunalSceneStartable(
                        dockManager = dockManager,
                        communalInteractor = communalInteractor,
                        keyguardTransitionInteractor = keyguardTransitionInteractor,
                        keyguardInteractor = keyguardInteractor,
                        systemSettings = fakeSettings,
                        applicationScope = applicationCoroutineScope,
                        bgScope = applicationCoroutineScope,
                    )
@@ -246,6 +255,95 @@ class CommunalSceneStartableTest : SysuiTestCase() {
            }
        }

    @Test
    fun hubTimeout_whenDreaming() =
        with(kosmos) {
            testScope.runTest {
                // Device is dreaming and on communal.
                fakeKeyguardRepository.setDreaming(true)
                communalInteractor.onSceneChanged(CommunalScenes.Communal)

                val scene by collectLastValue(communalInteractor.desiredScene)
                assertThat(scene).isEqualTo(CommunalScenes.Communal)

                // Scene times out back to blank after the screen timeout.
                advanceTimeBy(SCREEN_TIMEOUT.milliseconds)
                assertThat(scene).isEqualTo(CommunalScenes.Blank)
            }
        }

    @Test
    fun hubTimeout_dreamStopped() =
        with(kosmos) {
            testScope.runTest {
                // Device is dreaming and on communal.
                fakeKeyguardRepository.setDreaming(true)
                communalInteractor.onSceneChanged(CommunalScenes.Communal)

                val scene by collectLastValue(communalInteractor.desiredScene)
                assertThat(scene).isEqualTo(CommunalScenes.Communal)

                // Wait a bit, but not long enough to timeout.
                advanceTimeBy((SCREEN_TIMEOUT / 2).milliseconds)
                assertThat(scene).isEqualTo(CommunalScenes.Communal)

                // Dream stops, timeout is cancelled and device stays on hub, because the regular
                // screen timeout will take effect at this point.
                fakeKeyguardRepository.setDreaming(false)
                advanceTimeBy((SCREEN_TIMEOUT / 2).milliseconds)
                assertThat(scene).isEqualTo(CommunalScenes.Communal)
            }
        }

    @Test
    fun hubTimeout_userActivityTriggered_resetsTimeout() =
        with(kosmos) {
            testScope.runTest {
                // Device is dreaming and on communal.
                fakeKeyguardRepository.setDreaming(true)
                communalInteractor.onSceneChanged(CommunalScenes.Communal)

                val scene by collectLastValue(communalInteractor.desiredScene)
                assertThat(scene).isEqualTo(CommunalScenes.Communal)

                // Wait a bit, but not long enough to timeout.
                advanceTimeBy((SCREEN_TIMEOUT / 2).milliseconds)

                // Send user interaction to reset timeout.
                communalInteractor.signalUserInteraction()

                // If user activity didn't reset timeout, we would have gone back to Blank by now.
                advanceTimeBy((SCREEN_TIMEOUT / 2).milliseconds)
                assertThat(scene).isEqualTo(CommunalScenes.Communal)

                // Timeout happens one interval after the user interaction.
                advanceTimeBy((SCREEN_TIMEOUT / 2).milliseconds)
                assertThat(scene).isEqualTo(CommunalScenes.Blank)
            }
        }

    @Test
    fun hubTimeout_screenTimeoutChanged() =
        with(kosmos) {
            testScope.runTest {
                fakeSettings.putInt(Settings.System.SCREEN_OFF_TIMEOUT, SCREEN_TIMEOUT * 2)

                // Device is dreaming and on communal.
                fakeKeyguardRepository.setDreaming(true)
                communalInteractor.onSceneChanged(CommunalScenes.Communal)

                val scene by collectLastValue(communalInteractor.desiredScene)
                assertThat(scene).isEqualTo(CommunalScenes.Communal)

                // Scene times out back to blank after the screen timeout.
                advanceTimeBy(SCREEN_TIMEOUT.milliseconds)
                assertThat(scene).isEqualTo(CommunalScenes.Communal)

                advanceTimeBy(SCREEN_TIMEOUT.milliseconds)
                assertThat(scene).isEqualTo(CommunalScenes.Blank)
            }
        }

    private fun TestScope.updateDocked(docked: Boolean) =
        with(kosmos) {
            runCurrent()
@@ -260,4 +358,8 @@ class CommunalSceneStartableTest : SysuiTestCase() {
            setCommunalAvailable(true)
            runCurrent()
        }

    companion object {
        private const val SCREEN_TIMEOUT = 1000
    }
}
+52 −2
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.systemui.communal

import android.provider.Settings
import com.android.compose.animation.scene.SceneKey
import com.android.systemui.CoreStartable
import com.android.systemui.communal.domain.interactor.CommunalInteractor
@@ -24,25 +25,32 @@ 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.keyguard.domain.interactor.KeyguardInteractor
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.emitOnStart
import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
import com.android.systemui.util.settings.SystemSettings
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
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.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch

/**
 * A [CoreStartable] responsible for automatically navigating between communal scenes when certain
 * conditions are met.
 */
@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
@OptIn(ExperimentalCoroutinesApi::class)
@SysUISingleton
class CommunalSceneStartable
@Inject
@@ -50,9 +58,13 @@ constructor(
    private val dockManager: DockManager,
    private val communalInteractor: CommunalInteractor,
    private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
    private val keyguardInteractor: KeyguardInteractor,
    private val systemSettings: SystemSettings,
    @Application private val applicationScope: CoroutineScope,
    @Background private val bgScope: CoroutineScope,
) : CoreStartable {
    private var screenTimeout: Int = DEFAULT_SCREEN_TIMEOUT

    override fun start() {
        // Handle automatically switching based on keyguard state.
        keyguardTransitionInteractor.startedKeyguardTransitionStep
@@ -78,6 +90,43 @@ constructor(
        //                }
        //            }
        //            .launchIn(bgScope)

        systemSettings
            .observerFlow(Settings.System.SCREEN_OFF_TIMEOUT)
            // Read the setting value on start.
            .emitOnStart()
            .onEach {
                screenTimeout =
                    systemSettings.getInt(
                        Settings.System.SCREEN_OFF_TIMEOUT,
                        DEFAULT_SCREEN_TIMEOUT
                    )
            }
            .launchIn(bgScope)

        // Handle timing out back to the dream.
        bgScope.launch {
            combine(
                    communalInteractor.desiredScene,
                    keyguardInteractor.isDreaming,
                    // Emit a value on start so the combine starts.
                    communalInteractor.userActivity.emitOnStart()
                ) { scene, isDreaming, _ ->
                    // Time out should run whenever we're dreaming and the hub is open, even if not
                    // docked.
                    scene == CommunalScenes.Communal && isDreaming
                }
                // collectLatest cancels the previous action block when new values arrive, so any
                // already running timeout gets cancelled when conditions change or user interaction
                // is detected.
                .collectLatest { shouldTimeout ->
                    if (!shouldTimeout) {
                        return@collectLatest
                    }
                    delay(screenTimeout.milliseconds)
                    communalInteractor.onSceneChanged(CommunalScenes.Blank)
                }
        }
    }

    private suspend fun determineSceneAfterTransition(
@@ -105,5 +154,6 @@ constructor(
    companion object {
        val AWAKE_DEBOUNCE_DELAY = 5.seconds
        val DOCK_DEBOUNCE_DELAY = 1.seconds
        val DEFAULT_SCREEN_TIMEOUT = 15000
    }
}
+12 −1
Original line number Diff line number Diff line
@@ -63,10 +63,13 @@ import com.android.systemui.util.kotlin.emitOnStart
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
@@ -84,7 +87,7 @@ import kotlinx.coroutines.flow.shareIn
class CommunalInteractor
@Inject
constructor(
    @Application applicationScope: CoroutineScope,
    @Application val applicationScope: CoroutineScope,
    broadcastDispatcher: BroadcastDispatcher,
    private val communalRepository: CommunalRepository,
    private val widgetRepository: CommunalWidgetRepository,
@@ -152,6 +155,14 @@ constructor(
    /** Transition state of the hub mode. */
    val transitionState: StateFlow<ObservableTransitionState> = communalRepository.transitionState

    val _userActivity: MutableSharedFlow<Unit> =
        MutableSharedFlow(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
    val userActivity: Flow<Unit> = _userActivity.asSharedFlow()

    fun signalUserInteraction() {
        _userActivity.tryEmit(Unit)
    }

    /**
     * Updates the transition state of the hub [SceneTransitionLayout].
     *
+4 −0
Original line number Diff line number Diff line
@@ -45,6 +45,10 @@ abstract class BaseCommunalViewModel(
    val selectedKey: StateFlow<String?>
        get() = _selectedKey

    fun signalUserInteraction() {
        communalInteractor.signalUserInteraction()
    }

    fun onSceneChanged(scene: SceneKey) {
        communalInteractor.onSceneChanged(scene)
    }