Loading packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/sensorprivacy/domain/interactor/SensorPrivacyToggleTileDataInteractorTest.kt 0 → 100644 +102 −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.qs.tiles.impl.sensorprivacy.domain.interactor import android.hardware.SensorPrivacyManager.Sensors.CAMERA import android.hardware.SensorPrivacyManager.Sensors.MICROPHONE import android.os.UserHandle import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testScope import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger import com.android.systemui.qs.tiles.impl.sensorprivacy.SensorPrivacyToggleTileDataInteractor import com.android.systemui.statusbar.policy.IndividualSensorPrivacyController import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.toCollection import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.eq import org.mockito.Mockito.verify @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) class SensorPrivacyToggleTileDataInteractorTest : SysuiTestCase() { private val kosmos = Kosmos() private val testScope = kosmos.testScope private val mockSensorPrivacyController = mock<IndividualSensorPrivacyController> { whenever(isSensorBlocked(eq(CAMERA))).thenReturn(false) // determines initial value } private val testUser = UserHandle.of(1) private val underTest = SensorPrivacyToggleTileDataInteractor( testScope.testScheduler, mockSensorPrivacyController, CAMERA ) @Test fun availability_isTrue() = testScope.runTest { whenever(mockSensorPrivacyController.supportsSensorToggle(eq(CAMERA))).thenReturn(true) val availability = underTest.availability(testUser).toCollection(mutableListOf()) runCurrent() assertThat(availability).hasSize(1) assertThat(availability.last()).isTrue() } @Test fun tileData_matchesPrivacyControllerIsSensorBlocked() = testScope.runTest { val callbackCaptor = argumentCaptor<IndividualSensorPrivacyController.Callback>() val data by collectLastValue( underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest)) ) runCurrent() verify(mockSensorPrivacyController).addCallback(callbackCaptor.capture()) val callback = callbackCaptor.value runCurrent() assertThat(data!!.isBlocked).isFalse() callback.onSensorBlockedChanged(CAMERA, true) runCurrent() assertThat(data!!.isBlocked).isTrue() callback.onSensorBlockedChanged(CAMERA, false) runCurrent() assertThat(data!!.isBlocked).isFalse() callback.onSensorBlockedChanged(MICROPHONE, true) runCurrent() assertThat(data!!.isBlocked).isFalse() // We're NOT listening for MIC sensor changes } } packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/sensorprivacy/domain/interactor/SensorPrivacyToggleTileUserActionInteractorTest.kt 0 → 100644 +168 −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.qs.tiles.impl.sensorprivacy.domain.interactor import android.hardware.SensorPrivacyManager import android.hardware.SensorPrivacyManager.Sensors.CAMERA import android.hardware.SensorPrivacyManager.Sensors.MICROPHONE import android.provider.Settings import android.safetycenter.SafetyCenterManager import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.plugins.activityStarter import com.android.systemui.qs.tiles.base.actions.FakeQSTileIntentUserInputHandler import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandlerSubject import com.android.systemui.qs.tiles.base.interactor.QSTileInputTestKtx import com.android.systemui.qs.tiles.impl.sensorprivacy.domain.SensorPrivacyToggleTileUserActionInteractor import com.android.systemui.qs.tiles.impl.sensorprivacy.domain.model.SensorPrivacyToggleTileModel import com.android.systemui.statusbar.policy.IndividualSensorPrivacyController import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.eq import org.mockito.Mockito.never import org.mockito.Mockito.verify @SmallTest @RunWith(AndroidJUnit4::class) class SensorPrivacyToggleTileUserActionInteractorTest : SysuiTestCase() { private val kosmos = Kosmos() private val inputHandler = FakeQSTileIntentUserInputHandler() private val keyguardInteractor = kosmos.keyguardInteractor // The keyguard repository below is the same one kosmos used to create the interactor above private val keyguardRepository = kosmos.fakeKeyguardRepository private val mockActivityStarter = kosmos.activityStarter private val mockSensorPrivacyController = mock<IndividualSensorPrivacyController>() private val fakeSafetyCenterManager = mock<SafetyCenterManager>() private val underTest = SensorPrivacyToggleTileUserActionInteractor( inputHandler, keyguardInteractor, mockActivityStarter, mockSensorPrivacyController, fakeSafetyCenterManager, CAMERA ) @Test fun handleClickWhenNotBlocked() = runTest { val originalIsBlocked = false underTest.handleInput( QSTileInputTestKtx.click(SensorPrivacyToggleTileModel(originalIsBlocked)) ) verify(mockSensorPrivacyController) .setSensorBlocked( eq(SensorPrivacyManager.Sources.QS_TILE), eq(CAMERA), eq(!originalIsBlocked) ) } @Test fun handleClickWhenBlocked() = runTest { val originalIsBlocked = true underTest.handleInput( QSTileInputTestKtx.click(SensorPrivacyToggleTileModel(originalIsBlocked)) ) verify(mockSensorPrivacyController) .setSensorBlocked( eq(SensorPrivacyManager.Sources.QS_TILE), eq(CAMERA), eq(!originalIsBlocked) ) } @Test fun handleClick_whenKeyguardIsDismissableAndShowing_whenControllerRequiresAuth() = runTest { whenever(mockSensorPrivacyController.requiresAuthentication()).thenReturn(true) keyguardRepository.setKeyguardDismissible(true) keyguardRepository.setKeyguardShowing(true) val originalIsBlocked = true underTest.handleInput( QSTileInputTestKtx.click(SensorPrivacyToggleTileModel(originalIsBlocked)) ) verify(mockSensorPrivacyController, never()) .setSensorBlocked( eq(SensorPrivacyManager.Sources.QS_TILE), eq(CAMERA), eq(!originalIsBlocked) ) verify(mockActivityStarter).postQSRunnableDismissingKeyguard(any()) } @Test fun handleLongClick_whenSafetyManagerEnabled_privacyControlsIntent() = runTest { whenever(fakeSafetyCenterManager.isSafetyCenterEnabled).thenReturn(true) underTest.handleInput(QSTileInputTestKtx.longClick(SensorPrivacyToggleTileModel(false))) QSTileIntentUserInputHandlerSubject.assertThat(inputHandler).handledOneIntentInput { assertThat(it.intent.action).isEqualTo(Settings.ACTION_PRIVACY_CONTROLS) } } @Test fun handleLongClick_whenSafetyManagerDisabled_privacySettingsIntent() = runTest { whenever(fakeSafetyCenterManager.isSafetyCenterEnabled).thenReturn(false) underTest.handleInput(QSTileInputTestKtx.longClick(SensorPrivacyToggleTileModel(false))) QSTileIntentUserInputHandlerSubject.assertThat(inputHandler).handledOneIntentInput { assertThat(it.intent.action).isEqualTo(Settings.ACTION_PRIVACY_SETTINGS) } } @Test fun handleClick_microphone_flipsController() = runTest { val micUserActionInteractor = SensorPrivacyToggleTileUserActionInteractor( inputHandler, keyguardInteractor, mockActivityStarter, mockSensorPrivacyController, fakeSafetyCenterManager, MICROPHONE ) micUserActionInteractor.handleInput( QSTileInputTestKtx.click(SensorPrivacyToggleTileModel(false)) ) verify(mockSensorPrivacyController) .setSensorBlocked(eq(SensorPrivacyManager.Sources.QS_TILE), eq(MICROPHONE), eq(true)) micUserActionInteractor.handleInput( QSTileInputTestKtx.click(SensorPrivacyToggleTileModel(true)) ) verify(mockSensorPrivacyController) .setSensorBlocked(eq(SensorPrivacyManager.Sources.QS_TILE), eq(MICROPHONE), eq(false)) } } packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/sensorprivacy/ui/SensorPrivacyToggleTileMapperTest.kt 0 → 100644 +161 −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.qs.tiles.impl.sensorprivacy.ui import android.graphics.drawable.TestStubDrawable import android.hardware.SensorPrivacyManager.Sensors.CAMERA import android.hardware.SensorPrivacyManager.Sensors.MICROPHONE import android.widget.Switch import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.common.shared.model.Icon import com.android.systemui.kosmos.Kosmos import com.android.systemui.qs.tiles.impl.custom.QSTileStateSubject import com.android.systemui.qs.tiles.impl.sensorprivacy.domain.model.SensorPrivacyToggleTileModel import com.android.systemui.qs.tiles.impl.sensorprivacy.qsCameraSensorPrivacyToggleTileConfig import com.android.systemui.qs.tiles.impl.sensorprivacy.qsMicrophoneSensorPrivacyToggleTileConfig import com.android.systemui.qs.tiles.impl.sensorprivacy.ui.SensorPrivacyTileResources.CameraPrivacyTileResources import com.android.systemui.qs.tiles.impl.sensorprivacy.ui.SensorPrivacyTileResources.MicrophonePrivacyTileResources import com.android.systemui.qs.tiles.viewmodel.QSTileState import com.android.systemui.res.R import org.junit.Test import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) class SensorPrivacyToggleTileMapperTest : SysuiTestCase() { private val kosmos = Kosmos() private val cameraConfig = kosmos.qsCameraSensorPrivacyToggleTileConfig private val micConfig = kosmos.qsMicrophoneSensorPrivacyToggleTileConfig @Test fun mapCamera_notBlocked() { val mapper = createMapper(CameraPrivacyTileResources) val inputModel = SensorPrivacyToggleTileModel(false) val outputState = mapper.map(cameraConfig, inputModel) val expectedState = createSensorPrivacyToggleTileState( QSTileState.ActivationState.ACTIVE, context.getString(R.string.quick_settings_camera_mic_available), R.drawable.qs_camera_access_icon_on, null, CAMERA ) QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState) } @Test fun mapCamera_blocked() { val mapper = createMapper(CameraPrivacyTileResources) val inputModel = SensorPrivacyToggleTileModel(true) val outputState = mapper.map(cameraConfig, inputModel) val expectedState = createSensorPrivacyToggleTileState( QSTileState.ActivationState.INACTIVE, context.getString(R.string.quick_settings_camera_mic_blocked), R.drawable.qs_camera_access_icon_off, null, CAMERA ) QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState) } @Test fun mapMic_notBlocked() { val mapper = createMapper(MicrophonePrivacyTileResources) val inputModel = SensorPrivacyToggleTileModel(false) val outputState = mapper.map(micConfig, inputModel) val expectedState = createSensorPrivacyToggleTileState( QSTileState.ActivationState.ACTIVE, context.getString(R.string.quick_settings_camera_mic_available), R.drawable.qs_mic_access_on, null, MICROPHONE ) QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState) } @Test fun mapMic_blocked() { val mapper = createMapper(MicrophonePrivacyTileResources) val inputModel = SensorPrivacyToggleTileModel(true) val outputState = mapper.map(micConfig, inputModel) val expectedState = createSensorPrivacyToggleTileState( QSTileState.ActivationState.INACTIVE, context.getString(R.string.quick_settings_camera_mic_blocked), R.drawable.qs_mic_access_off, null, MICROPHONE ) QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState) } private fun createMapper( sensorResources: SensorPrivacyTileResources ): SensorPrivacyToggleTileMapper { val mapper = SensorPrivacyToggleTileMapper( context.orCreateTestableResources .apply { addOverride(R.drawable.qs_camera_access_icon_off, TestStubDrawable()) addOverride(R.drawable.qs_camera_access_icon_on, TestStubDrawable()) addOverride(R.drawable.qs_mic_access_off, TestStubDrawable()) addOverride(R.drawable.qs_mic_access_on, TestStubDrawable()) } .resources, context.theme, sensorResources, ) return mapper } private fun createSensorPrivacyToggleTileState( activationState: QSTileState.ActivationState, secondaryLabel: String, iconRes: Int, stateDescription: CharSequence?, sensorId: Int, ): QSTileState { val label = if (sensorId == CAMERA) context.getString(R.string.quick_settings_camera_label) else context.getString(R.string.quick_settings_mic_label) return QSTileState( { Icon.Loaded(context.getDrawable(iconRes)!!, null) }, label, activationState, secondaryLabel, setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK), label, stateDescription, QSTileState.SideViewIcon.None, QSTileState.EnabledState.ENABLED, Switch::class.qualifiedName ) } } packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt +1 −1 Original line number Diff line number Diff line Loading @@ -193,7 +193,7 @@ constructor( val isKeyguardShowing: Flow<Boolean> = repository.isKeyguardShowing /** Whether the keyguard is dismissible or not. */ val isKeyguardDismissible: Flow<Boolean> = repository.isKeyguardDismissible val isKeyguardDismissible: StateFlow<Boolean> = repository.isKeyguardDismissible /** Whether the keyguard is occluded (covered by an activity). */ @Deprecated("Use KeyguardTransitionInteractor + KeyguardState.OCCLUDED") Loading packages/SystemUI/src/com/android/systemui/qs/tiles/impl/sensorprivacy/SensorPrivacyToggleTileDataInteractor.kt 0 → 100644 +114 −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.qs.tiles.impl.sensorprivacy import android.hardware.SensorPrivacyManager.Sensors.CAMERA import android.hardware.SensorPrivacyManager.Sensors.MICROPHONE import android.hardware.SensorPrivacyManager.Sensors.Sensor import android.os.UserHandle import android.provider.DeviceConfig import android.util.Log import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor import com.android.systemui.qs.tiles.impl.sensorprivacy.domain.model.SensorPrivacyToggleTileModel import com.android.systemui.statusbar.policy.IndividualSensorPrivacyController import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.withContext /** Observes SensorPrivacyToggle mode state changes providing the [SensorPrivacyToggleTileModel]. */ class SensorPrivacyToggleTileDataInteractor @AssistedInject constructor( @Background private val bgCoroutineContext: CoroutineContext, private val privacyController: IndividualSensorPrivacyController, @Assisted @Sensor private val sensorId: Int, ) : QSTileDataInteractor<SensorPrivacyToggleTileModel> { @AssistedFactory interface Factory { fun create(@Sensor id: Int): SensorPrivacyToggleTileDataInteractor } override fun tileData( user: UserHandle, triggers: Flow<DataUpdateTrigger> ): Flow<SensorPrivacyToggleTileModel> = conflatedCallbackFlow { val callback = IndividualSensorPrivacyController.Callback { sensor, blocked -> if (sensor == sensorId) trySend(SensorPrivacyToggleTileModel(blocked)) } privacyController.addCallback(callback) // does not emit an initial state awaitClose { privacyController.removeCallback(callback) } } .onStart { emit(SensorPrivacyToggleTileModel(privacyController.isSensorBlocked(sensorId))) } .distinctUntilChanged() .flowOn(bgCoroutineContext) override fun availability(user: UserHandle) = flow { emit(isAvailable()) }.flowOn(bgCoroutineContext) private suspend fun isAvailable(): Boolean { return privacyController.supportsSensorToggle(sensorId) && isSensorDeviceConfigSet() } private suspend fun isSensorDeviceConfigSet(): Boolean = withContext(bgCoroutineContext) { try { val deviceConfigName = getDeviceConfigName(sensorId) return@withContext DeviceConfig.getBoolean( DeviceConfig.NAMESPACE_PRIVACY, deviceConfigName, true ) } catch (exception: IllegalArgumentException) { Log.w( TAG, "isDeviceConfigSet for sensorId $sensorId: " + "Defaulting to true due to exception. ", exception ) return@withContext true } } private fun getDeviceConfigName(sensorId: Int): String { if (sensorId == MICROPHONE) { return "mic_toggle_enabled" } else if (sensorId == CAMERA) { return "camera_toggle_enabled" } else { throw IllegalArgumentException("getDeviceConfigName: unexpected sensorId: $sensorId") } } private companion object { const val TAG = "SensorPrivacyToggleTileException" } } Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/sensorprivacy/domain/interactor/SensorPrivacyToggleTileDataInteractorTest.kt 0 → 100644 +102 −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.qs.tiles.impl.sensorprivacy.domain.interactor import android.hardware.SensorPrivacyManager.Sensors.CAMERA import android.hardware.SensorPrivacyManager.Sensors.MICROPHONE import android.os.UserHandle import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testScope import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger import com.android.systemui.qs.tiles.impl.sensorprivacy.SensorPrivacyToggleTileDataInteractor import com.android.systemui.statusbar.policy.IndividualSensorPrivacyController import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.toCollection import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.eq import org.mockito.Mockito.verify @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) class SensorPrivacyToggleTileDataInteractorTest : SysuiTestCase() { private val kosmos = Kosmos() private val testScope = kosmos.testScope private val mockSensorPrivacyController = mock<IndividualSensorPrivacyController> { whenever(isSensorBlocked(eq(CAMERA))).thenReturn(false) // determines initial value } private val testUser = UserHandle.of(1) private val underTest = SensorPrivacyToggleTileDataInteractor( testScope.testScheduler, mockSensorPrivacyController, CAMERA ) @Test fun availability_isTrue() = testScope.runTest { whenever(mockSensorPrivacyController.supportsSensorToggle(eq(CAMERA))).thenReturn(true) val availability = underTest.availability(testUser).toCollection(mutableListOf()) runCurrent() assertThat(availability).hasSize(1) assertThat(availability.last()).isTrue() } @Test fun tileData_matchesPrivacyControllerIsSensorBlocked() = testScope.runTest { val callbackCaptor = argumentCaptor<IndividualSensorPrivacyController.Callback>() val data by collectLastValue( underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest)) ) runCurrent() verify(mockSensorPrivacyController).addCallback(callbackCaptor.capture()) val callback = callbackCaptor.value runCurrent() assertThat(data!!.isBlocked).isFalse() callback.onSensorBlockedChanged(CAMERA, true) runCurrent() assertThat(data!!.isBlocked).isTrue() callback.onSensorBlockedChanged(CAMERA, false) runCurrent() assertThat(data!!.isBlocked).isFalse() callback.onSensorBlockedChanged(MICROPHONE, true) runCurrent() assertThat(data!!.isBlocked).isFalse() // We're NOT listening for MIC sensor changes } }
packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/sensorprivacy/domain/interactor/SensorPrivacyToggleTileUserActionInteractorTest.kt 0 → 100644 +168 −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.qs.tiles.impl.sensorprivacy.domain.interactor import android.hardware.SensorPrivacyManager import android.hardware.SensorPrivacyManager.Sensors.CAMERA import android.hardware.SensorPrivacyManager.Sensors.MICROPHONE import android.provider.Settings import android.safetycenter.SafetyCenterManager import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.plugins.activityStarter import com.android.systemui.qs.tiles.base.actions.FakeQSTileIntentUserInputHandler import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandlerSubject import com.android.systemui.qs.tiles.base.interactor.QSTileInputTestKtx import com.android.systemui.qs.tiles.impl.sensorprivacy.domain.SensorPrivacyToggleTileUserActionInteractor import com.android.systemui.qs.tiles.impl.sensorprivacy.domain.model.SensorPrivacyToggleTileModel import com.android.systemui.statusbar.policy.IndividualSensorPrivacyController import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.eq import org.mockito.Mockito.never import org.mockito.Mockito.verify @SmallTest @RunWith(AndroidJUnit4::class) class SensorPrivacyToggleTileUserActionInteractorTest : SysuiTestCase() { private val kosmos = Kosmos() private val inputHandler = FakeQSTileIntentUserInputHandler() private val keyguardInteractor = kosmos.keyguardInteractor // The keyguard repository below is the same one kosmos used to create the interactor above private val keyguardRepository = kosmos.fakeKeyguardRepository private val mockActivityStarter = kosmos.activityStarter private val mockSensorPrivacyController = mock<IndividualSensorPrivacyController>() private val fakeSafetyCenterManager = mock<SafetyCenterManager>() private val underTest = SensorPrivacyToggleTileUserActionInteractor( inputHandler, keyguardInteractor, mockActivityStarter, mockSensorPrivacyController, fakeSafetyCenterManager, CAMERA ) @Test fun handleClickWhenNotBlocked() = runTest { val originalIsBlocked = false underTest.handleInput( QSTileInputTestKtx.click(SensorPrivacyToggleTileModel(originalIsBlocked)) ) verify(mockSensorPrivacyController) .setSensorBlocked( eq(SensorPrivacyManager.Sources.QS_TILE), eq(CAMERA), eq(!originalIsBlocked) ) } @Test fun handleClickWhenBlocked() = runTest { val originalIsBlocked = true underTest.handleInput( QSTileInputTestKtx.click(SensorPrivacyToggleTileModel(originalIsBlocked)) ) verify(mockSensorPrivacyController) .setSensorBlocked( eq(SensorPrivacyManager.Sources.QS_TILE), eq(CAMERA), eq(!originalIsBlocked) ) } @Test fun handleClick_whenKeyguardIsDismissableAndShowing_whenControllerRequiresAuth() = runTest { whenever(mockSensorPrivacyController.requiresAuthentication()).thenReturn(true) keyguardRepository.setKeyguardDismissible(true) keyguardRepository.setKeyguardShowing(true) val originalIsBlocked = true underTest.handleInput( QSTileInputTestKtx.click(SensorPrivacyToggleTileModel(originalIsBlocked)) ) verify(mockSensorPrivacyController, never()) .setSensorBlocked( eq(SensorPrivacyManager.Sources.QS_TILE), eq(CAMERA), eq(!originalIsBlocked) ) verify(mockActivityStarter).postQSRunnableDismissingKeyguard(any()) } @Test fun handleLongClick_whenSafetyManagerEnabled_privacyControlsIntent() = runTest { whenever(fakeSafetyCenterManager.isSafetyCenterEnabled).thenReturn(true) underTest.handleInput(QSTileInputTestKtx.longClick(SensorPrivacyToggleTileModel(false))) QSTileIntentUserInputHandlerSubject.assertThat(inputHandler).handledOneIntentInput { assertThat(it.intent.action).isEqualTo(Settings.ACTION_PRIVACY_CONTROLS) } } @Test fun handleLongClick_whenSafetyManagerDisabled_privacySettingsIntent() = runTest { whenever(fakeSafetyCenterManager.isSafetyCenterEnabled).thenReturn(false) underTest.handleInput(QSTileInputTestKtx.longClick(SensorPrivacyToggleTileModel(false))) QSTileIntentUserInputHandlerSubject.assertThat(inputHandler).handledOneIntentInput { assertThat(it.intent.action).isEqualTo(Settings.ACTION_PRIVACY_SETTINGS) } } @Test fun handleClick_microphone_flipsController() = runTest { val micUserActionInteractor = SensorPrivacyToggleTileUserActionInteractor( inputHandler, keyguardInteractor, mockActivityStarter, mockSensorPrivacyController, fakeSafetyCenterManager, MICROPHONE ) micUserActionInteractor.handleInput( QSTileInputTestKtx.click(SensorPrivacyToggleTileModel(false)) ) verify(mockSensorPrivacyController) .setSensorBlocked(eq(SensorPrivacyManager.Sources.QS_TILE), eq(MICROPHONE), eq(true)) micUserActionInteractor.handleInput( QSTileInputTestKtx.click(SensorPrivacyToggleTileModel(true)) ) verify(mockSensorPrivacyController) .setSensorBlocked(eq(SensorPrivacyManager.Sources.QS_TILE), eq(MICROPHONE), eq(false)) } }
packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/sensorprivacy/ui/SensorPrivacyToggleTileMapperTest.kt 0 → 100644 +161 −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.qs.tiles.impl.sensorprivacy.ui import android.graphics.drawable.TestStubDrawable import android.hardware.SensorPrivacyManager.Sensors.CAMERA import android.hardware.SensorPrivacyManager.Sensors.MICROPHONE import android.widget.Switch import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.common.shared.model.Icon import com.android.systemui.kosmos.Kosmos import com.android.systemui.qs.tiles.impl.custom.QSTileStateSubject import com.android.systemui.qs.tiles.impl.sensorprivacy.domain.model.SensorPrivacyToggleTileModel import com.android.systemui.qs.tiles.impl.sensorprivacy.qsCameraSensorPrivacyToggleTileConfig import com.android.systemui.qs.tiles.impl.sensorprivacy.qsMicrophoneSensorPrivacyToggleTileConfig import com.android.systemui.qs.tiles.impl.sensorprivacy.ui.SensorPrivacyTileResources.CameraPrivacyTileResources import com.android.systemui.qs.tiles.impl.sensorprivacy.ui.SensorPrivacyTileResources.MicrophonePrivacyTileResources import com.android.systemui.qs.tiles.viewmodel.QSTileState import com.android.systemui.res.R import org.junit.Test import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) class SensorPrivacyToggleTileMapperTest : SysuiTestCase() { private val kosmos = Kosmos() private val cameraConfig = kosmos.qsCameraSensorPrivacyToggleTileConfig private val micConfig = kosmos.qsMicrophoneSensorPrivacyToggleTileConfig @Test fun mapCamera_notBlocked() { val mapper = createMapper(CameraPrivacyTileResources) val inputModel = SensorPrivacyToggleTileModel(false) val outputState = mapper.map(cameraConfig, inputModel) val expectedState = createSensorPrivacyToggleTileState( QSTileState.ActivationState.ACTIVE, context.getString(R.string.quick_settings_camera_mic_available), R.drawable.qs_camera_access_icon_on, null, CAMERA ) QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState) } @Test fun mapCamera_blocked() { val mapper = createMapper(CameraPrivacyTileResources) val inputModel = SensorPrivacyToggleTileModel(true) val outputState = mapper.map(cameraConfig, inputModel) val expectedState = createSensorPrivacyToggleTileState( QSTileState.ActivationState.INACTIVE, context.getString(R.string.quick_settings_camera_mic_blocked), R.drawable.qs_camera_access_icon_off, null, CAMERA ) QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState) } @Test fun mapMic_notBlocked() { val mapper = createMapper(MicrophonePrivacyTileResources) val inputModel = SensorPrivacyToggleTileModel(false) val outputState = mapper.map(micConfig, inputModel) val expectedState = createSensorPrivacyToggleTileState( QSTileState.ActivationState.ACTIVE, context.getString(R.string.quick_settings_camera_mic_available), R.drawable.qs_mic_access_on, null, MICROPHONE ) QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState) } @Test fun mapMic_blocked() { val mapper = createMapper(MicrophonePrivacyTileResources) val inputModel = SensorPrivacyToggleTileModel(true) val outputState = mapper.map(micConfig, inputModel) val expectedState = createSensorPrivacyToggleTileState( QSTileState.ActivationState.INACTIVE, context.getString(R.string.quick_settings_camera_mic_blocked), R.drawable.qs_mic_access_off, null, MICROPHONE ) QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState) } private fun createMapper( sensorResources: SensorPrivacyTileResources ): SensorPrivacyToggleTileMapper { val mapper = SensorPrivacyToggleTileMapper( context.orCreateTestableResources .apply { addOverride(R.drawable.qs_camera_access_icon_off, TestStubDrawable()) addOverride(R.drawable.qs_camera_access_icon_on, TestStubDrawable()) addOverride(R.drawable.qs_mic_access_off, TestStubDrawable()) addOverride(R.drawable.qs_mic_access_on, TestStubDrawable()) } .resources, context.theme, sensorResources, ) return mapper } private fun createSensorPrivacyToggleTileState( activationState: QSTileState.ActivationState, secondaryLabel: String, iconRes: Int, stateDescription: CharSequence?, sensorId: Int, ): QSTileState { val label = if (sensorId == CAMERA) context.getString(R.string.quick_settings_camera_label) else context.getString(R.string.quick_settings_mic_label) return QSTileState( { Icon.Loaded(context.getDrawable(iconRes)!!, null) }, label, activationState, secondaryLabel, setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK), label, stateDescription, QSTileState.SideViewIcon.None, QSTileState.EnabledState.ENABLED, Switch::class.qualifiedName ) } }
packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt +1 −1 Original line number Diff line number Diff line Loading @@ -193,7 +193,7 @@ constructor( val isKeyguardShowing: Flow<Boolean> = repository.isKeyguardShowing /** Whether the keyguard is dismissible or not. */ val isKeyguardDismissible: Flow<Boolean> = repository.isKeyguardDismissible val isKeyguardDismissible: StateFlow<Boolean> = repository.isKeyguardDismissible /** Whether the keyguard is occluded (covered by an activity). */ @Deprecated("Use KeyguardTransitionInteractor + KeyguardState.OCCLUDED") Loading
packages/SystemUI/src/com/android/systemui/qs/tiles/impl/sensorprivacy/SensorPrivacyToggleTileDataInteractor.kt 0 → 100644 +114 −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.qs.tiles.impl.sensorprivacy import android.hardware.SensorPrivacyManager.Sensors.CAMERA import android.hardware.SensorPrivacyManager.Sensors.MICROPHONE import android.hardware.SensorPrivacyManager.Sensors.Sensor import android.os.UserHandle import android.provider.DeviceConfig import android.util.Log import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor import com.android.systemui.qs.tiles.impl.sensorprivacy.domain.model.SensorPrivacyToggleTileModel import com.android.systemui.statusbar.policy.IndividualSensorPrivacyController import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.withContext /** Observes SensorPrivacyToggle mode state changes providing the [SensorPrivacyToggleTileModel]. */ class SensorPrivacyToggleTileDataInteractor @AssistedInject constructor( @Background private val bgCoroutineContext: CoroutineContext, private val privacyController: IndividualSensorPrivacyController, @Assisted @Sensor private val sensorId: Int, ) : QSTileDataInteractor<SensorPrivacyToggleTileModel> { @AssistedFactory interface Factory { fun create(@Sensor id: Int): SensorPrivacyToggleTileDataInteractor } override fun tileData( user: UserHandle, triggers: Flow<DataUpdateTrigger> ): Flow<SensorPrivacyToggleTileModel> = conflatedCallbackFlow { val callback = IndividualSensorPrivacyController.Callback { sensor, blocked -> if (sensor == sensorId) trySend(SensorPrivacyToggleTileModel(blocked)) } privacyController.addCallback(callback) // does not emit an initial state awaitClose { privacyController.removeCallback(callback) } } .onStart { emit(SensorPrivacyToggleTileModel(privacyController.isSensorBlocked(sensorId))) } .distinctUntilChanged() .flowOn(bgCoroutineContext) override fun availability(user: UserHandle) = flow { emit(isAvailable()) }.flowOn(bgCoroutineContext) private suspend fun isAvailable(): Boolean { return privacyController.supportsSensorToggle(sensorId) && isSensorDeviceConfigSet() } private suspend fun isSensorDeviceConfigSet(): Boolean = withContext(bgCoroutineContext) { try { val deviceConfigName = getDeviceConfigName(sensorId) return@withContext DeviceConfig.getBoolean( DeviceConfig.NAMESPACE_PRIVACY, deviceConfigName, true ) } catch (exception: IllegalArgumentException) { Log.w( TAG, "isDeviceConfigSet for sensorId $sensorId: " + "Defaulting to true due to exception. ", exception ) return@withContext true } } private fun getDeviceConfigName(sensorId: Int): String { if (sensorId == MICROPHONE) { return "mic_toggle_enabled" } else if (sensorId == CAMERA) { return "camera_toggle_enabled" } else { throw IllegalArgumentException("getDeviceConfigName: unexpected sensorId: $sensorId") } } private companion object { const val TAG = "SensorPrivacyToggleTileException" } }