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

Commit 89c9e9e9 authored by Jan Lanik's avatar Jan Lanik
Browse files

Dot suppression

Suppressing the privacy dot outside of the full-screen mode.
Additionally - ading some unit tests.

Bug: 394074664
Flag: com.android.systemui.expanded_privacy_indicators_on_large_screen
Test: Unit tests
Change-Id: Ie6613b0b431f12f088af3e62283084e7ce54de42
parent cdf76490
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