diff --git a/AndroidManifest.xml b/AndroidManifest.xml index b7043d41dc2..2a456240efc 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -4185,6 +4185,25 @@ android:value="@string/menu_key_apps"/> + + + + + + + + + + + Allow this app to display on top of other apps you\u2019re using. This app will be able to see where you tap or change what\u2019s displayed on the screen. + + + Change media output + + Allow app to switch media output + + Allow this app to choose which connected device plays audio or video from other apps. If allowed, this app can access a list of available devices such as headphones and speakers and choose which output device is used to stream or cast audio or video. + All files access diff --git a/res/xml/special_access.xml b/res/xml/special_access.xml index 3f3d75d2daf..743a122a585 100644 --- a/res/xml/special_access.xml +++ b/res/xml/special_access.xml @@ -94,6 +94,13 @@ android:fragment="com.android.settings.notification.NotificationAccessSettings" settings:controller="com.android.settings.applications.specialaccess.notificationaccess.NotificationAccessController" /> + + + intentCaptor = ArgumentCaptor.forClass(Intent.class); + verify(mContext).startActivity(intentCaptor.capture()); + final Intent intent = intentCaptor.getValue(); + assertThat(intent.getComponent().getClassName()).isEqualTo(SpaActivity.class.getName()); + assertThat(intent.getStringExtra(KEY_DESTINATION)).isEqualTo( + MediaRoutingControlAppListProvider.INSTANCE.getAppListRoute()); + } +} diff --git a/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/MediaRoutingControlTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/MediaRoutingControlTest.kt new file mode 100644 index 00000000000..5f0f2c6dd25 --- /dev/null +++ b/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/MediaRoutingControlTest.kt @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2023 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.settings.spa.app.specialaccess + +import android.Manifest +import android.app.AppOpsManager +import android.app.role.RoleManager +import android.app.settings.SettingsEnums +import android.companion.AssociationRequest +import android.content.Context +import android.content.pm.ApplicationInfo +import androidx.lifecycle.MutableLiveData +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.settings.R +import com.android.settings.testutils.FakeFeatureFactory +import com.android.settingslib.spaprivileged.model.app.IAppOpsController +import com.android.settingslib.spaprivileged.template.app.AppOpPermissionRecord +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Spy +import org.mockito.junit.MockitoJUnit +import org.mockito.junit.MockitoRule +import org.mockito.Mockito.`when` as whenever +import org.mockito.Mockito.verify + +@RunWith(AndroidJUnit4::class) +class MediaRoutingControlTest { + @get:Rule + val mockito: MockitoRule = MockitoJUnit.rule() + + @Spy + private val context: Context = ApplicationProvider.getApplicationContext() + + private lateinit var listModel: MediaRoutingControlAppsListModel + + @Mock + private lateinit var mockRoleManager: RoleManager + + private val fakeFeatureFactory = FakeFeatureFactory() + private val metricsFeatureProvider = fakeFeatureFactory.metricsFeatureProvider + + @Before + fun setUp() { + whenever(context.getSystemService(RoleManager::class.java)) + .thenReturn(mockRoleManager) + listModel = MediaRoutingControlAppsListModel(context) + } + + @Test + fun modelResourceIdAndProperties() { + assertThat(listModel.pageTitleResId).isEqualTo(R.string.media_routing_control_title) + assertThat(listModel.switchTitleResId).isEqualTo(R.string.allow_media_routing_control) + assertThat(listModel.footerResId).isEqualTo(R.string.allow_media_routing_description) + assertThat(listModel.appOp).isEqualTo(AppOpsManager.OP_MEDIA_ROUTING_CONTROL) + assertThat(listModel.permission).isEqualTo(Manifest.permission.MEDIA_ROUTING_CONTROL) + assertThat(listModel.setModeByUid).isTrue() + } + + @Test + fun setAllowed_callWithNewStatusAsTrue_shouldChangeAppControllerModeToAllowed() { + val fakeAppOpController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT) + val permissionRequestedRecord = + AppOpPermissionRecord( + app = ApplicationInfo().apply { packageName = PACKAGE_NAME }, + hasRequestPermission = true, + hasRequestBroaderPermission = false, + appOpsController = fakeAppOpController, + ) + + listModel.setAllowed(permissionRequestedRecord, true) + + assertThat(fakeAppOpController.getMode()).isEqualTo(AppOpsManager.MODE_ALLOWED) + } + + @Test + fun setAllowed_callWithNewStatusAsTrue_shouldLogPermissionToggleActionAsAllowed() { + val fakeAppOpController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT) + val permissionRequestedRecord = + AppOpPermissionRecord( + app = ApplicationInfo().apply { packageName = PACKAGE_NAME }, + hasRequestPermission = true, + hasRequestBroaderPermission = false, + appOpsController = fakeAppOpController, + ) + + listModel.setAllowed(permissionRequestedRecord, true) + + verify(metricsFeatureProvider) + .action(context, SettingsEnums.MEDIA_ROUTING_CONTROL, VALUE_LOGGING_ALLOWED) + } + + @Test + fun setAllowed_callWithNewStatusAsFalse_shouldChangeAppControllerModeToErrored() { + val fakeAppOpController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT) + val permissionRequestedRecord = + AppOpPermissionRecord( + app = ApplicationInfo().apply { packageName = PACKAGE_NAME }, + hasRequestPermission = true, + hasRequestBroaderPermission = false, + appOpsController = fakeAppOpController, + ) + + listModel.setAllowed(permissionRequestedRecord, false) + + assertThat(fakeAppOpController.getMode()).isEqualTo(AppOpsManager.MODE_ERRORED) + } + + @Test + fun setAllowed_callWithNewStatusAsFalse_shouldLogPermissionToggleActionAsDenied() { + val fakeAppOpController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT) + val permissionRequestedRecord = + AppOpPermissionRecord( + app = ApplicationInfo().apply { packageName = PACKAGE_NAME }, + hasRequestPermission = true, + hasRequestBroaderPermission = false, + appOpsController = fakeAppOpController, + ) + + listModel.setAllowed(permissionRequestedRecord, false) + + verify(metricsFeatureProvider) + .action(context, SettingsEnums.MEDIA_ROUTING_CONTROL, VALUE_LOGGING_DENIED) + } + + @Test + fun isChangeable_permissionRequestedByAppAndWatchCompanionRoleAssigned_shouldReturnTrue() { + val permissionRequestedRecord = + AppOpPermissionRecord( + app = ApplicationInfo().apply { packageName = PACKAGE_NAME }, + hasRequestPermission = true, + hasRequestBroaderPermission = false, + appOpsController = + FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT), + ) + whenever(mockRoleManager.getRoleHolders(AssociationRequest.DEVICE_PROFILE_WATCH)) + .thenReturn(listOf(PACKAGE_NAME)) + + val isSpecialAccessChangeable = listModel.isChangeable(permissionRequestedRecord) + + assertThat(isSpecialAccessChangeable).isTrue() + } + + @Test + fun isChangeable_permissionNotRequestedByAppButWatchCompanionRoleAssigned_shouldReturnFalse() { + val permissionNotRequestedRecord = + AppOpPermissionRecord( + app = ApplicationInfo().apply { packageName = PACKAGE_NAME }, + hasRequestPermission = false, + hasRequestBroaderPermission = false, + appOpsController = + FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT), + ) + whenever(mockRoleManager.getRoleHolders(AssociationRequest.DEVICE_PROFILE_WATCH)) + .thenReturn(listOf(PACKAGE_NAME)) + + val isSpecialAccessChangeable = listModel.isChangeable(permissionNotRequestedRecord) + + assertThat(isSpecialAccessChangeable).isFalse() + } + + @Test + fun isChangeable_permissionRequestedByAppButWatchCompanionRoleNotAssigned_shouldReturnFalse() { + val permissionRequestedRecord = + AppOpPermissionRecord( + app = ApplicationInfo().apply { packageName = PACKAGE_NAME }, + hasRequestPermission = true, + hasRequestBroaderPermission = false, + appOpsController = + FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT), + ) + whenever(mockRoleManager.getRoleHolders(AssociationRequest.DEVICE_PROFILE_WATCH)) + .thenReturn(listOf("other.package.name")) + + val isSpecialAccessChangeable = listModel.isChangeable(permissionRequestedRecord) + + assertThat(isSpecialAccessChangeable).isFalse() + } + + private class FakeAppOpsController(fakeMode: Int) : IAppOpsController { + + override val mode = MutableLiveData(fakeMode) + + override fun setAllowed(allowed: Boolean) { + if (allowed) + mode.postValue(AppOpsManager.MODE_ALLOWED) + else + mode.postValue(AppOpsManager.MODE_ERRORED) + } + + override fun getMode(): Int = mode.value!! + } + + companion object { + const val PACKAGE_NAME = "test.package.name" + const val VALUE_LOGGING_ALLOWED = 1 + const val VALUE_LOGGING_DENIED = 0 + } +} \ No newline at end of file