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

Commit 07ebbee2 authored by Michael Cheng's avatar Michael Cheng Committed by Android (Google) Code Review
Browse files

Merge "Make privacy dot and chip blue when only location privacy events are active." into main

parents 8f512fe1 de6a9439
Loading
Loading
Loading
Loading
+101 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.privacy

import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.dump.DumpManager
import com.android.systemui.res.R
import com.android.systemui.util.DeviceConfigProxy
import com.android.systemui.util.DeviceConfigProxyFake
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.time.FakeSystemClock
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.MockitoAnnotations

@SmallTest
@RunWith(AndroidJUnit4::class)
class PrivacyConfigTest : SysuiTestCase() {

    private lateinit var privacyConfig: PrivacyConfig

    @Mock private lateinit var callback: PrivacyConfig.Callback
    @Mock private lateinit var dumpManager: DumpManager

    private lateinit var executor: FakeExecutor
    private lateinit var deviceConfigProxy: DeviceConfigProxy

    fun createPrivacyConfig(): PrivacyConfig {
        return PrivacyConfig(executor, deviceConfigProxy, dumpManager)
    }

    @Before
    fun setup() {
        MockitoAnnotations.initMocks(this)
        executor = FakeExecutor(FakeSystemClock())
        deviceConfigProxy = DeviceConfigProxyFake()

        privacyConfig = createPrivacyConfig()
        privacyConfig.addCallback(callback)

        executor.runAllReady()
    }

    @Test
    fun getPrivacyColor_locationOnly_returnsLocationOnlyColor() {
        assertEquals(
            R.color.privacy_chip_location_only_background,
            PrivacyConfig.Companion.getPrivacyColor(locationOnly = true),
        )
    }

    @Test
    fun getPrivacyColor_multiplePrivacyItems_returnsDefaultPrivacyColor() {
        assertEquals(
            R.color.privacy_chip_background,
            PrivacyConfig.Companion.getPrivacyColor(locationOnly = false),
        )
    }

    @Test
    fun privacyItemsAreLocationOnly_locationOnly_returnsTrue() {
        assertTrue(
            PrivacyConfig.Companion.privacyItemsAreLocationOnly(
                listOf(PrivacyItem(PrivacyType.TYPE_LOCATION, PrivacyApplication("app", 1)))
            )
        )
    }

    @Test
    fun privacyItemsAreLocationOnly_multiplePrivacyItems_returnsFalse() {
        assertFalse(
            PrivacyConfig.Companion.privacyItemsAreLocationOnly(
                listOf(
                    PrivacyItem(PrivacyType.TYPE_CAMERA, PrivacyApplication("app", 1)),
                    PrivacyItem(PrivacyType.TYPE_LOCATION, PrivacyApplication("app", 1)),
                )
            )
        )
    }
}
+93 −6
Original line number Diff line number Diff line
@@ -18,20 +18,27 @@ package com.android.systemui.statusbar.events

import android.graphics.Point
import android.graphics.Rect
import android.location.flags.Flags
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.RunWithLooper
import android.view.Display
import android.view.DisplayAdjustments
import android.view.View
import android.widget.FrameLayout
import android.widget.FrameLayout.LayoutParams.UNSPECIFIED_GRAVITY
import androidx.test.ext.junit.runners.AndroidJUnit4
import android.widget.ImageView
import androidx.test.filters.SmallTest
import com.android.systemui.Flags.FLAG_SHADE_WINDOW_GOES_AROUND
import com.android.systemui.SysuiTestCase
import com.android.systemui.kosmos.backgroundScope
import com.android.systemui.kosmos.runTest
import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.privacy.PrivacyApplication
import com.android.systemui.privacy.PrivacyItem
import com.android.systemui.privacy.PrivacyType
import com.android.systemui.res.R
import com.android.systemui.shade.data.repository.fakeShadeDisplaysRepository
import com.android.systemui.shade.domain.interactor.shadeDisplaysInteractor
@@ -61,11 +68,14 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.Mockito
import platform.test.runner.parameterized.ParameterizedAndroidJunit4
import platform.test.runner.parameterized.Parameters

@SmallTest
@RunWith(AndroidJUnit4::class)
@RunWith(ParameterizedAndroidJunit4::class)
@RunWithLooper
class PrivacyDotViewControllerTest : SysuiTestCase() {
@UsesFlags(Flags::class)
class PrivacyDotViewControllerTest(flags: FlagsParameterization) : SysuiTestCase() {

    private val kosmos = testKosmos().useUnconfinedTestDispatcher()
    private val mockDisplay = createMockDisplay()
@@ -105,6 +115,18 @@ class PrivacyDotViewControllerTest : SysuiTestCase() {
            shadeDisplaysInteractor = { shadeDisplaysInteractor },
        )

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

    init {
        mSetFlagsRule.setFlagsParameterization(flags)
    }

    @Test
    fun topMargin_topLeftView_basedOnSeascapeArea() {
        createAndInitializeController()
@@ -383,7 +405,7 @@ class PrivacyDotViewControllerTest : SysuiTestCase() {
            val callback: SystemStatusAnimationCallback = captor.value
            fakeAvControlsChipInteractor.isShowingAvChip.value = false
            // This informs the controller of an active privacy event.
            callback.onSystemStatusAnimationTransitionToPersistentDot(null)
            callback.onSystemStatusAnimationTransitionToPersistentDot(null, null)
            assertThat(controller.currentViewState.shouldShowDot()).isEqualTo(true)
        }

@@ -396,16 +418,81 @@ class PrivacyDotViewControllerTest : SysuiTestCase() {
            val callback: SystemStatusAnimationCallback = captor.value
            fakeAvControlsChipInteractor.isShowingAvChip.value = true
            // This informs the controller of an active privacy event.
            callback.onSystemStatusAnimationTransitionToPersistentDot(null)
            callback.onSystemStatusAnimationTransitionToPersistentDot(null, null)
            assertThat(controller.currentViewState.shouldShowDot()).isEqualTo(false)
        }

    @Test
    @DisableFlags(Flags.FLAG_LOCATION_INDICATORS_ENABLED)
    fun animationCallback_locationIndicatorsDisabled_doesNotDetermineLocationOnlyEvents() =
        kosmos.runTest {
            val captor = ArgumentCaptor.forClass(SystemStatusAnimationCallback::class.java)
            val controller: PrivacyDotViewController = createAndInitializeController()
            Mockito.verify(mockAnimationScheduler).addCallback(captor.capture())
            val callback: SystemStatusAnimationCallback = captor.value
            fakeAvControlsChipInteractor.isShowingAvChip.value = false
            // This informs the controller of an active privacy event.
            // Even with just the location event, the location-only flag is not set.
            callback.onSystemStatusAnimationTransitionToPersistentDot(
                null,
                listOf(
                    PrivacyItem(
                        privacyType = PrivacyType.TYPE_LOCATION,
                        application = PrivacyApplication(packageName = "com.android", uid = 1),
                    )
                ),
            )
            assertThat(controller.currentViewState.systemPrivacyEventLocationOnlyIsActive)
                .isEqualTo(false)
        }

    @Test
    @EnableFlags(Flags.FLAG_LOCATION_INDICATORS_ENABLED)
    fun animationCallback_locationIndicatorsEnabled_determinesLocationOnlyEvents() =
        kosmos.runTest {
            val captor = ArgumentCaptor.forClass(SystemStatusAnimationCallback::class.java)
            val controller: PrivacyDotViewController = createAndInitializeController()
            Mockito.verify(mockAnimationScheduler).addCallback(captor.capture())
            val callback: SystemStatusAnimationCallback = captor.value
            fakeAvControlsChipInteractor.isShowingAvChip.value = false
            // This informs the controller of an active privacy event.
            // Multiple privacy items are active, so the location-only flag is not set.
            callback.onSystemStatusAnimationTransitionToPersistentDot(
                null,
                listOf(
                    PrivacyItem(
                        privacyType = PrivacyType.TYPE_CAMERA,
                        application = PrivacyApplication(packageName = "com.android", uid = 1),
                    ),
                    PrivacyItem(
                        privacyType = PrivacyType.TYPE_LOCATION,
                        application = PrivacyApplication(packageName = "com.android", uid = 2),
                    ),
                ),
            )
            assertThat(controller.currentViewState.systemPrivacyEventLocationOnlyIsActive)
                .isEqualTo(false)

            // Only location event is active, so the location-only flag is set.
            callback.onSystemStatusAnimationTransitionToPersistentDot(
                null,
                listOf(
                    PrivacyItem(
                        privacyType = PrivacyType.TYPE_LOCATION,
                        application = PrivacyApplication(packageName = "com.android", uid = 1),
                    )
                ),
            )
            assertThat(controller.currentViewState.systemPrivacyEventLocationOnlyIsActive)
                .isEqualTo(true)
        }

    private fun setRotation(rotation: Int) {
        whenever(mockDisplay.rotation).thenReturn(rotation)
    }

    private fun initDotView(): View {
        val privacyDot = View(context).also { it.id = R.id.privacy_dot }
        val privacyDot = ImageView(context).also { it.id = R.id.privacy_dot }
        return FrameLayout(context).also {
            it.layoutParams = FrameLayout.LayoutParams(/* width= */ 0, /* height= */ 0)
            it.addView(privacyDot)
+1 −0
Original line number Diff line number Diff line
@@ -249,6 +249,7 @@
    <color name="screenrecord_icon_color">#D93025</color><!-- red 600 -->

    <color name="privacy_chip_background">#3ddc84</color>
    <color name="privacy_chip_location_only_background">#3dbaf4</color>

    <!-- Accessibility floating menu -->
    <color name="accessibility_floating_menu_background">#CCFFFFFF</color> <!-- 80% -->
+30 −2
Original line number Diff line number Diff line
@@ -17,6 +17,8 @@ package com.android.systemui.privacy
import android.content.Context
import android.content.pm.ActivityInfo
import android.content.res.Configuration
import android.graphics.drawable.GradientDrawable
import android.location.flags.Flags.locationIndicatorsEnabled
import android.util.AttributeSet
import android.view.Gravity.CENTER_VERTICAL
import android.view.Gravity.END
@@ -27,6 +29,7 @@ import android.view.accessibility.AccessibilityNodeInfo
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.LinearLayout
import androidx.annotation.VisibleForTesting
import com.android.settingslib.Utils
import com.android.systemui.Flags
import com.android.systemui.res.R
@@ -46,8 +49,9 @@ constructor(
    private var iconMargin = 0
    private var iconSize = 0
    private var iconColor = 0
    private var chipDrawable: GradientDrawable? = null

    private val iconsContainer: LinearLayout
    @VisibleForTesting val iconsContainer: LinearLayout
    val launchableContentView
        get() = iconsContainer

@@ -55,6 +59,17 @@ constructor(
        set(value) {
            field = value
            updateView(PrivacyChipBuilder(context, field))
            if (locationIndicatorsEnabled()) {
                updateResources()
            }
        }

    private val locationOnly: Boolean
        private get() =
            if (locationIndicatorsEnabled()) {
                PrivacyConfig.Companion.privacyItemsAreLocationOnly(privacyList)
            } else {
                false
            }

    init {
@@ -149,6 +164,19 @@ constructor(
            context.resources.getDimensionPixelSize(R.dimen.ongoing_appops_chip_side_padding)
        iconsContainer.layoutParams.height = height
        iconsContainer.setPaddingRelative(padding, 0, padding, 0)
        if (locationIndicatorsEnabled()) {
            if (chipDrawable == null) {
                chipDrawable =
                    context.getDrawable(R.drawable.statusbar_privacy_chip_bg)?.mutate()
                        as? GradientDrawable
                iconsContainer.background = chipDrawable
            }
            chipDrawable?.let { drawable ->
                val color = context.getColor(PrivacyConfig.Companion.getPrivacyColor(locationOnly))
                drawable.setColor(color)
            }
        } else {
            iconsContainer.background = context.getDrawable(R.drawable.statusbar_privacy_chip_bg)
        }
    }
}
+14 −1
Original line number Diff line number Diff line
@@ -25,6 +25,7 @@ import com.android.systemui.Dumpable
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.dump.DumpManager
import com.android.systemui.res.R
import com.android.systemui.util.DeviceConfigProxy
import com.android.systemui.util.asIndenting
import com.android.systemui.util.concurrency.DelayableExecutor
@@ -43,13 +44,25 @@ constructor(
) : Dumpable {

    @VisibleForTesting
    internal companion object {
    companion object {
        const val TAG = "PrivacyConfig"
        private const val MIC_CAMERA = SystemUiDeviceConfigFlags.PROPERTY_MIC_CAMERA_ENABLED
        private const val MEDIA_PROJECTION =
            SystemUiDeviceConfigFlags.PROPERTY_MEDIA_PROJECTION_INDICATORS_ENABLED
        private const val DEFAULT_MIC_CAMERA = true
        private const val DEFAULT_MEDIA_PROJECTION = true

        fun getPrivacyColor(locationOnly: Boolean): Int {
            if (locationOnly) {
                return R.color.privacy_chip_location_only_background
            }
            return R.color.privacy_chip_background
        }

        fun privacyItemsAreLocationOnly(privacyItems: List<PrivacyItem>): Boolean {
            return privacyItems.isNotEmpty() &&
                privacyItems.all { it.privacyType == PrivacyType.TYPE_LOCATION }
        }
    }

    private val callbacks = mutableListOf<WeakReference<Callback>>()
Loading