/* * Copyright (C) 2022 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 android.app.AppOpsManager import android.content.pm.UserInfo import android.os.UserHandle import android.testing.TestableLooper.RunWithLooper import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.appops.AppOpItem import com.android.systemui.appops.AppOpsController import com.android.systemui.privacy.logging.PrivacyLogger import com.android.systemui.settings.UserTracker import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.time.FakeSystemClock import org.hamcrest.Matchers.hasItem import org.hamcrest.Matchers.not import org.hamcrest.Matchers.nullValue import org.junit.Assert.assertEquals import org.junit.Assert.assertThat import org.junit.Assert.assertTrue import org.junit.Assert.assertFalse import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentCaptor import org.mockito.ArgumentMatchers.anyBoolean import org.mockito.Captor import org.mockito.Mock import org.mockito.Mockito import org.mockito.Mockito.`when` import org.mockito.Mockito.atLeastOnce import org.mockito.Mockito.doReturn import org.mockito.Mockito.never import org.mockito.Mockito.reset import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations @RunWith(AndroidJUnit4::class) @SmallTest @RunWithLooper class AppOpsPrivacyItemMonitorTest : SysuiTestCase() { companion object { val CURRENT_USER_ID = 1 val TEST_UID = CURRENT_USER_ID * UserHandle.PER_USER_RANGE const val TEST_PACKAGE_NAME = "test" fun capture(argumentCaptor: ArgumentCaptor): T = argumentCaptor.capture() fun eq(value: T): T = Mockito.eq(value) ?: value fun any(): T = Mockito.any() } @Mock private lateinit var appOpsController: AppOpsController @Mock private lateinit var callback: PrivacyItemMonitor.Callback @Mock private lateinit var userTracker: UserTracker @Mock private lateinit var privacyConfig: PrivacyConfig @Mock private lateinit var logger: PrivacyLogger @Captor private lateinit var argCaptorConfigCallback: ArgumentCaptor @Captor private lateinit var argCaptorCallback: ArgumentCaptor private lateinit var appOpsPrivacyItemMonitor: AppOpsPrivacyItemMonitor private lateinit var executor: FakeExecutor fun createAppOpsPrivacyItemMonitor(): AppOpsPrivacyItemMonitor { return AppOpsPrivacyItemMonitor( appOpsController, userTracker, privacyConfig, executor, logger) } @Before fun setup() { MockitoAnnotations.initMocks(this) executor = FakeExecutor(FakeSystemClock()) // Listen to everything by default `when`(privacyConfig.micCameraAvailable).thenReturn(true) `when`(privacyConfig.locationAvailable).thenReturn(true) `when`(userTracker.userProfiles).thenReturn( listOf(UserInfo(CURRENT_USER_ID, TEST_PACKAGE_NAME, 0))) appOpsPrivacyItemMonitor = createAppOpsPrivacyItemMonitor() verify(privacyConfig).addCallback(capture(argCaptorConfigCallback)) } @Test fun testStartListeningAddsAppOpsCallback() { appOpsPrivacyItemMonitor.startListening(callback) executor.runAllReady() verify(appOpsController).addCallback(eq(AppOpsPrivacyItemMonitor.OPS), any()) } @Test fun testStopListeningRemovesAppOpsCallback() { appOpsPrivacyItemMonitor.startListening(callback) executor.runAllReady() verify(appOpsController, never()).removeCallback(any(), any()) appOpsPrivacyItemMonitor.stopListening() executor.runAllReady() verify(appOpsController).removeCallback(eq(AppOpsPrivacyItemMonitor.OPS), any()) } @Test fun testDistinctItems() { doReturn(listOf(AppOpItem(AppOpsManager.OP_CAMERA, TEST_UID, TEST_PACKAGE_NAME, 0), AppOpItem(AppOpsManager.OP_CAMERA, TEST_UID, TEST_PACKAGE_NAME, 0))) .`when`(appOpsController).getActiveAppOps(anyBoolean()) assertEquals(1, appOpsPrivacyItemMonitor.getActivePrivacyItems().size) } @Test fun testVoiceActivationPrivacyItems() { doReturn(listOf(AppOpItem(AppOpsManager.OP_RECEIVE_SANDBOX_TRIGGER_AUDIO, TEST_UID, TEST_PACKAGE_NAME, 0))) .`when`(appOpsController).getActiveAppOps(anyBoolean()) val privacyItems = appOpsPrivacyItemMonitor.getActivePrivacyItems() assertEquals(1, privacyItems.size) assertEquals(PrivacyType.TYPE_MICROPHONE, privacyItems[0].privacyType) } @Test fun testSimilarItemsDifferentTimeStamp() { doReturn(listOf(AppOpItem(AppOpsManager.OP_CAMERA, TEST_UID, TEST_PACKAGE_NAME, 0), AppOpItem(AppOpsManager.OP_CAMERA, TEST_UID, TEST_PACKAGE_NAME, 1))) .`when`(appOpsController).getActiveAppOps(anyBoolean()) assertEquals(2, appOpsPrivacyItemMonitor.getActivePrivacyItems().size) } @Test fun testRegisterUserTrackerCallback() { appOpsPrivacyItemMonitor.startListening(callback) executor.runAllReady() verify(userTracker, atLeastOnce()).addCallback( eq(appOpsPrivacyItemMonitor.userTrackerCallback), any()) verify(userTracker, never()).removeCallback( eq(appOpsPrivacyItemMonitor.userTrackerCallback)) } @Test fun testUserTrackerCallback_userChanged() { appOpsPrivacyItemMonitor.userTrackerCallback.onUserChanged(0, mContext) executor.runAllReady() verify(userTracker).userProfiles } @Test fun testUserTrackerCallback_profilesChanged() { appOpsPrivacyItemMonitor.userTrackerCallback.onProfilesChanged(emptyList()) executor.runAllReady() verify(userTracker).userProfiles } @Test fun testCallbackIsUpdated() { doReturn(emptyList()).`when`(appOpsController).getActiveAppOps(anyBoolean()) appOpsPrivacyItemMonitor.startListening(callback) executor.runAllReady() reset(callback) verify(appOpsController).addCallback(any(), capture(argCaptorCallback)) argCaptorCallback.value.onActiveStateChanged(0, TEST_UID, TEST_PACKAGE_NAME, true) executor.runAllReady() verify(callback).onPrivacyItemsChanged() } @Test fun testRemoveCallback() { doReturn(emptyList()).`when`(appOpsController).getActiveAppOps(anyBoolean()) appOpsPrivacyItemMonitor.startListening(callback) executor.runAllReady() reset(callback) verify(appOpsController).addCallback(any(), capture(argCaptorCallback)) appOpsPrivacyItemMonitor.stopListening() argCaptorCallback.value.onActiveStateChanged(0, TEST_UID, TEST_PACKAGE_NAME, true) executor.runAllReady() verify(callback, never()).onPrivacyItemsChanged() } @Test fun testListShouldNotHaveNull() { doReturn(listOf(AppOpItem(AppOpsManager.OP_ACTIVATE_VPN, TEST_UID, TEST_PACKAGE_NAME, 0), AppOpItem(AppOpsManager.OP_COARSE_LOCATION, TEST_UID, TEST_PACKAGE_NAME, 0))) .`when`(appOpsController).getActiveAppOps(anyBoolean()) assertThat(appOpsPrivacyItemMonitor.getActivePrivacyItems(), not(hasItem(nullValue()))) } @Test fun testNotListeningWhenIndicatorsDisabled() { changeMicCamera(false) changeLocation(false) appOpsPrivacyItemMonitor.startListening(callback) executor.runAllReady() verify(appOpsController, never()).addCallback(eq(AppOpsPrivacyItemMonitor.OPS), any()) } @Test fun testNotSendingLocationWhenLocationDisabled() { changeLocation(false) executor.runAllReady() doReturn(listOf(AppOpItem(AppOpsManager.OP_CAMERA, TEST_UID, TEST_PACKAGE_NAME, 0), AppOpItem(AppOpsManager.OP_COARSE_LOCATION, TEST_UID, TEST_PACKAGE_NAME, 0))) .`when`(appOpsController).getActiveAppOps(anyBoolean()) val privacyItems = appOpsPrivacyItemMonitor.getActivePrivacyItems() assertEquals(1, privacyItems.size) assertEquals(PrivacyType.TYPE_CAMERA, privacyItems[0].privacyType) } @Test fun testNotUpdated_LocationChangeWhenLocationDisabled() { doReturn(listOf( AppOpItem(AppOpsManager.OP_COARSE_LOCATION, TEST_UID, TEST_PACKAGE_NAME, 0))) .`when`(appOpsController).getActiveAppOps(anyBoolean()) appOpsPrivacyItemMonitor.startListening(callback) changeLocation(false) executor.runAllReady() reset(callback) // Clean callback verify(appOpsController).addCallback(any(), capture(argCaptorCallback)) argCaptorCallback.value.onActiveStateChanged( AppOpsManager.OP_FINE_LOCATION, TEST_UID, TEST_PACKAGE_NAME, true) verify(callback, never()).onPrivacyItemsChanged() } @Test fun testLogActiveChanged() { appOpsPrivacyItemMonitor.startListening(callback) executor.runAllReady() verify(appOpsController).addCallback(any(), capture(argCaptorCallback)) argCaptorCallback.value.onActiveStateChanged( AppOpsManager.OP_FINE_LOCATION, TEST_UID, TEST_PACKAGE_NAME, true) verify(logger).logUpdatedItemFromAppOps( AppOpsManager.OP_FINE_LOCATION, TEST_UID, TEST_PACKAGE_NAME, true) } @Test fun testListRequestedShowPaused() { appOpsPrivacyItemMonitor.getActivePrivacyItems() verify(appOpsController).getActiveAppOps(true) } @Test fun testListFilterCurrentUser() { val otherUser = CURRENT_USER_ID + 1 val otherUserUid = otherUser * UserHandle.PER_USER_RANGE `when`(userTracker.userProfiles) .thenReturn(listOf(UserInfo(otherUser, TEST_PACKAGE_NAME, 0))) doReturn(listOf( AppOpItem(AppOpsManager.OP_COARSE_LOCATION, TEST_UID, TEST_PACKAGE_NAME, 0), AppOpItem(AppOpsManager.OP_CAMERA, otherUserUid, TEST_PACKAGE_NAME, 0)) ).`when`(appOpsController).getActiveAppOps(anyBoolean()) appOpsPrivacyItemMonitor.userTrackerCallback.onUserChanged(otherUser, mContext) executor.runAllReady() appOpsPrivacyItemMonitor.startListening(callback) executor.runAllReady() val privacyItems = appOpsPrivacyItemMonitor.getActivePrivacyItems() assertEquals(1, privacyItems.size) assertEquals(PrivacyType.TYPE_CAMERA, privacyItems[0].privacyType) assertEquals(otherUserUid, privacyItems[0].application.uid) } @Test fun testAlwaysGetPhoneCameraOps() { val otherUser = CURRENT_USER_ID + 1 `when`(userTracker.userProfiles) .thenReturn(listOf(UserInfo(otherUser, TEST_PACKAGE_NAME, 0))) doReturn(listOf( AppOpItem(AppOpsManager.OP_COARSE_LOCATION, TEST_UID, TEST_PACKAGE_NAME, 0), AppOpItem(AppOpsManager.OP_RECORD_AUDIO, TEST_UID, TEST_PACKAGE_NAME, 0), AppOpItem(AppOpsManager.OP_PHONE_CALL_CAMERA, TEST_UID, TEST_PACKAGE_NAME, 0)) ).`when`(appOpsController).getActiveAppOps(anyBoolean()) appOpsPrivacyItemMonitor.userTrackerCallback.onUserChanged(otherUser, mContext) executor.runAllReady() appOpsPrivacyItemMonitor.startListening(callback) executor.runAllReady() val privacyItems = appOpsPrivacyItemMonitor.getActivePrivacyItems() assertEquals(1, privacyItems.size) assertEquals(PrivacyType.TYPE_CAMERA, privacyItems[0].privacyType) } @Test fun testAlwaysGetPhoneMicOps() { val otherUser = CURRENT_USER_ID + 1 `when`(userTracker.userProfiles) .thenReturn(listOf(UserInfo(otherUser, TEST_PACKAGE_NAME, 0))) doReturn(listOf( AppOpItem(AppOpsManager.OP_COARSE_LOCATION, TEST_UID, TEST_PACKAGE_NAME, 0), AppOpItem(AppOpsManager.OP_CAMERA, TEST_UID, TEST_PACKAGE_NAME, 0), AppOpItem(AppOpsManager.OP_PHONE_CALL_MICROPHONE, TEST_UID, TEST_PACKAGE_NAME, 0)) ).`when`(appOpsController).getActiveAppOps(anyBoolean()) appOpsPrivacyItemMonitor.userTrackerCallback.onUserChanged(otherUser, mContext) executor.runAllReady() appOpsPrivacyItemMonitor.startListening(callback) executor.runAllReady() val privacyItems = appOpsPrivacyItemMonitor.getActivePrivacyItems() assertEquals(1, privacyItems.size) assertEquals(PrivacyType.TYPE_MICROPHONE, privacyItems[0].privacyType) } @Test fun testDisabledAppOpIsPaused() { val item = AppOpItem(AppOpsManager.OP_RECORD_AUDIO, TEST_UID, TEST_PACKAGE_NAME, 0) item.isDisabled = true `when`(appOpsController.getActiveAppOps(anyBoolean())).thenReturn(listOf(item)) val privacyItems = appOpsPrivacyItemMonitor.getActivePrivacyItems() assertEquals(1, privacyItems.size) assertTrue(privacyItems[0].paused) } @Test fun testEnabledAppOpIsNotPaused() { val item = AppOpItem(AppOpsManager.OP_RECORD_AUDIO, TEST_UID, TEST_PACKAGE_NAME, 0) `when`(appOpsController.getActiveAppOps(anyBoolean())).thenReturn(listOf(item)) val privacyItems = appOpsPrivacyItemMonitor.getActivePrivacyItems() assertEquals(1, privacyItems.size) assertFalse(privacyItems[0].paused) } private fun changeMicCamera(value: Boolean) { `when`(privacyConfig.micCameraAvailable).thenReturn(value) argCaptorConfigCallback.value.onFlagMicCameraChanged(value) } private fun changeLocation(value: Boolean) { `when`(privacyConfig.locationAvailable).thenReturn(value) argCaptorConfigCallback.value.onFlagLocationChanged(value) } }