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

Commit 7f15cc04 authored by Lawrence Huang's avatar Lawrence Huang Committed by Android (Google) Code Review
Browse files

Merge "Avoid chip animation when the default camera is active" into main

parents d103719c 17d4cae7
Loading
Loading
Loading
Loading
+10 −0
Original line number Diff line number Diff line
@@ -342,6 +342,16 @@ flag {
    bug: "374159193"
}

flag {
    name: "status_bar_privacy_chip_animation_exemption"
    namespace: "systemui"
    description: "Exempt the default camera app from the privacy chip animation."
    bug: "422243884"
    metadata {
        purpose: PURPOSE_BUGFIX
    }
}

flag {
    name: "notification_shade_ui_thread"
    namespace: "systemui"
+118 −0
Original line number Diff line number Diff line
@@ -15,19 +15,29 @@
 */
package com.android.systemui.statusbar.events

import android.platform.test.annotations.EnableFlags
import android.platform.test.annotations.DisableFlags
import android.testing.TestableLooper
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
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
import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor.PendingDisplay
import com.android.systemui.log.logcatLogBuffer
import com.android.systemui.privacy.PrivacyApplication
import com.android.systemui.privacy.PrivacyItem
import com.android.systemui.privacy.PrivacyItemController
import com.android.systemui.privacy.PrivacyType
import com.android.systemui.res.R
import com.android.systemui.statusbar.featurepods.av.domain.interactor.AvControlsChipInteractor
import com.android.systemui.statusbar.policy.BatteryController
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.argThat
import com.android.systemui.util.time.FakeSystemClock
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
@@ -38,6 +48,7 @@ import org.mockito.Mock
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoMoreInteractions
import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations

@RunWith(AndroidJUnit4::class)
@@ -60,6 +71,7 @@ class SystemEventCoordinatorTest : SysuiTestCase() {
    @Before
    fun setup() {
        MockitoAnnotations.initMocks(this)
        overrideResource(R.string.config_cameraGesturePackage, DEFAULT_CAMERA_PACKAGE_NAME)
        systemEventCoordinator =
            SystemEventCoordinator(
                    fakeSystemClock,
@@ -69,8 +81,10 @@ class SystemEventCoordinatorTest : SysuiTestCase() {
                    context,
                    TestScope(UnconfinedTestDispatcher()),
                    connectedDisplayInteractor,
                    logcatLogBuffer("SystemEventCoordinatorTest"),
                )
                .apply { attachScheduler(scheduler) }
        `when`(avControlsChipInteractor.isEnabled).thenReturn(MutableStateFlow(false))
    }

    @Test
@@ -100,6 +114,108 @@ class SystemEventCoordinatorTest : SysuiTestCase() {
            verifyNoMoreInteractions(scheduler)
        }

    @Test
    @EnableFlags(FLAG_STATUS_BAR_PRIVACY_CHIP_ANIMATION_EXEMPTION)
    fun onPrivacyItemsChanged_notDefaultCamera_showsAnimation() =
        testScope.runTest {
            val privacyList = listOf(
                PrivacyItem(
                    application = PrivacyApplication("package1", 1),
                    privacyType = PrivacyType.TYPE_CAMERA,
                )
            )
            systemEventCoordinator.getPrivacyStateListener().onPrivacyItemsChanged(privacyList)

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

    @Test
    @EnableFlags(FLAG_STATUS_BAR_PRIVACY_CHIP_ANIMATION_EXEMPTION)
    fun onPrivacyItemsChanged_defaultCameraApp_cameraAccess_doesNotShowAnimation() =
        testScope.runTest {
            val privacyList = listOf(
                PrivacyItem(
                    application = PrivacyApplication(DEFAULT_CAMERA_PACKAGE_NAME, 1),
                    privacyType = PrivacyType.TYPE_CAMERA,
                ),
            )
            systemEventCoordinator.getPrivacyStateListener().onPrivacyItemsChanged(privacyList)

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

    @Test
    @EnableFlags(FLAG_STATUS_BAR_PRIVACY_CHIP_ANIMATION_EXEMPTION)
    fun onPrivacyItemsChanged_defaultCameraApp_microphoneAccess_doesNotShowAnimation() =
        testScope.runTest {
            val privacyList = listOf(
                PrivacyItem(
                    application = PrivacyApplication(DEFAULT_CAMERA_PACKAGE_NAME, 1),
                    privacyType = PrivacyType.TYPE_MICROPHONE,
                )
            )
            systemEventCoordinator.getPrivacyStateListener().onPrivacyItemsChanged(privacyList)

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

    @Test
    @EnableFlags(FLAG_STATUS_BAR_PRIVACY_CHIP_ANIMATION_EXEMPTION)
    fun onPrivacyItemsChanged_defaultCamera_thenAnotherApp_showsAnimation() =
        testScope.runTest {
            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(
                PrivacyItem(
                    application = PrivacyApplication(DEFAULT_CAMERA_PACKAGE_NAME, 1),
                    privacyType = PrivacyType.TYPE_CAMERA,
                ),
                PrivacyItem(
                    application = PrivacyApplication("package1", 1),
                    privacyType = PrivacyType.TYPE_MICROPHONE,
                ),
            )
            systemEventCoordinator.getPrivacyStateListener().onPrivacyItemsChanged(privacyList2)
            verify(scheduler).onStatusEvent(argThat { it.showAnimation })
        }

    @Test
    @DisableFlags(FLAG_STATUS_BAR_PRIVACY_CHIP_ANIMATION_EXEMPTION)
    fun onPrivacyItemsChanged_defaultCameraApp_cameraAccess_flagOff_showsAnimation() =
        testScope.runTest {
            val privacyList = listOf(
                PrivacyItem(
                    application = PrivacyApplication(DEFAULT_CAMERA_PACKAGE_NAME, 1),
                    privacyType = PrivacyType.TYPE_CAMERA,
                )
            )
            systemEventCoordinator.getPrivacyStateListener().onPrivacyItemsChanged(privacyList)

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

    @Test
    @DisableFlags(FLAG_STATUS_BAR_PRIVACY_CHIP_ANIMATION_EXEMPTION)
    fun onPrivacyItemsChanged_defaultCameraApp_microphoneAccess_flagOff_showsAnimation() =
        testScope.runTest {
            val privacyList = listOf(
                PrivacyItem(
                    application = PrivacyApplication(DEFAULT_CAMERA_PACKAGE_NAME, 1),
                    privacyType = PrivacyType.TYPE_MICROPHONE,
                )
            )
            systemEventCoordinator.getPrivacyStateListener().onPrivacyItemsChanged(privacyList)

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

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

@@ -118,3 +234,5 @@ class SystemEventCoordinatorTest : SysuiTestCase() {
            get() = TODO("Not yet implemented")
    }
}

private const val DEFAULT_CAMERA_PACKAGE_NAME = "my.camera.package"
+7 −0
Original line number Diff line number Diff line
@@ -34,6 +34,13 @@ interface StatusBarEventsModule {
        fun provideSystemStatusAnimationSchedulerLogBuffer(factory: LogBufferFactory): LogBuffer {
            return factory.create("SystemStatusAnimationSchedulerLog", 60)
        }

        @Provides
        @SysUISingleton
        @SystemEventCoordinatorLog
        fun provideSystemEventCoordinatorLogBuffer(factory: LogBufferFactory): LogBuffer {
            return factory.create("SystemEventCoordinatorLog", 60)
        }
    }

    @Binds
+2 −1
Original line number Diff line number Diff line
@@ -128,7 +128,8 @@ open class PrivacyEvent(override val showAnimation: Boolean = true) : StatusEven
    }

    override fun toString(): String {
        return "${javaClass.simpleName}(forceVisible=$forceVisible, privacyItems=$privacyItems)"
        return "${javaClass.simpleName}(forceVisible=$forceVisible, " +
            "privacyItems=$privacyItems, showAnimation=$showAnimation)"
    }

    override fun shouldUpdateFromEvent(other: StatusEvent?): Boolean {
+52 −1
Original line number Diff line number Diff line
@@ -20,12 +20,17 @@ import android.annotation.IntRange
import android.content.Context
import android.provider.DeviceConfig
import android.provider.DeviceConfig.NAMESPACE_PRIVACY
import com.android.internal.annotations.VisibleForTesting
import com.android.systemui.Flags
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor
import com.android.systemui.log.LogBuffer
import com.android.systemui.log.core.LogLevel
import com.android.systemui.privacy.PrivacyChipBuilder
import com.android.systemui.privacy.PrivacyItem
import com.android.systemui.privacy.PrivacyItemController
import com.android.systemui.privacy.PrivacyType
import com.android.systemui.res.R
import com.android.systemui.statusbar.featurepods.av.domain.interactor.AvControlsChipInteractor
import com.android.systemui.statusbar.policy.BatteryController
@@ -51,8 +56,11 @@ constructor(
    private val context: Context,
    @Application private val appScope: CoroutineScope,
    connectedDisplayInteractor: ConnectedDisplayInteractor,
    @SystemEventCoordinatorLog private val logBuffer: LogBuffer,
) {
    private val onDisplayConnectedFlow = connectedDisplayInteractor.connectedDisplayAddition
    private val defaultCameraPackageName =
        context.resources.getString(R.string.config_cameraGesturePackage)

    private var connectedDisplayCollectionJob: Job? = null
    private lateinit var scheduler: SystemStatusAnimationScheduler
@@ -154,7 +162,7 @@ constructor(
                } else {
                    val showAnimation =
                        isChipAnimationEnabled() &&
                            !containsOnlyLocation(currentPrivacyItems) &&
                            !isExemptFromChipAnimation(currentPrivacyItems) &&
                            (!uniqueItemsMatch(currentPrivacyItems, previousPrivacyItems) ||
                                systemClock.elapsedRealtime() - timeLastEmpty >= DEBOUNCE_TIME)
                    notifyPrivacyItemsChanged(showAnimation)
@@ -167,6 +175,40 @@ constructor(
                    two.map { it.application.uid to it.privacyType.permGroupName }.toSet()
            }

            // Returns true if the privacy items are exempt from the chip animation.
            private fun isExemptFromChipAnimation(items: List<PrivacyItem>): Boolean {
                if (!Flags.statusBarPrivacyChipAnimationExemption()) {
                    return containsOnlyLocation(items)
                }

                // Camera and microphone requests by the default camera app are exempt from the
                // chip animation. Filter those out.
                val nonExemptItems =
                    items.filterNot {
                        val shouldFilter = isCameraOrMicrophoneRequest(it) &&
                            it.application.packageName == defaultCameraPackageName
                        if (shouldFilter) {
                            logBuffer.log(
                                TAG,
                                LogLevel.DEBUG,
                                {
                                    str1 = it.application.packageName
                                    str2 = it.privacyType.permGroupName
                                },
                                {
                                    "Privacy item from default camera ($str1) is exempt from " +
                                    "chip animation. Permission group=$str2"
                                },
                            )
                        }

                        shouldFilter
                    }

                // If the remaining items are only location, the chip animation is also exempt
                return containsOnlyLocation(nonExemptItems)
            }

            // Return true if the only privacy item is location
            private fun containsOnlyLocation(items: List<PrivacyItem>): Boolean {
                return items
@@ -176,6 +218,12 @@ constructor(
                    .isEmpty()
            }

            private fun isCameraOrMicrophoneRequest(item: PrivacyItem): Boolean {
                return item.privacyType.let {
                    it == PrivacyType.TYPE_CAMERA || it == PrivacyType.TYPE_MICROPHONE
                 }
            }

            private fun isChipAnimationEnabled(): Boolean {
                val defaultValue =
                    context.resources.getBoolean(R.bool.config_enablePrivacyChipAnimation)
@@ -186,6 +234,9 @@ constructor(
                )
            }
        }

        @VisibleForTesting
        fun getPrivacyStateListener() = privacyStateListener
}

private const val DEBOUNCE_TIME = 3000L
Loading