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

Commit dae634e0 authored by Justin Weir's avatar Justin Weir Committed by Android (Google) Code Review
Browse files

Merge "Add ShadeController implementation based on Scenes" into main

parents 2376ab52 0b84e076
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