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

Commit 976c29c7 authored by Lucas Silva's avatar Lucas Silva Committed by Android (Google) Code Review
Browse files

Merge "Automatically navigate between communal scenes." into main

parents dd39a910 560ea1cc
Loading
Loading
Loading
Loading
+252 −0
Original line number Diff line number Diff line
/*
 * 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()
        }
}
+22 −0
Original line number Diff line number Diff line
/*
 * 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() }
+110 −0
Original line number Diff line number Diff line
/*
 * 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
    }
}
+2 −0
Original line number Diff line number Diff line
@@ -29,6 +29,7 @@ import androidx.activity.result.ActivityResultLauncher
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 @@ constructor(
            },
            onEditDone = {
                try {
                    communalViewModel.onSceneChanged(CommunalSceneKey.Communal)
                    checkNotNull(windowManagerService).lockNow(/* options */ null)
                    finish()
                } catch (e: RemoteException) {
+6 −0
Original line number Diff line number Diff line
@@ -24,6 +24,7 @@ import com.android.systemui.accessibility.Magnification
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
@@ -326,6 +327,11 @@ abstract class SystemUICoreStartableModule {
    @ClassKey(CommunalLoggerStartable::class)
    abstract fun bindCommunalLoggerStartable(impl: CommunalLoggerStartable): CoreStartable

    @Binds
    @IntoMap
    @ClassKey(CommunalSceneStartable::class)
    abstract fun bindCommunalSceneStartable(impl: CommunalSceneStartable): CoreStartable

    @Binds
    @IntoMap
    @ClassKey(CommunalAppWidgetHostStartable::class)