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

Commit fc74f979 authored by Jan Lanik's avatar Jan Lanik Committed by Jan Láník
Browse files

VCPrivacy status chip

Adding a status bar chip that indicates the camera/microphone activity
on large devices.

Flag: com.android.systemui.expanded_privacy_indicators_on_large_screen
Test: Tested manually and added unit tests
Bug: 394074664
Change-Id: I0f1b6c1e9557144e50deb7906c7b0c73019f87e4
parent 7aa54bb1
Loading
Loading
Loading
Loading
+139 −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.ui.viewmodel

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.runTest
import com.android.systemui.kosmos.testScope
import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.lifecycle.activateIn
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.featurepods.popups.shared.model.PopupChipId
import com.android.systemui.statusbar.featurepods.popups.shared.model.PopupChipModel
import com.android.systemui.statusbar.featurepods.vc.domain.interactor.avControlsChipInteractor
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlin.test.Test
import org.junit.Before
import org.junit.runner.RunWith

@SmallTest
@RunWith(AndroidJUnit4::class)
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 =
        PrivacyItem(PrivacyType.TYPE_MICROPHONE, PrivacyApplication("fakepackage", 0))

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

    @Test
    @EnableFlags(FLAG_EXPANDED_PRIVACY_INDICATORS_ON_LARGE_SCREEN)
    fun avControlsChip_initialState_isHidden() = kosmos.runTest { underTest.chip.verifyHidden() }

    @Test
    @EnableFlags(FLAG_EXPANDED_PRIVACY_INDICATORS_ON_LARGE_SCREEN)
    fun avControlsChip_showingCamera_chipVisible() =
        kosmos.runTest {
            fakePrivacyChipRepository.setPrivacyItems(listOf(cameraItem))
            underTest.chip.verifyShown().verifyHasText("Camera")
        }

    @Test
    @EnableFlags(FLAG_EXPANDED_PRIVACY_INDICATORS_ON_LARGE_SCREEN)
    fun avControlsChip_showingMicrophone_chipVisible() =
        kosmos.runTest {
            fakePrivacyChipRepository.setPrivacyItems(listOf(microphoneItem))
            underTest.chip.verifyShown().verifyHasText("Microphone")
        }

    @Test
    @EnableFlags(FLAG_EXPANDED_PRIVACY_INDICATORS_ON_LARGE_SCREEN)
    fun avControlsChip_showingCameraAndMicrophone_chipVisible() =
        kosmos.runTest {
            fakePrivacyChipRepository.setPrivacyItems(listOf(cameraItem, microphoneItem))
            underTest.chip.verifyShown().verifyHasText("Cam & Mic")
        }

    @Test
    @EnableFlags(FLAG_EXPANDED_PRIVACY_INDICATORS_ON_LARGE_SCREEN)
    fun avControlsChip_chipUpdates() =
        kosmos.runTest {
            underTest.chip.verifyHidden()

            fakePrivacyChipRepository.setPrivacyItems(listOf(cameraItem))
            underTest.chip.verifyShown().verifyHasText("Camera")

            fakePrivacyChipRepository.setPrivacyItems(listOf())
            underTest.chip.verifyHidden()

            fakePrivacyChipRepository.setPrivacyItems(listOf(microphoneItem))
            underTest.chip.verifyShown().verifyHasText("Microphone")

            fakePrivacyChipRepository.setPrivacyItems(listOf(microphoneItem, cameraItem))
            underTest.chip.verifyShown().verifyHasText("Cam & Mic")
        }

    @Test
    fun avControlsChip_flagNotEnabled_isHidden() =
        kosmos.runTest {
            underTest.chip.verifyHidden()
            fakePrivacyChipRepository.setPrivacyItems(listOf(cameraItem))

            underTest.chip.verifyHidden()
            fakePrivacyChipRepository.setPrivacyItems(listOf())

            underTest.chip.verifyHidden()
            fakePrivacyChipRepository.setPrivacyItems(listOf(microphoneItem))

            underTest.chip.verifyHidden()
            fakePrivacyChipRepository.setPrivacyItems(listOf(microphoneItem, cameraItem))

            underTest.chip.verifyHidden()
        }
}

private fun PopupChipModel.verifyHidden(): PopupChipModel.Hidden {
    assertThat(this.chipId).isEqualTo(PopupChipId.AvControlsIndicator)
    assertThat(this).isInstanceOf(PopupChipModel.Hidden::class.java)
    return this as PopupChipModel.Hidden
}

private fun PopupChipModel.verifyShown(): PopupChipModel.Shown {
    assertThat(this.chipId).isEqualTo(PopupChipId.AvControlsIndicator)
    assertThat(this).isInstanceOf(PopupChipModel.Shown::class.java)
    return this as PopupChipModel.Shown
}

private fun PopupChipModel.Shown.verifyHasText(text: String?): PopupChipModel.Shown {
    assertThat(this.chipText).isEqualTo(text)
    return this
}
+43 −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

import com.android.systemui.CoreStartable
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.statusbar.featurepods.vc.domain.interactor.AvControlsChipInteractor
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch

/**
 * A [CoreStartable] that initializes and starts the VC/Privacy control chip functionality. The chip
 * is limited to large screen devices currently. Therefore, this [CoreStartable] should not be used
 * for phones or smaller form factor devices.
 */
@SysUISingleton
class AvControlsChipStartable
@Inject
constructor(
    @Background val bgScope: CoroutineScope,
    private val avControlsChipInteractor: AvControlsChipInteractor,
) : CoreStartable {

    override fun start() {
        bgScope.launch { avControlsChipInteractor.initialize() }
    }
}
+80 −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 com.android.systemui.Flags
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.privacy.PrivacyItem
import com.android.systemui.privacy.PrivacyType
import com.android.systemui.shade.data.repository.PrivacyChipRepository
import com.android.systemui.statusbar.featurepods.vc.shared.model.AvControlsChipModel
import com.android.systemui.statusbar.featurepods.vc.shared.model.SensorActivityModel
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine

/**
 * Interactor for managing the state of the video conference privacy chip in the status bar.
 *
 * Provides a [Flow] of [AvControlsChipModel] representing the current state of the media control
 * chip.
 *
 * This functionality is only enabled on large screen devices.
 */
@SysUISingleton
class AvControlsChipInteractor @Inject constructor(privacyChipRepository: PrivacyChipRepository) {
    private val isEnabled = MutableStateFlow(false)

    val model =
        combine(isEnabled, privacyChipRepository.privacyItems) { isEnabled, privacyItems ->
            if (isEnabled) createModel(privacyItems)
            else AvControlsChipModel(sensorActivityModel = SensorActivityModel.Inactive())
        }

    private fun createModel(privacyItems: List<PrivacyItem>): AvControlsChipModel {
        return AvControlsChipModel(
            sensorActivityModel =
                createSensorActivityModel(
                    cameraActive = privacyItems.any { it.privacyType == PrivacyType.TYPE_CAMERA },
                    microphoneActive =
                        privacyItems.any { it.privacyType == PrivacyType.TYPE_MICROPHONE },
                )
        )
    }

    private fun createSensorActivityModel(
        cameraActive: Boolean,
        microphoneActive: Boolean,
    ): SensorActivityModel =
        when {
            !cameraActive && microphoneActive ->
                SensorActivityModel.Active(SensorActivityModel.Active.Sensors.MICROPHONE)
            cameraActive && !microphoneActive ->
                SensorActivityModel.Active(SensorActivityModel.Active.Sensors.CAMERA)
            cameraActive && microphoneActive ->
                SensorActivityModel.Active(SensorActivityModel.Active.Sensors.CAMERA_AND_MICROPHONE)
            else -> SensorActivityModel.Inactive()
        }

    /**
     * The VC/Privacy control chip may not be enabled on all form factors, so only the relevant form
     * factors should initialize the interactor. This must be called from a CoreStartable.
     */
    fun initialize() {
        isEnabled.value = Flags.expandedPrivacyIndicatorsOnLargeScreen()
    }
}
+25 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.shared.model

/**
 * Model used to display a VC/Privacy control chip in the status bar.
 *
 * The class currently wraps only the SensorActivityModel, however in future it is intended to
 * contain more elements as we add functionality into the status bar chip.
 */
data class AvControlsChipModel(val sensorActivityModel: SensorActivityModel)
+30 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.shared.model

/** Model that describes which sensors are active */
sealed class SensorActivityModel {
    class Inactive : SensorActivityModel()

    data class Active(val sensors: Sensors) : SensorActivityModel() {
        enum class Sensors {
            CAMERA,
            MICROPHONE,
            CAMERA_AND_MICROPHONE,
        }
    }
}
Loading