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

Commit 7830dd80 authored by Nicolo' Mazzucato's avatar Nicolo' Mazzucato
Browse files

Introduce policy to move the shade window according to the last status bar touch

This policy sets the shade in the same display of the last status bar touch.

When a display is removed, the shade window falls back to the default one.

Note that StatusBarTouchShadeDisplayPolicy has been written to be performant: no useless operations are done unless the policy is the selected one.

To trigger this behaviour, it is necessary to run:
"adb shell cmd statusbar shade_display_override status_bar_latest_touch"

(This only works if the flag is on)

Bug: 362719719
Bug: 380444270
Test: PhoneStatusBarViewControllerTest, StatusBarTouchShadeDisplayPolicyTest
Flag: com.android.systemui.shade_window_goes_around
Change-Id: I25ac6b954f4671a4db30c8620852efd8e437cecf
parent 964fb064
Loading
Loading
Loading
Loading
+86 −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.display

import android.view.Display
import android.view.Display.TYPE_EXTERNAL
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.coroutines.collectValues
import com.android.systemui.display.data.repository.display
import com.android.systemui.display.data.repository.displayRepository
import com.android.systemui.kosmos.testScope
import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlin.test.Test
import kotlinx.coroutines.test.runTest
import org.junit.runner.RunWith

@SmallTest
@RunWith(AndroidJUnit4::class)
class StatusBarTouchShadeDisplayPolicyTest : SysuiTestCase() {
    private val kosmos = testKosmos().useUnconfinedTestDispatcher()
    private val testScope = kosmos.testScope
    private val displayRepository = kosmos.displayRepository
    val underTest = StatusBarTouchShadeDisplayPolicy(displayRepository, testScope.backgroundScope)

    @Test
    fun displayId_defaultToDefaultDisplay() {
        assertThat(underTest.displayId.value).isEqualTo(Display.DEFAULT_DISPLAY)
    }

    @Test
    fun onStatusBarTouched_called_updatesDisplayId() =
        testScope.runTest {
            val displayId by collectLastValue(underTest.displayId)

            displayRepository.addDisplays(display(id = 2, type = TYPE_EXTERNAL))
            underTest.onStatusBarTouched(2)

            assertThat(displayId).isEqualTo(2)
        }

    @Test
    fun onStatusBarTouched_notExistentDisplay_displayIdNotUpdated() =
        testScope.runTest {
            val displayIds by collectValues(underTest.displayId)
            assertThat(displayIds).isEqualTo(listOf(Display.DEFAULT_DISPLAY))

            underTest.onStatusBarTouched(2)

            // Never set, as 2 was not a display according to the repository.
            assertThat(displayIds).isEqualTo(listOf(Display.DEFAULT_DISPLAY))
        }

    @Test
    fun onStatusBarTouched_afterDisplayRemoved_goesBackToDefaultDisplay() =
        testScope.runTest {
            val displayId by collectLastValue(underTest.displayId)

            displayRepository.addDisplays(display(id = 2, type = TYPE_EXTERNAL))
            underTest.onStatusBarTouched(2)

            assertThat(displayId).isEqualTo(2)

            displayRepository.removeDisplay(2)

            assertThat(displayId).isEqualTo(Display.DEFAULT_DISPLAY)
        }
}
+17 −3
Original line number Diff line number Diff line
@@ -66,9 +66,21 @@ interface DisplayRepository {
    /** Display removal event indicating a display has been removed. */
    val displayRemovalEvent: Flow<Int>

    /** Provides the current set of displays. */
    /**
     * Provides the current set of displays.
     *
     * Consider using [displayIds] if only the [Display.getDisplayId] is needed.
     */
    val displays: StateFlow<Set<Display>>

    /**
     * Provides the current set of display ids.
     *
     * Note that it is preferred to use this instead of [displays] if only the
     * [Display.getDisplayId] is needed.
     */
    val displayIds: StateFlow<Set<Int>>

    /**
     * Pending display id that can be enabled/disabled.
     *
@@ -159,7 +171,7 @@ constructor(
    private val initialDisplayIds = initialDisplays.map { display -> display.displayId }.toSet()

    /** Propagate to the listeners only enabled displays */
    private val enabledDisplayIds: Flow<Set<Int>> =
    private val enabledDisplayIds: StateFlow<Set<Int>> =
        allDisplayEvents
            .scan(initial = initialDisplayIds) { previousIds: Set<Int>, event: DisplayEvent ->
                val id = event.displayId
@@ -170,8 +182,8 @@ constructor(
                }
            }
            .distinctUntilChanged()
            .stateIn(bgApplicationScope, SharingStarted.WhileSubscribed(), initialDisplayIds)
            .debugLog("enabledDisplayIds")
            .stateIn(bgApplicationScope, SharingStarted.WhileSubscribed(), initialDisplayIds)

    private val defaultDisplay by lazy {
        getDisplayFromDisplayManager(Display.DEFAULT_DISPLAY)
@@ -209,6 +221,8 @@ constructor(
     */
    override val displays: StateFlow<Set<Display>> = enabledDisplays

    override val displayIds: StateFlow<Set<Int>> = enabledDisplayIds

    /**
     * Implementation that maps from [displays], instead of [allDisplayEvents] for 2 reasons:
     * 1. Guarantee that it emits __after__ [displays] emitted. This way it is guaranteed that
+6 −0
Original line number Diff line number Diff line
@@ -41,5 +41,11 @@ interface ShadeDisplayPolicyModule {
        impl: AnyExternalShadeDisplayPolicy
    ): ShadeDisplayPolicy

    @Binds
    @IntoSet
    fun provideStatusBarTouchShadeDisplayPolicy(
        impl: StatusBarTouchShadeDisplayPolicy
    ): ShadeDisplayPolicy

    @Binds fun provideDefaultPolicy(impl: DefaultShadeDisplayPolicy): ShadeDisplayPolicy
}
+95 −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.display

import android.util.Log
import android.view.Display
import com.android.app.tracing.coroutines.launchTraced
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.display.data.repository.DisplayRepository
import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map

/**
 * Moves the shade on the last display that received a status bar touch.
 *
 * If the display is removed, falls back to the default one.
 */
@SysUISingleton
class StatusBarTouchShadeDisplayPolicy
@Inject
constructor(displayRepository: DisplayRepository, @Background val backgroundScope: CoroutineScope) :
    ShadeDisplayPolicy {
    override val name: String
        get() = "status_bar_latest_touch"

    private val currentDisplayId = MutableStateFlow(Display.DEFAULT_DISPLAY)
    private val availableDisplayIds: StateFlow<Set<Int>> = displayRepository.displayIds

    override val displayId: StateFlow<Int>
        get() = currentDisplayId

    private var removalListener: Job? = null

    /** Called when the status bar on the given display is touched. */
    fun onStatusBarTouched(statusBarDisplayId: Int) {
        ShadeWindowGoesAround.isUnexpectedlyInLegacyMode()
        if (statusBarDisplayId !in availableDisplayIds.value) {
            Log.e(TAG, "Got touch on unknown display $statusBarDisplayId")
            return
        }
        currentDisplayId.value = statusBarDisplayId
        if (removalListener == null) {
            // Lazy start this at the first invocation. it's fine to let it run also when the policy
            // is not selected anymore, as the job doesn't do anything until someone subscribes to
            // displayId.
            removalListener = monitorDisplayRemovals()
        }
    }

    private fun monitorDisplayRemovals(): Job {
        return backgroundScope.launchTraced("StatusBarTouchDisplayPolicy#monitorDisplayRemovals") {
            currentDisplayId.subscriptionCount
                .map { it > 0 }
                .distinctUntilChanged()
                // When Active is false, no collect happens, and the old one is cancelled.
                // This is needed to prevent "availableDisplayIds" collection while nobody is
                // listening at the flow provided by this class.
                .collectLatest { active ->
                    if (active) {
                        availableDisplayIds.collect { availableIds ->
                            if (currentDisplayId.value !in availableIds) {
                                currentDisplayId.value = Display.DEFAULT_DISPLAY
                            }
                        }
                    }
                }
        }
    }

    private companion object {
        const val TAG = "StatusBarTouchDisplayPolicy"
    }
}
+9 −0
Original line number Diff line number Diff line
@@ -39,7 +39,9 @@ import com.android.systemui.shade.ShadeExpandsOnStatusBarLongPress
import com.android.systemui.shade.ShadeLogger
import com.android.systemui.shade.ShadeViewController
import com.android.systemui.shade.StatusBarLongPressGestureDetector
import com.android.systemui.shade.display.StatusBarTouchShadeDisplayPolicy
import com.android.systemui.shade.domain.interactor.PanelExpansionInteractor
import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround
import com.android.systemui.shared.animation.UnfoldMoveFromCenterAnimator
import com.android.systemui.statusbar.data.repository.StatusBarContentInsetsProviderStore
import com.android.systemui.statusbar.policy.Clock
@@ -52,6 +54,7 @@ import com.android.systemui.user.ui.viewmodel.StatusBarUserChipViewModel
import com.android.systemui.util.ViewController
import com.android.systemui.util.kotlin.getOrNull
import com.android.systemui.util.view.ViewUtil
import dagger.Lazy
import java.util.Optional
import javax.inject.Inject
import javax.inject.Named
@@ -79,6 +82,7 @@ private constructor(
    private val statusOverlayHoverListenerFactory: StatusOverlayHoverListenerFactory,
    private val darkIconDispatcher: DarkIconDispatcher,
    private val statusBarContentInsetsProvider: StatusBarContentInsetsProvider,
    private val lazyStatusBarShadeDisplayPolicy: Lazy<StatusBarTouchShadeDisplayPolicy>,
) : ViewController<PhoneStatusBarView>(view) {

    private lateinit var battery: BatteryMeterView
@@ -226,6 +230,9 @@ private constructor(
                !upOrCancel || shadeController.isExpandedVisible,
            )
        }
        if (ShadeWindowGoesAround.isEnabled && event.action == MotionEvent.ACTION_DOWN) {
            lazyStatusBarShadeDisplayPolicy.get().onStatusBarTouched(context.displayId)
        }
    }

    private fun addDarkReceivers() {
@@ -344,6 +351,7 @@ private constructor(
        private val statusOverlayHoverListenerFactory: StatusOverlayHoverListenerFactory,
        @DisplaySpecific private val darkIconDispatcher: DarkIconDispatcher,
        private val statusBarContentInsetsProviderStore: StatusBarContentInsetsProviderStore,
        private val lazyStatusBarShadeDisplayPolicy: Lazy<StatusBarTouchShadeDisplayPolicy>,
    ) {
        fun create(view: PhoneStatusBarView): PhoneStatusBarViewController {
            val statusBarMoveFromCenterAnimationController =
@@ -371,6 +379,7 @@ private constructor(
                statusOverlayHoverListenerFactory,
                darkIconDispatcher,
                statusBarContentInsetsProviderStore.defaultDisplay,
                lazyStatusBarShadeDisplayPolicy,
            )
        }
    }
Loading