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

Commit fca2fdaa authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Dot suppression" into main

parents 7ffafa2e 89c9e9e9
Loading
Loading
Loading
Loading
+43 −8
Original line number Diff line number Diff line
@@ -29,7 +29,8 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.Flags.FLAG_SHADE_WINDOW_GOES_AROUND
import com.android.systemui.SysuiTestCase
import com.android.systemui.display.data.repository.display
import com.android.systemui.kosmos.backgroundScope
import com.android.systemui.kosmos.runTest
import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.res.R
import com.android.systemui.shade.data.repository.fakeShadeDisplaysRepository
@@ -42,6 +43,7 @@ import com.android.systemui.statusbar.events.PrivacyDotCorner.BottomLeft
import com.android.systemui.statusbar.events.PrivacyDotCorner.BottomRight
import com.android.systemui.statusbar.events.PrivacyDotCorner.TopLeft
import com.android.systemui.statusbar.events.PrivacyDotCorner.TopRight
import com.android.systemui.statusbar.featurepods.vc.domain.interactor.fakeAvControlsChipInteractor
import com.android.systemui.statusbar.layout.StatusBarContentInsetsProvider
import com.android.systemui.statusbar.policy.FakeConfigurationController
import com.android.systemui.testKosmos
@@ -55,10 +57,10 @@ import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.Mockito

@SmallTest
@RunWith(AndroidJUnit4::class)
@@ -67,9 +69,9 @@ class PrivacyDotViewControllerTest : SysuiTestCase() {

    private val kosmos = testKosmos().useUnconfinedTestDispatcher()
    private val mockDisplay = createMockDisplay()
    private val mockAnimationScheduler = mock<SystemStatusAnimationScheduler>()
    private val context = getContext().createDisplayContext(mockDisplay)

    private val testScope = TestScope()
    private val executor = InstantExecutor()
    private val statusBarStateController = FakeStatusBarStateController()
    private val configurationController = FakeConfigurationController()
@@ -91,12 +93,13 @@ class PrivacyDotViewControllerTest : SysuiTestCase() {
    private fun createController() =
        PrivacyDotViewControllerImpl(
            executor,
            testScope.backgroundScope,
            kosmos.backgroundScope,
            statusBarStateController,
            configurationController,
            contentInsetsProvider,
            animationScheduler = mock<SystemStatusAnimationScheduler>(),
            animationScheduler = mockAnimationScheduler,
            shadeInteractor = shadeInteractor,
            avControlsChipInteractor = kosmos.fakeAvControlsChipInteractor,
            uiExecutor = executor,
            displayId = DISPLAY_ID,
            shadeDisplaysInteractor = { shadeDisplaysInteractor },
@@ -317,7 +320,7 @@ class PrivacyDotViewControllerTest : SysuiTestCase() {
    @Test
    @EnableFlags(FLAG_STATUS_BAR_CONNECTED_DISPLAYS, FLAG_SHADE_WINDOW_GOES_AROUND)
    fun init_shadeExpandedOnDifferentDisplay_doesNotChangeShadeExpandedState() =
        testScope.runTest {
        kosmos.runTest {
            shadeDisplaysRepository.setDisplayId(Display.DEFAULT_DISPLAY)
            statusBarStateController.state = SHADE
            statusBarStateController.expanded = true
@@ -332,7 +335,7 @@ class PrivacyDotViewControllerTest : SysuiTestCase() {
    @Test
    @EnableFlags(FLAG_STATUS_BAR_CONNECTED_DISPLAYS, FLAG_SHADE_WINDOW_GOES_AROUND)
    fun init_shadeExpandedOnThisDisplay_doesChangeShadeExpandedState() =
        testScope.runTest {
        kosmos.runTest {
            shadeDisplaysRepository.setDisplayId(Display.DEFAULT_DISPLAY)
            statusBarStateController.state = SHADE
            statusBarStateController.expanded = false
@@ -365,6 +368,38 @@ class PrivacyDotViewControllerTest : SysuiTestCase() {
            .isNotEqualTo(UNSPECIFIED_GRAVITY)
    }

    @Test
    fun initialize_noShow() {
        val controller: PrivacyDotViewController = createAndInitializeController()
        assertThat(controller.currentViewState.shouldShowDot()).isEqualTo(false)
    }

    @Test
    fun initialize_animationFinished_shouldShow() =
        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.
            callback.onSystemStatusAnimationTransitionToPersistentDot(null)
            assertThat(controller.currentViewState.shouldShowDot()).isEqualTo(true)
        }

    @Test
    fun initialize_animationFinished_showingAvChip_noShow() =
        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 = true
            // This informs the controller of an active privacy event.
            callback.onSystemStatusAnimationTransitionToPersistentDot(null)
            assertThat(controller.currentViewState.shouldShowDot()).isEqualTo(false)
        }

    private fun setRotation(rotation: Int) {
        whenever(mockDisplay.rotation).thenReturn(rotation)
    }
+171 −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.statusbar.featurepods.vc.domain.interactor

import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.Flags.FLAG_EXPANDED_PRIVACY_INDICATORS_ON_LARGE_SCREEN
import com.android.systemui.SysuiTestCase
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.collectLastValue
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.shade.data.repository.fakePrivacyChipRepository
import com.android.systemui.statusbar.data.repository.fakeStatusBarModeRepository
import com.android.systemui.statusbar.featurepods.vc.shared.model.AvControlsChipModel
import com.android.systemui.statusbar.featurepods.vc.shared.model.SensorActivityModel
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlin.test.Test
import kotlinx.coroutines.flow.first
import org.junit.Before
import org.junit.runner.RunWith

@SmallTest
@RunWith(AndroidJUnit4::class)
class AvControlsChipInteractorTest() : SysuiTestCase() {
    private val kosmos = testKosmos().useUnconfinedTestDispatcher()
    private val Kosmos.underTest by Kosmos.Fixture { avControlsChipInteractorImpl }

    private val cameraItem =
        PrivacyItem(PrivacyType.TYPE_CAMERA, PrivacyApplication("fakepackage", 0))
    private val microphoneItem =
        PrivacyItem(PrivacyType.TYPE_MICROPHONE, PrivacyApplication("fakepackage", 0))

    private fun cameraModel() =
        AvControlsChipModel(SensorActivityModel.Active(SensorActivityModel.Active.Sensors.CAMERA))

    private fun microphoneModel() =
        AvControlsChipModel(
            SensorActivityModel.Active(SensorActivityModel.Active.Sensors.MICROPHONE)
        )

    private fun cameraAndMicrophoneModel() =
        AvControlsChipModel(
            SensorActivityModel.Active(SensorActivityModel.Active.Sensors.CAMERA_AND_MICROPHONE)
        )

    private fun inactiveModel() = AvControlsChipModel(SensorActivityModel.Inactive)

    private fun Kosmos.lastModel(): AvControlsChipModel? =
        collectLastValue(underTest.model).invoke()

    @Before
    fun setUp() {
        kosmos.underTest.initialize()
    }

    @Test
    @DisableFlags(FLAG_EXPANDED_PRIVACY_INDICATORS_ON_LARGE_SCREEN)
    fun flagOn_disabled() =
        kosmos.runTest { assertThat(underTest.isEnabled.value).isEqualTo(false) }

    @Test
    @EnableFlags(FLAG_EXPANDED_PRIVACY_INDICATORS_ON_LARGE_SCREEN)
    fun flagOn_enabled() = kosmos.runTest { assertThat(underTest.isEnabled.value).isEqualTo(true) }

    @Test fun defaultModel() = kosmos.runTest { assertThat(lastModel()).isEqualTo(inactiveModel()) }

    @Test
    @EnableFlags(FLAG_EXPANDED_PRIVACY_INDICATORS_ON_LARGE_SCREEN)
    fun cameraActiveModel() =
        kosmos.runTest {
            fakePrivacyChipRepository.setPrivacyItems(listOf(cameraItem))
            assertThat(lastModel()).isEqualTo(cameraModel())
        }

    @Test
    @DisableFlags(FLAG_EXPANDED_PRIVACY_INDICATORS_ON_LARGE_SCREEN)
    fun cameraActive_flagOff_InactiveModel() =
        kosmos.runTest {
            fakePrivacyChipRepository.setPrivacyItems(listOf(cameraItem))
            assertThat(lastModel()).isEqualTo(inactiveModel())
        }

    @Test
    @EnableFlags(FLAG_EXPANDED_PRIVACY_INDICATORS_ON_LARGE_SCREEN)
    fun microphoneActiveModel() =
        kosmos.runTest {
            fakePrivacyChipRepository.setPrivacyItems(listOf(microphoneItem))
            assertThat(lastModel()).isEqualTo(microphoneModel())
        }

    @Test
    @DisableFlags(FLAG_EXPANDED_PRIVACY_INDICATORS_ON_LARGE_SCREEN)
    fun microphoneActive_flagOff_InactiveModel() =
        kosmos.runTest {
            fakePrivacyChipRepository.setPrivacyItems(listOf(microphoneItem))
            assertThat(lastModel()).isEqualTo(inactiveModel())
        }

    @Test
    @EnableFlags(FLAG_EXPANDED_PRIVACY_INDICATORS_ON_LARGE_SCREEN)
    fun cameraAndMicrophoneActiveModel() =
        kosmos.runTest {
            fakePrivacyChipRepository.setPrivacyItems(listOf(cameraItem, microphoneItem))
            assertThat(lastModel()).isEqualTo(cameraAndMicrophoneModel())
        }

    @Test
    @DisableFlags(FLAG_EXPANDED_PRIVACY_INDICATORS_ON_LARGE_SCREEN)
    fun cameraAndMicrophoneActive_flagOff_InactiveModel() =
        kosmos.runTest {
            fakePrivacyChipRepository.setPrivacyItems(listOf(cameraItem, microphoneItem))
            assertThat(lastModel()).isEqualTo(inactiveModel())
        }

    @Test
    @EnableFlags(FLAG_EXPANDED_PRIVACY_INDICATORS_ON_LARGE_SCREEN)
    fun cameraActive_noFullscreen_shouldSuppressDot() =
        kosmos.runTest {
            fakePrivacyChipRepository.setPrivacyItems(listOf(cameraItem))
            fakeStatusBarModeRepository.defaultDisplay.isInFullscreenMode.value = false
            assertThat(underTest.isShowingAvChip.first()).isEqualTo(true)
        }

    @Test
    @EnableFlags(FLAG_EXPANDED_PRIVACY_INDICATORS_ON_LARGE_SCREEN)
    fun cameraActive_fullscreen_shouldNotSuppressDot() =
        kosmos.runTest {
            fakePrivacyChipRepository.setPrivacyItems(listOf(cameraItem))
            fakeStatusBarModeRepository.defaultDisplay.isInFullscreenMode.value = true
            assertThat(underTest.isShowingAvChip.first()).isEqualTo(false)
        }

    @Test
    @DisableFlags(FLAG_EXPANDED_PRIVACY_INDICATORS_ON_LARGE_SCREEN)
    fun cameraActive_flagOff_noFullscreen_shouldNotSuppressDot() =
        kosmos.runTest {
            fakePrivacyChipRepository.setPrivacyItems(listOf(cameraItem))
            fakeStatusBarModeRepository.defaultDisplay.isInFullscreenMode.value = false
            assertThat(underTest.isShowingAvChip.first()).isEqualTo(false)
        }

    @Test
    @DisableFlags(FLAG_EXPANDED_PRIVACY_INDICATORS_ON_LARGE_SCREEN)
    fun cameraActive_flagOff_fullscreen_shouldNotSuppressDot() =
        kosmos.runTest {
            fakePrivacyChipRepository.setPrivacyItems(listOf(cameraItem))
            fakeStatusBarModeRepository.defaultDisplay.isInFullscreenMode.value = true
            assertThat(underTest.isShowingAvChip.first()).isEqualTo(false)
        }
}
+2 −3
Original line number Diff line number Diff line
@@ -34,7 +34,7 @@ import com.android.systemui.statusbar.featurepods.av.ui.viewmodel.AvControlsChip
import com.android.systemui.statusbar.featurepods.popups.ui.model.ChipIcon
import com.android.systemui.statusbar.featurepods.popups.ui.model.PopupChipId
import com.android.systemui.statusbar.featurepods.popups.ui.model.PopupChipModel
import com.android.systemui.statusbar.featurepods.vc.domain.interactor.avControlsChipInteractor
import com.android.systemui.statusbar.featurepods.vc.domain.interactor.avControlsChipInteractorImpl
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlin.test.Test
@@ -46,7 +46,6 @@ import org.junit.runner.RunWith
class AvControlsChipViewModelTest() : SysuiTestCase() {
    private val kosmos = testKosmos().useUnconfinedTestDispatcher()
    private val underTest = kosmos.avControlsChipViewModelFactory.create()
    private val avControlsChipInteractor by lazy { kosmos.avControlsChipInteractor }
    private val cameraItem =
        PrivacyItem(PrivacyType.TYPE_CAMERA, PrivacyApplication("fakepackage", 0))
    private val microphoneItem =
@@ -54,7 +53,7 @@ class AvControlsChipViewModelTest() : SysuiTestCase() {

    @Before
    fun setUp() {
        avControlsChipInteractor.initialize()
        kosmos.avControlsChipInteractorImpl.initialize()
        underTest.activateIn(kosmos.testScope)
    }

+2 −0
Original line number Diff line number Diff line
@@ -132,6 +132,7 @@ import com.android.systemui.statusbar.dagger.StatusBarModule;
import com.android.systemui.statusbar.disableflags.dagger.DisableFlagsModule;
import com.android.systemui.statusbar.events.StatusBarEventsModule;
import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler;
import com.android.systemui.statusbar.featurepods.vc.AvControlsChipModule;
import com.android.systemui.statusbar.notification.NotifPipelineFlags;
import com.android.systemui.statusbar.notification.collection.NotifPipeline;
import com.android.systemui.statusbar.notification.collection.inflation.NotificationRowBinder;
@@ -210,6 +211,7 @@ import javax.inject.Named;
@Module(includes = {
        ActivityManagerModule.class,
        AmbientModule.class,
        AvControlsChipModule.class,
        AppOpsModule.class,
        AssistModule.class,
        AuthenticationModule.class,
+15 −1
Original line number Diff line number Diff line
@@ -44,6 +44,7 @@ import com.android.systemui.statusbar.events.PrivacyDotCorner.BottomLeft
import com.android.systemui.statusbar.events.PrivacyDotCorner.BottomRight
import com.android.systemui.statusbar.events.PrivacyDotCorner.TopLeft
import com.android.systemui.statusbar.events.PrivacyDotCorner.TopRight
import com.android.systemui.statusbar.featurepods.vc.domain.interactor.AvControlsChipInteractor
import com.android.systemui.statusbar.layout.StatusBarContentInsetsChangedListener
import com.android.systemui.statusbar.layout.StatusBarContentInsetsProvider
import com.android.systemui.statusbar.policy.ConfigurationController
@@ -123,6 +124,7 @@ constructor(
    @Assisted private val contentInsetsProvider: StatusBarContentInsetsProvider,
    private val animationScheduler: SystemStatusAnimationScheduler,
    shadeInteractor: ShadeInteractor?,
    avControlsChipInteractor: AvControlsChipInteractor?,
    @ScreenDecorationsThread val uiExecutor: DelayableExecutor,
    @Assisted private val displayId: Int,
    private val shadeDisplaysInteractor: Lazy<ShadeDisplaysInteractor>?,
@@ -189,6 +191,14 @@ constructor(
        contentInsetsProvider.addCallback(insetsChangedListener)
        configurationController.addCallback(configurationListener)
        stateController.addCallback(statusBarStateListener)
        scope.launch {
            avControlsChipInteractor?.isShowingAvChip?.collect { shouldSuppress ->
                synchronized(lock) {
                    nextViewState =
                        nextViewState.copy(dotDuplicatedByAvControlsChip = shouldSuppress)
                }
            }
        }
        scope.launch {
            if (
                StatusBarConnectedDisplays.isEnabled &&
@@ -672,6 +682,7 @@ data class ViewState(
    val systemPrivacyEventIsActive: Boolean = false,
    val shadeExpanded: Boolean = false,
    val qsExpanded: Boolean = false,
    val dotDuplicatedByAvControlsChip: Boolean = false,
    val portraitRect: Rect? = null,
    val landscapeRect: Rect? = null,
    val upsideDownRect: Rect? = null,
@@ -684,7 +695,10 @@ data class ViewState(
    val contentDescription: String? = null,
) {
    fun shouldShowDot(): Boolean {
        return systemPrivacyEventIsActive && !shadeExpanded && !qsExpanded
        return systemPrivacyEventIsActive &&
            !shadeExpanded &&
            !qsExpanded &&
            !dotDuplicatedByAvControlsChip
    }

    fun needsLayout(other: ViewState): Boolean {
Loading