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

Commit 80f0f594 authored by Michael Cheng's avatar Michael Cheng
Browse files

Show animation when location indicator appears, to avoid cases where the dot...

Show animation when location indicator appears, to avoid cases where the dot is invisible due to app / wallpaper color.

The animation only shows if an app has not used location for the past 10 min (i.e. debounce time is 10 minutes).

https://screencast.googleplex.com/cast/NTY2MjkzODcxNDA3OTIzMnxlOGY0MDdiYy03Mw

Fixes: 441820971
Flag: android.location.flags.location_indicators_enabled
Change-Id: I3db3f9581905a70fd7f4277d43fdd902750a5063
Test: atest frameworks/base/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/events/SystemEventCoordinatorTest.kt
Test: open maps -> chip should animate. close it -> wait 30s then reopen -> chip should not animate. wait 10+m -> chip should animate
parent 81c6443f
Loading
Loading
Loading
Loading
+386 −46
Original line number Diff line number Diff line
@@ -15,11 +15,14 @@
 */
package com.android.systemui.statusbar.events

import android.platform.test.annotations.EnableFlags
import android.location.flags.Flags.FLAG_LOCATION_INDICATORS_ENABLED
import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.platform.test.annotations.UsesFlags
import android.platform.test.flag.junit.FlagsParameterization
import android.testing.TestableLooper
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.Flags
import com.android.systemui.Flags.FLAG_STATUS_BAR_PRIVACY_CHIP_ANIMATION_EXEMPTION
import com.android.systemui.SysuiTestCase
import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor
@@ -50,11 +53,14 @@ import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoMoreInteractions
import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations
import platform.test.runner.parameterized.ParameterizedAndroidJunit4
import platform.test.runner.parameterized.Parameters

@RunWith(AndroidJUnit4::class)
@RunWith(ParameterizedAndroidJunit4::class)
@TestableLooper.RunWithLooper(setAsMainLooper = true)
@SmallTest
class SystemEventCoordinatorTest : SysuiTestCase() {
@UsesFlags(Flags::class, android.location.flags.Flags::class)
class SystemEventCoordinatorTest(flags: FlagsParameterization) : SysuiTestCase() {

    private val fakeSystemClock = FakeSystemClock()
    private val testScope = TestScope(UnconfinedTestDispatcher())
@@ -67,6 +73,22 @@ class SystemEventCoordinatorTest : SysuiTestCase() {

    private lateinit var systemEventCoordinator: SystemEventCoordinator

    companion object {
        @JvmStatic
        @Parameters(name = "{0}")
        fun getParams(): List<FlagsParameterization> {
            return FlagsParameterization.allCombinationsOf(FLAG_LOCATION_INDICATORS_ENABLED)
        }

        private const val ADVANCE_TIME_WITHIN_DEBOUNCE_MS = DEBOUNCE_TIME_LOCATION - 100_000L
        private const val ADVANCE_TIME_OUTSIDE_DEBOUNCE_MS = DEBOUNCE_TIME_LOCATION + 100_000L
        private const val ADVANCE_TIME_SHORT_MS = 50_000L
    }

    init {
        mSetFlagsRule.setFlagsParameterization(flags)
    }

    @Before
    fun setup() {
        MockitoAnnotations.initMocks(this)
@@ -113,11 +135,185 @@ class SystemEventCoordinatorTest : SysuiTestCase() {
            verifyNoMoreInteractions(scheduler)
        }

    @Test
    @EnableFlags(FLAG_LOCATION_INDICATORS_ENABLED)
    fun onPrivacyItemsChanged_locationOnly_locationFlagOn_showsAnimation() =
        testScope.runTest {
            val privacyList =
                listOf(
                    PrivacyItem(
                        application = PrivacyApplication("maps", 1),
                        privacyType = PrivacyType.TYPE_LOCATION,
                    )
                )
            systemEventCoordinator.getPrivacyStateListener().onPrivacyItemsChanged(privacyList)

            verify(scheduler).onStatusEvent(argThat { it.showAnimation })
        }

    @Test
    @DisableFlags(FLAG_LOCATION_INDICATORS_ENABLED)
    fun onPrivacyItemsChanged_locationOnly_locationFlagOff_doesNotShowAnimation() =
        testScope.runTest {
            val privacyList =
                listOf(
                    PrivacyItem(
                        application = PrivacyApplication("maps", 1),
                        privacyType = PrivacyType.TYPE_LOCATION,
                    )
                )
            systemEventCoordinator.getPrivacyStateListener().onPrivacyItemsChanged(privacyList)

            verify(scheduler).onStatusEvent(argThat { !it.showAnimation })
        }

    @Test
    @DisableFlags(FLAG_LOCATION_INDICATORS_ENABLED)
    fun onPrivacyItemsChanged_locationAndMic_locationFlagOff_showsAnimation() =
        testScope.runTest {
            val privacyList =
                listOf(
                    PrivacyItem(
                        application = PrivacyApplication("maps", 1),
                        privacyType = PrivacyType.TYPE_LOCATION,
                    ),
                    PrivacyItem(
                        application = PrivacyApplication("recorder", 2),
                        privacyType = PrivacyType.TYPE_MICROPHONE,
                    ),
                )
            systemEventCoordinator.getPrivacyStateListener().onPrivacyItemsChanged(privacyList)

            verify(scheduler).onStatusEvent(argThat { it.showAnimation })
        }

    @Test
    @EnableFlags(FLAG_LOCATION_INDICATORS_ENABLED)
    fun onPrivacyItemsChanged_locationAndMic_locationFlagOn_showsAnimation() =
        testScope.runTest {
            val privacyList =
                listOf(
                    PrivacyItem(
                        application = PrivacyApplication("maps", 1),
                        privacyType = PrivacyType.TYPE_LOCATION,
                    ),
                    PrivacyItem(
                        application = PrivacyApplication("recorder", 2),
                        privacyType = PrivacyType.TYPE_MICROPHONE,
                    ),
                )
            systemEventCoordinator.getPrivacyStateListener().onPrivacyItemsChanged(privacyList)

            verify(scheduler).onStatusEvent(argThat { it.showAnimation })
        }

    @Test
    @EnableFlags(FLAG_LOCATION_INDICATORS_ENABLED)
    fun onPrivacyItemsChanged_locationOnly_respectsDebounceWindow() =
        testScope.runTest {
            val privacyList =
                listOf(
                    PrivacyItem(
                        application = PrivacyApplication("maps", 1),
                        privacyType = PrivacyType.TYPE_LOCATION,
                    )
                )

            // First event, show animation
            systemEventCoordinator.getPrivacyStateListener().onPrivacyItemsChanged(privacyList)
            verify(scheduler).onStatusEvent(argThat { it.showAnimation })

            // Second event, within debounce window, should not show animation
            fakeSystemClock.advanceTime(ADVANCE_TIME_WITHIN_DEBOUNCE_MS)
            // We must clear the privacy items first to trigger the listener.
            // The listener no-ops if the list of privacy items is identical to the last one.
            systemEventCoordinator.getPrivacyStateListener().onPrivacyItemsChanged(emptyList())
            systemEventCoordinator.getPrivacyStateListener().onPrivacyItemsChanged(privacyList)
            verify(scheduler).onStatusEvent(argThat { !it.showAnimation })

            // Third event, after debounce window, should show animation again (More than 600_000L
            // since last animation)
            fakeSystemClock.advanceTime(ADVANCE_TIME_OUTSIDE_DEBOUNCE_MS)
            // We must clear the privacy items first to trigger the listener.
            systemEventCoordinator.getPrivacyStateListener().onPrivacyItemsChanged(emptyList())
            systemEventCoordinator.getPrivacyStateListener().onPrivacyItemsChanged(privacyList)
            verify(scheduler, times(2)).onStatusEvent(argThat { it.showAnimation })
        }

    @Test
    @EnableFlags(FLAG_LOCATION_INDICATORS_ENABLED)
    fun onPrivacyItemsChanged_locationOnly_differentApps_respectsDebounceWindow() =
        testScope.runTest {
            val privacyList1 =
                listOf(
                    PrivacyItem(
                        application = PrivacyApplication("maps", 1),
                        privacyType = PrivacyType.TYPE_LOCATION,
                    )
                )
            val privacyList2 =
                listOf(
                    PrivacyItem(
                        application = PrivacyApplication("photos", 2),
                        privacyType = PrivacyType.TYPE_LOCATION,
                    )
                )

            // First event, show animation
            systemEventCoordinator.getPrivacyStateListener().onPrivacyItemsChanged(privacyList1)
            verify(scheduler).onStatusEvent(argThat { it.showAnimation })

            // Second event from a different app, within debounce window, should show animation
            fakeSystemClock.advanceTime(ADVANCE_TIME_WITHIN_DEBOUNCE_MS)
            systemEventCoordinator.getPrivacyStateListener().onPrivacyItemsChanged(privacyList2)
            verify(scheduler, times(2)).onStatusEvent(argThat { it.showAnimation })

            // Third event, from the first app again, within its debounce window, should NOT show
            // animation
            fakeSystemClock.advanceTime(ADVANCE_TIME_SHORT_MS)
            systemEventCoordinator.getPrivacyStateListener().onPrivacyItemsChanged(privacyList1)
            verify(scheduler).onStatusEvent(argThat { !it.showAnimation })
        }

    @Test
    @DisableFlags(FLAG_LOCATION_INDICATORS_ENABLED)
    fun onPrivacyItemsChanged_locationOnly_flagOff_neverShowsAnimation() =
        testScope.runTest {
            val privacyList =
                listOf(
                    PrivacyItem(
                        application = PrivacyApplication("maps", 1),
                        privacyType = PrivacyType.TYPE_LOCATION,
                    )
                )

            // First event, should not show animation
            systemEventCoordinator.getPrivacyStateListener().onPrivacyItemsChanged(privacyList)
            verify(scheduler).onStatusEvent(argThat { !it.showAnimation })

            // Second event, within debounce window, should not show animation
            fakeSystemClock.advanceTime(ADVANCE_TIME_WITHIN_DEBOUNCE_MS)
            // We must clear the privacy items first to trigger the listener.
            // The listener no-ops if the list of privacy items is identical to the last one.
            systemEventCoordinator.getPrivacyStateListener().onPrivacyItemsChanged(emptyList())
            systemEventCoordinator.getPrivacyStateListener().onPrivacyItemsChanged(privacyList)
            verify(scheduler, times(2)).onStatusEvent(argThat { !it.showAnimation })

            // Third event, after debounce window, should not show animation (More than 600_000L
            // since last animation)
            fakeSystemClock.advanceTime(ADVANCE_TIME_OUTSIDE_DEBOUNCE_MS)
            // We must clear the privacy items first to trigger the listener.
            systemEventCoordinator.getPrivacyStateListener().onPrivacyItemsChanged(emptyList())
            systemEventCoordinator.getPrivacyStateListener().onPrivacyItemsChanged(privacyList)
            verify(scheduler, times(3)).onStatusEvent(argThat { !it.showAnimation })
        }

    @Test
    @EnableFlags(FLAG_STATUS_BAR_PRIVACY_CHIP_ANIMATION_EXEMPTION)
    fun onPrivacyItemsChanged_notDefaultCamera_showsAnimation() =
        testScope.runTest {
            val privacyList = listOf(
            val privacyList =
                listOf(
                    PrivacyItem(
                        application = PrivacyApplication("package1", 1),
                        privacyType = PrivacyType.TYPE_CAMERA,
@@ -132,11 +328,12 @@ class SystemEventCoordinatorTest : SysuiTestCase() {
    @EnableFlags(FLAG_STATUS_BAR_PRIVACY_CHIP_ANIMATION_EXEMPTION)
    fun onPrivacyItemsChanged_defaultCameraApp_cameraAccess_doesNotShowAnimation() =
        testScope.runTest {
            val privacyList = listOf(
            val privacyList =
                listOf(
                    PrivacyItem(
                        application = PrivacyApplication(DEFAULT_CAMERA_PACKAGE_NAME, 1),
                        privacyType = PrivacyType.TYPE_CAMERA,
                ),
                    )
                )
            systemEventCoordinator.getPrivacyStateListener().onPrivacyItemsChanged(privacyList)

@@ -147,7 +344,8 @@ class SystemEventCoordinatorTest : SysuiTestCase() {
    @EnableFlags(FLAG_STATUS_BAR_PRIVACY_CHIP_ANIMATION_EXEMPTION)
    fun onPrivacyItemsChanged_defaultCameraApp_microphoneAccess_doesNotShowAnimation() =
        testScope.runTest {
            val privacyList = listOf(
            val privacyList =
                listOf(
                    PrivacyItem(
                        application = PrivacyApplication(DEFAULT_CAMERA_PACKAGE_NAME, 1),
                        privacyType = PrivacyType.TYPE_MICROPHONE,
@@ -162,16 +360,18 @@ class SystemEventCoordinatorTest : SysuiTestCase() {
    @EnableFlags(FLAG_STATUS_BAR_PRIVACY_CHIP_ANIMATION_EXEMPTION)
    fun onPrivacyItemsChanged_defaultCamera_thenAnotherApp_showsAnimation() =
        testScope.runTest {
            val privacyList1 = listOf(
            val privacyList1 =
                listOf(
                    PrivacyItem(
                        application = PrivacyApplication(DEFAULT_CAMERA_PACKAGE_NAME, 1),
                        privacyType = PrivacyType.TYPE_CAMERA,
                ),
                    )
                )
            systemEventCoordinator.getPrivacyStateListener().onPrivacyItemsChanged(privacyList1)
            verify(scheduler).onStatusEvent(argThat { !it.showAnimation })

            val privacyList2 = listOf(
            val privacyList2 =
                listOf(
                    PrivacyItem(
                        application = PrivacyApplication(DEFAULT_CAMERA_PACKAGE_NAME, 1),
                        privacyType = PrivacyType.TYPE_CAMERA,
@@ -189,7 +389,8 @@ class SystemEventCoordinatorTest : SysuiTestCase() {
    @DisableFlags(FLAG_STATUS_BAR_PRIVACY_CHIP_ANIMATION_EXEMPTION)
    fun onPrivacyItemsChanged_defaultCameraApp_cameraAccess_flagOff_showsAnimation() =
        testScope.runTest {
            val privacyList = listOf(
            val privacyList =
                listOf(
                    PrivacyItem(
                        application = PrivacyApplication(DEFAULT_CAMERA_PACKAGE_NAME, 1),
                        privacyType = PrivacyType.TYPE_CAMERA,
@@ -204,7 +405,8 @@ class SystemEventCoordinatorTest : SysuiTestCase() {
    @DisableFlags(FLAG_STATUS_BAR_PRIVACY_CHIP_ANIMATION_EXEMPTION)
    fun onPrivacyItemsChanged_defaultCameraApp_microphoneAccess_flagOff_showsAnimation() =
        testScope.runTest {
            val privacyList = listOf(
            val privacyList =
                listOf(
                    PrivacyItem(
                        application = PrivacyApplication(DEFAULT_CAMERA_PACKAGE_NAME, 1),
                        privacyType = PrivacyType.TYPE_MICROPHONE,
@@ -215,6 +417,144 @@ class SystemEventCoordinatorTest : SysuiTestCase() {
            verify(scheduler).onStatusEvent(argThat { it.showAnimation })
        }

    @Test
    @EnableFlags(FLAG_LOCATION_INDICATORS_ENABLED, FLAG_STATUS_BAR_PRIVACY_CHIP_ANIMATION_EXEMPTION)
    fun onPrivacyItemsChanged_locationThenMicForDefaultCamera_noAnimation() =
        testScope.runTest {
            val locationList =
                listOf(
                    PrivacyItem(
                        application = PrivacyApplication("maps", 1),
                        privacyType = PrivacyType.TYPE_LOCATION,
                    )
                )
            val micAndLocationList =
                locationList +
                    PrivacyItem(
                        application = PrivacyApplication(DEFAULT_CAMERA_PACKAGE_NAME, 2),
                        privacyType = PrivacyType.TYPE_MICROPHONE,
                    )

            // First, location access shows animation
            systemEventCoordinator.getPrivacyStateListener().onPrivacyItemsChanged(locationList)
            verify(scheduler).onStatusEvent(argThat { it.showAnimation })

            // Second, location access within debounce window, no animation
            fakeSystemClock.advanceTime(ADVANCE_TIME_WITHIN_DEBOUNCE_MS)
            systemEventCoordinator.getPrivacyStateListener().onPrivacyItemsChanged(emptyList())
            systemEventCoordinator.getPrivacyStateListener().onPrivacyItemsChanged(locationList)
            verify(scheduler).onStatusEvent(argThat { !it.showAnimation })

            // Third, add mic access for default camera, no animation
            systemEventCoordinator
                .getPrivacyStateListener()
                .onPrivacyItemsChanged(micAndLocationList)
            verify(scheduler, times(2)).onStatusEvent(argThat { !it.showAnimation })
        }

    @Test
    @EnableFlags(FLAG_LOCATION_INDICATORS_ENABLED, FLAG_STATUS_BAR_PRIVACY_CHIP_ANIMATION_EXEMPTION)
    fun onPrivacyItemsChanged_micForDefaultCameraThenLocation_showsAnimationOnce() =
        testScope.runTest {
            val micList =
                listOf(
                    PrivacyItem(
                        application = PrivacyApplication(DEFAULT_CAMERA_PACKAGE_NAME, 1),
                        privacyType = PrivacyType.TYPE_MICROPHONE,
                    )
                )
            val micAndLocationList =
                micList +
                    PrivacyItem(
                        application = PrivacyApplication("maps", 2),
                        privacyType = PrivacyType.TYPE_LOCATION,
                    )

            // First, mic access for default camera, no animation
            systemEventCoordinator.getPrivacyStateListener().onPrivacyItemsChanged(micList)
            verify(scheduler).onStatusEvent(argThat { !it.showAnimation })

            // Second, add location access, shows animation
            systemEventCoordinator
                .getPrivacyStateListener()
                .onPrivacyItemsChanged(micAndLocationList)
            verify(scheduler).onStatusEvent(argThat { it.showAnimation })

            // Third, second location access within debounce window, no animation
            fakeSystemClock.advanceTime(ADVANCE_TIME_WITHIN_DEBOUNCE_MS)
            systemEventCoordinator.getPrivacyStateListener().onPrivacyItemsChanged(emptyList())
            systemEventCoordinator
                .getPrivacyStateListener()
                .onPrivacyItemsChanged(micAndLocationList)
            verify(scheduler, times(2)).onStatusEvent(argThat { !it.showAnimation })
        }

    @Test
    @EnableFlags(FLAG_LOCATION_INDICATORS_ENABLED)
    fun onPrivacyItemsChanged_locationDebounced_thenMic_showsAnimation() =
        testScope.runTest {
            val locationList =
                listOf(
                    PrivacyItem(
                        application = PrivacyApplication("maps", 1),
                        privacyType = PrivacyType.TYPE_LOCATION,
                    )
                )
            val micAndLocationList =
                locationList +
                    PrivacyItem(
                        application = PrivacyApplication("recorder", 2),
                        privacyType = PrivacyType.TYPE_MICROPHONE,
                    )

            // First, location access shows animation
            systemEventCoordinator.getPrivacyStateListener().onPrivacyItemsChanged(locationList)
            verify(scheduler).onStatusEvent(argThat { it.showAnimation })

            // Second, location access within debounce window, no animation
            fakeSystemClock.advanceTime(ADVANCE_TIME_WITHIN_DEBOUNCE_MS)
            systemEventCoordinator.getPrivacyStateListener().onPrivacyItemsChanged(emptyList())
            systemEventCoordinator.getPrivacyStateListener().onPrivacyItemsChanged(locationList)
            verify(scheduler).onStatusEvent(argThat { !it.showAnimation })

            // Third, add mic access, which should show an animation
            systemEventCoordinator
                .getPrivacyStateListener()
                .onPrivacyItemsChanged(micAndLocationList)
            verify(scheduler, times(2)).onStatusEvent(argThat { it.showAnimation })
        }

    @Test
    @EnableFlags(FLAG_LOCATION_INDICATORS_ENABLED)
    fun onPrivacyItemsChanged_location_continuousUsage_onlyAnimatesFirstTime() =
        testScope.runTest {
            val privacyList =
                listOf(
                    PrivacyItem(
                        application = PrivacyApplication("maps", 1),
                        privacyType = PrivacyType.TYPE_LOCATION,
                    )
                )

            // First event, show animation
            systemEventCoordinator.getPrivacyStateListener().onPrivacyItemsChanged(privacyList)

            // Second event, within debounce window, should not show animation
            fakeSystemClock.advanceTime(ADVANCE_TIME_WITHIN_DEBOUNCE_MS)
            systemEventCoordinator.getPrivacyStateListener().onPrivacyItemsChanged(emptyList())
            systemEventCoordinator.getPrivacyStateListener().onPrivacyItemsChanged(privacyList)

            // Third event, also within the debounce window of the *last usage*, should not show
            // animation
            fakeSystemClock.advanceTime(ADVANCE_TIME_WITHIN_DEBOUNCE_MS)
            systemEventCoordinator.getPrivacyStateListener().onPrivacyItemsChanged(emptyList())
            systemEventCoordinator.getPrivacyStateListener().onPrivacyItemsChanged(privacyList)

            // Verify that the animation was only shown once, and no-animation was shown twice.
            verify(scheduler).onStatusEvent(argThat { it.showAnimation })
            verify(scheduler, times(2)).onStatusEvent(argThat { !it.showAnimation })
        }

    class FakeConnectedDisplayInteractor : ConnectedDisplayInteractor {
        private val flow = MutableSharedFlow<Unit>()

+96 −35

File changed.

Preview size limit exceeded, changes collapsed.