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

Commit 0b84e076 authored by Justin Weir's avatar Justin Weir
Browse files

Add ShadeController implementation based on Scenes

Bug: 303267552
Test: Manually verified basic CUJs
Test: Added new unit tests and updated affected tests
Flag: ACONFIG com.android.systemui.scene_container DEVELOPMENT
Change-Id: Ia1cc804f6c46211b20c9842ff4d369219517c932
parent 63320d3f
Loading
Loading
Loading
Loading
+7 −2
Original line number Diff line number Diff line
@@ -25,10 +25,13 @@ import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
import com.android.systemui.keyguard.shared.model.StatusBarState
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.testScope
import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest
import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest
import com.android.systemui.power.domain.interactor.PowerInteractorFactory
import com.android.systemui.scene.data.repository.WindowRootViewVisibilityRepository
import com.android.systemui.scene.shared.flag.sceneContainerFlags
import com.android.systemui.statusbar.NotificationPresenter
import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository
import com.android.systemui.statusbar.notification.data.repository.setActiveNotifs
@@ -44,7 +47,6 @@ import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Test
@@ -56,7 +58,8 @@ import org.mockito.Mockito.verify
@RunWith(AndroidJUnit4::class)
class WindowRootViewVisibilityInteractorTest : SysuiTestCase() {

    private val testScope = TestScope()
    private val kosmos = Kosmos()
    private val testScope = kosmos.testScope
    private val testDispatcher = StandardTestDispatcher()
    private val iStatusBarService = mock<IStatusBarService>()
    private val executor = FakeExecutor(FakeSystemClock())
@@ -79,6 +82,8 @@ class WindowRootViewVisibilityInteractorTest : SysuiTestCase() {
                headsUpManager,
                powerInteractor,
                activeNotificationsInteractor,
                kosmos.sceneContainerFlags,
                kosmos::sceneInteractor,
            )
            .apply { setUp(notificationPresenter, notificationsController) }

+202 −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.shade

import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository
import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
import com.android.systemui.flags.Flags
import com.android.systemui.flags.fakeFeatureFlagsClassic
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.testCase
import com.android.systemui.kosmos.testScope
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags
import com.android.systemui.scene.shared.model.ObservableTransitionState
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.shared.model.SceneModel
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.shade.domain.interactor.shadeInteractor
import com.android.systemui.statusbar.CommandQueue
import com.android.systemui.util.mockito.mock
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.never
import org.mockito.Mockito.times
import org.mockito.Mockito.verify

@ExperimentalCoroutinesApi
@SmallTest
@RunWith(AndroidJUnit4::class)
class ShadeControllerSceneImplTest : SysuiTestCase() {
    private val kosmos = Kosmos()
    private val testScope = kosmos.testScope
    private val sceneInteractor = kosmos.sceneInteractor
    private val deviceEntryInteractor = kosmos.deviceEntryInteractor

    private lateinit var shadeInteractor: ShadeInteractor
    private lateinit var underTest: ShadeControllerSceneImpl

    @Before
    fun setup() {
        kosmos.testCase = this
        kosmos.fakeSceneContainerFlags.enabled = true
        kosmos.fakeFeatureFlagsClassic.apply {
            set(Flags.FULL_SCREEN_USER_SWITCHER, false)
            set(Flags.NSSL_DEBUG_LINES, false)
            set(Flags.FULL_SCREEN_USER_SWITCHER, false)
        }
        kosmos.fakeDeviceEntryRepository.setUnlocked(true)
        testScope.runCurrent()
        shadeInteractor = kosmos.shadeInteractor
        underTest = kosmos.shadeControllerSceneImpl
    }

    @Test
    fun animateCollapseShade_noForceNoExpansion() =
        testScope.runTest {
            // GIVEN shade is collapsed and a post-collapse action is enqueued
            val testRunnable = mock<Runnable>()
            setDeviceEntered(true)
            setCollapsed()
            underTest.addPostCollapseAction(testRunnable)

            // WHEN a collapse is requested
            underTest.animateCollapseShade(0, force = false, delayed = false, 1f)
            runCurrent()

            // THEN the shade remains collapsed and the post-collapse action ran
            assertThat(sceneInteractor.desiredScene.value.key).isEqualTo(SceneKey.Gone)
            verify(testRunnable, times(1)).run()
        }

    @Test
    fun animateCollapseShade_expandedExcludeFlagOn() =
        testScope.runTest {
            // GIVEN shade is fully expanded and a post-collapse action is enqueued
            val testRunnable = mock<Runnable>()
            underTest.addPostCollapseAction(testRunnable)
            setDeviceEntered(true)
            setShadeFullyExpanded()

            // WHEN a collapse is requested with FLAG_EXCLUDE_NOTIFICATION_PANEL
            underTest.animateCollapseShade(CommandQueue.FLAG_EXCLUDE_NOTIFICATION_PANEL)
            runCurrent()

            // THEN the shade remains expanded and the post-collapse action did not run
            assertThat(sceneInteractor.desiredScene.value.key).isEqualTo(SceneKey.Shade)
            assertThat(shadeInteractor.isAnyFullyExpanded.value).isTrue()
            verify(testRunnable, never()).run()
        }

    @Test
    fun animateCollapseShade_locked() =
        testScope.runTest {
            // GIVEN shade is fully expanded on lockscreen
            setDeviceEntered(false)
            setShadeFullyExpanded()

            // WHEN a collapse is requested
            underTest.animateCollapseShade()
            runCurrent()

            // THEN the shade collapses back to lockscreen and the post-collapse action ran
            assertThat(sceneInteractor.desiredScene.value.key).isEqualTo(SceneKey.Lockscreen)
        }

    @Test
    fun animateCollapseShade_unlocked() =
        testScope.runTest {
            // GIVEN shade is fully expanded on an unlocked device
            setDeviceEntered(true)
            setShadeFullyExpanded()

            // WHEN a collapse is requested
            underTest.animateCollapseShade()
            runCurrent()

            // THEN the shade collapses back to lockscreen and the post-collapse action ran
            assertThat(sceneInteractor.desiredScene.value.key).isEqualTo(SceneKey.Gone)
        }

    @Test
    fun onCollapseShade_runPostCollapseActionsCalled() =
        testScope.runTest {
            // GIVEN shade is expanded and a post-collapse action is enqueued
            val testRunnable = mock<Runnable>()
            setShadeFullyExpanded()
            underTest.addPostCollapseAction(testRunnable)

            // WHEN shade collapses
            setCollapsed()

            // THEN post-collapse action ran
            verify(testRunnable, times(1)).run()
        }

    @Test
    fun postOnShadeExpanded() =
        testScope.runTest {
            // GIVEN shade is collapsed and a post-collapse action is enqueued
            val testRunnable = mock<Runnable>()
            setCollapsed()
            underTest.postOnShadeExpanded(testRunnable)

            // WHEN shade expands
            setShadeFullyExpanded()

            // THEN post-collapse action ran
            verify(testRunnable, times(1)).run()
        }

    private fun setScene(key: SceneKey) {
        sceneInteractor.changeScene(SceneModel(key), "test")
        sceneInteractor.setTransitionState(
            MutableStateFlow<ObservableTransitionState>(ObservableTransitionState.Idle(key))
        )
        testScope.runCurrent()
    }

    private fun setDeviceEntered(isEntered: Boolean) {
        setScene(
            if (isEntered) {
                SceneKey.Gone
            } else {
                SceneKey.Lockscreen
            }
        )
        assertThat(deviceEntryInteractor.isDeviceEntered.value).isEqualTo(isEntered)
    }

    private fun setCollapsed() {
        setScene(SceneKey.Gone)
        assertThat(shadeInteractor.isAnyExpanded.value).isFalse()
    }

    private fun setShadeFullyExpanded() {
        setScene(SceneKey.Shade)
        assertThat(shadeInteractor.isAnyFullyExpanded.value).isTrue()
    }
}
+14 −12
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 The Android Open Source Project
 * 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
 * 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.
 * 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.statusbar.phone
@@ -18,9 +20,9 @@ import android.app.PendingIntent
import android.content.Intent
import android.os.RemoteException
import android.os.UserHandle
import android.testing.AndroidTestingRunner
import android.view.View
import android.widget.FrameLayout
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.keyguard.KeyguardUpdateMonitor
import com.android.systemui.ActivityIntentHelper
@@ -66,7 +68,7 @@ import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations

@SmallTest
@RunWith(AndroidTestingRunner::class)
@RunWith(AndroidJUnit4::class)
class ActivityStarterImplTest : SysuiTestCase() {
    @Mock private lateinit var centralSurfaces: CentralSurfaces
    @Mock private lateinit var assistManager: AssistManager
@@ -139,7 +141,7 @@ class ActivityStarterImplTest : SysuiTestCase() {
    }

    @Test
    fun startPendingIntentMaybeDismissingKeyguard_keyguardShowing_showOverLockscreen_activityLaunchAnimator() {
    fun startPendingIntentMaybeDismissingKeyguard_keyguardShowing_showOverLs_launchAnimator() {
        val pendingIntent = mock(PendingIntent::class.java)
        val parent = FrameLayout(context)
        val view =
@@ -214,7 +216,7 @@ class ActivityStarterImplTest : SysuiTestCase() {
        mainExecutor.runAllReady()

        verify(deviceProvisionedController).isDeviceProvisioned
        verify(shadeController).runPostCollapseRunnables()
        verify(shadeController).collapseShadeForActivityStart()
    }

    @Test
@@ -226,7 +228,7 @@ class ActivityStarterImplTest : SysuiTestCase() {
        mainExecutor.runAllReady()

        verify(deviceProvisionedController).isDeviceProvisioned
        verify(shadeController, never()).runPostCollapseRunnables()
        verify(shadeController, never()).collapseShadeForActivityStart()
    }

    @Test
+28 −3
Original line number Diff line number Diff line
@@ -23,16 +23,22 @@ import com.android.systemui.keyguard.data.repository.KeyguardRepository
import com.android.systemui.keyguard.shared.model.StatusBarState
import com.android.systemui.power.domain.interactor.PowerInteractor
import com.android.systemui.scene.data.repository.WindowRootViewVisibilityRepository
import com.android.systemui.scene.shared.flag.SceneContainerFlags
import com.android.systemui.scene.shared.model.ObservableTransitionState
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.statusbar.NotificationPresenter
import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
import com.android.systemui.statusbar.notification.init.NotificationsController
import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor
import com.android.systemui.statusbar.policy.HeadsUpManager
import javax.inject.Inject
import javax.inject.Provider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch

@@ -47,6 +53,8 @@ constructor(
    private val headsUpManager: HeadsUpManager,
    private val powerInteractor: PowerInteractor,
    private val activeNotificationsInteractor: ActiveNotificationsInteractor,
    sceneContainerFlags: SceneContainerFlags,
    sceneInteractorProvider: Provider<SceneInteractor>,
) : CoreStartable {

    private var notificationPresenter: NotificationPresenter? = null
@@ -58,11 +66,28 @@ constructor(
    /**
     * True if lockscreen (including AOD) or the shade is visible and false otherwise. Notably,
     * false if the bouncer is visible.
     *
     * TODO(b/297080059): Use [SceneInteractor] as the source of truth if the scene flag is on.
     */
    val isLockscreenOrShadeVisible: StateFlow<Boolean> =
        if (!sceneContainerFlags.isEnabled()) {
            windowRootViewVisibilityRepository.isLockscreenOrShadeVisible
        } else {
            sceneInteractorProvider
                .get()
                .transitionState
                .map { state ->
                    when (state) {
                        is ObservableTransitionState.Idle ->
                            state.scene == SceneKey.Shade || state.scene == SceneKey.Lockscreen
                        is ObservableTransitionState.Transition ->
                            state.toScene == SceneKey.Shade ||
                                state.toScene == SceneKey.Lockscreen ||
                                state.fromScene == SceneKey.Shade ||
                                state.fromScene == SceneKey.Lockscreen
                    }
                }
                .distinctUntilChanged()
                .stateIn(scope, SharingStarted.Eagerly, false)
        }

    /**
     * True if lockscreen (including AOD) or the shade is visible **and** the user is currently
+109 −0
Original line number Diff line number Diff line
/*
 * 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.shade

import com.android.systemui.assist.AssistManager
import com.android.systemui.log.LogBuffer
import com.android.systemui.shade.TouchLogger.Companion.logTouchesTo
import com.android.systemui.statusbar.CommandQueue
import com.android.systemui.statusbar.NotificationPresenter
import com.android.systemui.statusbar.NotificationShadeWindowController
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
import dagger.Lazy
import kotlinx.coroutines.ExperimentalCoroutinesApi

/** A base class for non-empty implementations of ShadeController. */
@OptIn(ExperimentalCoroutinesApi::class)
abstract class BaseShadeControllerImpl(
    private val touchLog: LogBuffer,
    protected val commandQueue: CommandQueue,
    protected val statusBarKeyguardViewManager: StatusBarKeyguardViewManager,
    protected val notificationShadeWindowController: NotificationShadeWindowController,
    protected val assistManagerLazy: Lazy<AssistManager>
) : ShadeController {
    protected lateinit var notifPresenter: NotificationPresenter
    /** Runnables to run after completing a collapse of the shade. */
    private val postCollapseActions = ArrayList<Runnable>()

    override fun start() {
        logTouchesTo(touchLog)
    }

    final override fun animateExpandShade() {
        if (isShadeEnabled) {
            expandToNotifications()
        }
    }

    /** Expand the shade with notifications visible. */
    protected abstract fun expandToNotifications()

    final override fun animateExpandQs() {
        if (isShadeEnabled) {
            expandToQs()
        }
    }

    /** Expand the shade showing only quick settings. */
    protected abstract fun expandToQs()

    final override fun addPostCollapseAction(action: Runnable) {
        postCollapseActions.add(action)
    }

    protected fun runPostCollapseActions() {
        val clonedList: ArrayList<Runnable> = ArrayList(postCollapseActions)
        postCollapseActions.clear()
        for (r in clonedList) {
            r.run()
        }
        statusBarKeyguardViewManager.readyForKeyguardDone()
    }

    final override fun onLaunchAnimationEnd(launchIsFullScreen: Boolean) {
        if (!this.notifPresenter.isCollapsing()) {
            onClosingFinished()
        }
        if (launchIsFullScreen) {
            instantCollapseShade()
        }
    }
    final override fun onLaunchAnimationCancelled(isLaunchForActivity: Boolean) {
        if (
            notifPresenter.isPresenterFullyCollapsed() &&
                !notifPresenter.isCollapsing() &&
                isLaunchForActivity
        ) {
            onClosingFinished()
        } else {
            collapseShade(true /* animate */)
        }
    }

    protected fun onClosingFinished() {
        runPostCollapseActions()
        if (!this.notifPresenter.isPresenterFullyCollapsed()) {
            // if we set it not to be focusable when collapsing, we have to undo it when we aborted
            // the closing
            notificationShadeWindowController.setNotificationShadeFocusable(true)
        }
    }

    override fun setNotificationPresenter(presenter: NotificationPresenter) {
        notifPresenter = presenter
    }
}
Loading