diff --git a/src/com/android/settings/spa/SettingsSpaEnvironment.kt b/src/com/android/settings/spa/SettingsSpaEnvironment.kt index 9eab400326a..6b22b9e17a1 100644 --- a/src/com/android/settings/spa/SettingsSpaEnvironment.kt +++ b/src/com/android/settings/spa/SettingsSpaEnvironment.kt @@ -29,6 +29,7 @@ import com.android.settings.spa.app.specialaccess.MediaManagementAppsAppListProv import com.android.settings.spa.app.specialaccess.ModifySystemSettingsAppListProvider import com.android.settings.spa.app.specialaccess.PictureInPictureListProvider import com.android.settings.spa.app.specialaccess.SpecialAppAccessPageProvider +import com.android.settings.spa.app.specialaccess.WifiControlAppListProvider import com.android.settings.spa.development.UsageStatsPageProvider import com.android.settings.spa.home.HomePageProvider import com.android.settings.spa.notification.AppListNotificationsPageProvider @@ -43,17 +44,20 @@ import com.android.settingslib.spaprivileged.template.app.TogglePermissionAppLis open class SettingsSpaEnvironment(context: Context) : SpaEnvironment(context) { override val pageProviderRepository = lazy { - val togglePermissionAppListTemplate = TogglePermissionAppListTemplate( - allProviders = listOf( - AllFilesAccessAppListProvider, - DisplayOverOtherAppsAppListProvider, - MediaManagementAppsAppListProvider, - ModifySystemSettingsAppListProvider, - PictureInPictureListProvider, - InstallUnknownAppsListProvider, - AlarmsAndRemindersAppListProvider, - ), - ) + val togglePermissionAppListTemplate = + TogglePermissionAppListTemplate( + allProviders = + listOf( + AllFilesAccessAppListProvider, + DisplayOverOtherAppsAppListProvider, + MediaManagementAppsAppListProvider, + ModifySystemSettingsAppListProvider, + PictureInPictureListProvider, + InstallUnknownAppsListProvider, + AlarmsAndRemindersAppListProvider, + WifiControlAppListProvider, + ), + ) SettingsPageProviderRepository( allPageProviders = listOf( HomePageProvider, diff --git a/src/com/android/settings/spa/app/specialaccess/InstallUnknownApps.kt b/src/com/android/settings/spa/app/specialaccess/InstallUnknownApps.kt index 556ea579a83..2d2dddf9f5d 100644 --- a/src/com/android/settings/spa/app/specialaccess/InstallUnknownApps.kt +++ b/src/com/android/settings/spa/app/specialaccess/InstallUnknownApps.kt @@ -19,6 +19,7 @@ package com.android.settings.spa.app.specialaccess import android.Manifest import android.app.AppGlobals import android.app.AppOpsManager.MODE_DEFAULT +import android.app.AppOpsManager.MODE_ERRORED import android.app.AppOpsManager.OP_REQUEST_INSTALL_PACKAGES import android.content.Context import android.content.pm.ApplicationInfo @@ -50,27 +51,32 @@ class InstallUnknownAppsListModel(private val context: Context) : override val pageTitleResId = R.string.install_other_apps override val switchTitleResId = R.string.external_source_switch_title override val footerResId = R.string.install_all_warning - override val switchRestrictionKeys = listOf( - UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES, - UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES_GLOBALLY, - ) + override val switchRestrictionKeys = + listOf( + UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES, + UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES_GLOBALLY, + ) - override fun transformItem(app: ApplicationInfo) = InstallUnknownAppsRecord( - app = app, - appOpsController = AppOpsController( - context = context, + override fun transformItem(app: ApplicationInfo) = + InstallUnknownAppsRecord( app = app, - op = OP_REQUEST_INSTALL_PACKAGES, - ), - ) + appOpsController = + AppOpsController( + context = context, + app = app, + op = OP_REQUEST_INSTALL_PACKAGES, + modeForNotAllowed = MODE_ERRORED + ), + ) override fun filter( - userIdFlow: Flow, recordListFlow: Flow>, - ) = userIdFlow.map(::getPotentialPackageNames) - .combine(recordListFlow) { potentialPackageNames, recordList -> - recordList.filter { record -> - isChangeable(record, potentialPackageNames) - } + userIdFlow: Flow, + recordListFlow: Flow>, + ) = + userIdFlow.map(::getPotentialPackageNames).combine(recordListFlow) { + potentialPackageNames, + recordList -> + recordList.filter { record -> isChangeable(record, potentialPackageNames) } } @Composable @@ -88,12 +94,13 @@ class InstallUnknownAppsListModel(private val context: Context) : private fun isChangeable( record: InstallUnknownAppsRecord, potentialPackageNames: Set, - ) = record.appOpsController.getMode() != MODE_DEFAULT || + ) = + record.appOpsController.getMode() != MODE_DEFAULT || record.app.packageName in potentialPackageNames private fun getPotentialPackageNames(userId: Int): Set = - AppGlobals.getPackageManager().getAppOpPermissionPackages( - Manifest.permission.REQUEST_INSTALL_PACKAGES, userId - ).toSet() + AppGlobals.getPackageManager() + .getAppOpPermissionPackages(Manifest.permission.REQUEST_INSTALL_PACKAGES, userId) + .toSet() } } diff --git a/src/com/android/settings/spa/app/specialaccess/PictureInPicture.kt b/src/com/android/settings/spa/app/specialaccess/PictureInPicture.kt index 0c56e56d4d3..994ee92ba53 100644 --- a/src/com/android/settings/spa/app/specialaccess/PictureInPicture.kt +++ b/src/com/android/settings/spa/app/specialaccess/PictureInPicture.kt @@ -16,6 +16,7 @@ package com.android.settings.spa.app.specialaccess +import android.app.AppOpsManager.MODE_ERRORED import android.app.AppOpsManager.OP_PICTURE_IN_PICTURE import android.content.Context import android.content.pm.ActivityInfo @@ -46,8 +47,8 @@ data class PictureInPictureRecord( val appOpsController: AppOpsController, ) : AppRecord -class PictureInPictureListModel(private val context: Context) - : TogglePermissionAppListModel { +class PictureInPictureListModel(private val context: Context) : + TogglePermissionAppListModel { override val pageTitleResId = R.string.picture_in_picture_title override val switchTitleResId = R.string.picture_in_picture_app_detail_switch override val footerResId = R.string.picture_in_picture_app_detail_summary @@ -55,15 +56,16 @@ class PictureInPictureListModel(private val context: Context) private val packageManager = context.packageManager override fun transform(userIdFlow: Flow, appListFlow: Flow>) = - userIdFlow.map(::getPictureInPicturePackages) - .combine(appListFlow) { pictureInPicturePackages, appList -> - appList.map { app -> - createPictureInPictureRecord( - app = app, - isSupport = app.packageName in pictureInPicturePackages, - ) - } + userIdFlow.map(::getPictureInPicturePackages).combine(appListFlow) { + pictureInPicturePackages, + appList -> + appList.map { app -> + createPictureInPictureRecord( + app = app, + isSupport = app.packageName in pictureInPicturePackages, + ) } + } override fun transformItem(app: ApplicationInfo): PictureInPictureRecord { val packageInfo = @@ -78,17 +80,17 @@ class PictureInPictureListModel(private val context: Context) PictureInPictureRecord( app = app, isSupport = isSupport, - appOpsController = AppOpsController( - context = context, - app = app, - op = OP_PICTURE_IN_PICTURE, - ), + appOpsController = + AppOpsController( + context = context, + app = app, + op = OP_PICTURE_IN_PICTURE, + modeForNotAllowed = MODE_ERRORED, + ), ) override fun filter(userIdFlow: Flow, recordListFlow: Flow>) = - recordListFlow.map { recordList -> - recordList.filter { it.isSupport } - } + recordListFlow.map { recordList -> recordList.filter { it.isSupport } } @Composable override fun isAllowed(record: PictureInPictureRecord) = @@ -101,7 +103,8 @@ class PictureInPictureListModel(private val context: Context) } private fun getPictureInPicturePackages(userId: Int): Set = - packageManager.getInstalledPackagesAsUser(GET_ACTIVITIES_FLAGS, userId) + packageManager + .getInstalledPackagesAsUser(GET_ACTIVITIES_FLAGS, userId) .filter { it.supportsPictureInPicture() } .map { it.packageName } .toSet() diff --git a/src/com/android/settings/spa/app/specialaccess/SpecialAppAccess.kt b/src/com/android/settings/spa/app/specialaccess/SpecialAppAccess.kt index f955adf8324..5b9205af780 100644 --- a/src/com/android/settings/spa/app/specialaccess/SpecialAppAccess.kt +++ b/src/com/android/settings/spa/app/specialaccess/SpecialAppAccess.kt @@ -43,10 +43,12 @@ object SpecialAppAccessPageProvider : SettingsPageProvider { @Composable fun EntryItem() { - Preference(object : PreferenceModel { - override val title = stringResource(R.string.special_access) - override val onClick = navigator(name) - }) + Preference( + object : PreferenceModel { + override val title = stringResource(R.string.special_access) + override val onClick = navigator(name) + } + ) } fun buildInjectEntry() = SettingsEntryBuilder.createInject(owner = SettingsPage.create(name)) @@ -54,13 +56,15 @@ object SpecialAppAccessPageProvider : SettingsPageProvider { override fun buildEntry(arguments: Bundle?): List { val owner = SettingsPage.create(name, parameter = parameter, arguments = arguments) return listOf( - AllFilesAccessAppListProvider, - DisplayOverOtherAppsAppListProvider, - MediaManagementAppsAppListProvider, - ModifySystemSettingsAppListProvider, - PictureInPictureListProvider, - InstallUnknownAppsListProvider, - AlarmsAndRemindersAppListProvider, - ).map { it.buildAppListInjectEntry().setLink(fromPage = owner).build() } + AllFilesAccessAppListProvider, + DisplayOverOtherAppsAppListProvider, + MediaManagementAppsAppListProvider, + ModifySystemSettingsAppListProvider, + PictureInPictureListProvider, + InstallUnknownAppsListProvider, + AlarmsAndRemindersAppListProvider, + WifiControlAppListProvider, + ) + .map { it.buildAppListInjectEntry().setLink(fromPage = owner).build() } } } diff --git a/src/com/android/settings/spa/app/specialaccess/WifiControlApps.kt b/src/com/android/settings/spa/app/specialaccess/WifiControlApps.kt new file mode 100644 index 00000000000..69f7b1e1688 --- /dev/null +++ b/src/com/android/settings/spa/app/specialaccess/WifiControlApps.kt @@ -0,0 +1,49 @@ +/* + * 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.settings.spa.app.specialaccess + +import android.Manifest +import android.app.AppOpsManager +import android.app.AppOpsManager.MODE_IGNORED +import android.content.Context +import com.android.settings.R +import com.android.settingslib.spaprivileged.model.app.IPackageManagers +import com.android.settingslib.spaprivileged.model.app.PackageManagers +import com.android.settingslib.spaprivileged.template.app.AppOpPermissionListModel +import com.android.settingslib.spaprivileged.template.app.TogglePermissionAppListProvider + +object WifiControlAppListProvider : TogglePermissionAppListProvider { + override val permissionType = "WifiControl" + override fun createModel(context: Context) = WifiControlAppListModel(context) +} + +class WifiControlAppListModel( + private val context: Context, + private val packageManagers: IPackageManagers = PackageManagers +) : AppOpPermissionListModel(context, packageManagers) { + override val pageTitleResId = R.string.change_wifi_state_title + override val switchTitleResId = R.string.change_wifi_state_app_detail_switch + override val footerResId = R.string.change_wifi_state_app_detail_summary + + override val appOp = AppOpsManager.OP_CHANGE_WIFI_STATE + override val permission = Manifest.permission.CHANGE_WIFI_STATE + + /** NETWORK_SETTINGS permission trumps CHANGE_WIFI_CONFIG. */ + override val broaderPermission = Manifest.permission.NETWORK_SETTINGS + override val permissionHasAppopFlag = false + override val modeForNotAllowed = MODE_IGNORED +} diff --git a/tests/spa_unit/src/com/android/settings/spa/app/WifiControlAppListModelTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/WifiControlAppListModelTest.kt new file mode 100644 index 00000000000..c5c48f5b70a --- /dev/null +++ b/tests/spa_unit/src/com/android/settings/spa/app/WifiControlAppListModelTest.kt @@ -0,0 +1,283 @@ +/* + * 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.settings.spa.app.specialaccess + +import android.Manifest +import android.app.AppOpsManager +import android.app.AppOpsManager.MODE_ALLOWED +import android.app.AppOpsManager.MODE_DEFAULT +import android.content.Context +import android.content.pm.ApplicationInfo +import androidx.compose.runtime.State +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.lifecycle.MutableLiveData +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.settingslib.spa.testutils.firstWithTimeoutOrNull +import com.android.settingslib.spaprivileged.model.app.IAppOpsController +import com.android.settingslib.spaprivileged.model.app.IPackageManagers +import com.android.settingslib.spaprivileged.template.app.AppOpPermissionRecord +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.`when` as whenever +import org.mockito.junit.MockitoJUnit +import org.mockito.junit.MockitoRule + +@ExperimentalCoroutinesApi +@RunWith(AndroidJUnit4::class) +class WifiControlAppListModelTest { + + @get:Rule val mockito: MockitoRule = MockitoJUnit.rule() + + @get:Rule val composeTestRule = createComposeRule() + + private val context: Context = ApplicationProvider.getApplicationContext() + + @Mock private lateinit var packageManagers: IPackageManagers + + private lateinit var listModel: WifiControlAppListModel + + @Before + fun setUp() { + listModel = WifiControlAppListModel(context, packageManagers) + } + + @Test + fun transformItem_recordHasCorrectApp() { + val record = listModel.transformItem(APP) + + assertThat(record.app).isSameInstanceAs(APP) + } + + @Test + fun transformItem_hasRequestPermission() = runTest { + with(packageManagers) { + whenever(APP.hasRequestPermission(PM_CHANGE_WIFI_STATE)).thenReturn(true) + } + + val record = listModel.transformItem(APP) + + assertThat(record.hasRequestPermission).isTrue() + } + + @Test + fun transformItem_notRequestPermission() = runTest { + with(packageManagers) { + whenever(APP.hasRequestPermission(PM_CHANGE_WIFI_STATE)).thenReturn(false) + } + + val record = listModel.transformItem(APP) + + assertThat(record.hasRequestPermission).isFalse() + } + + @Test + fun transformItem_hasRequestNetworkSettingsPermission() = runTest { + with(packageManagers) { + whenever(APP.hasRequestPermission(PM_NETWORK_SETTINGS)).thenReturn(true) + } + + val record = listModel.transformItem(APP) + + assertThat(record.hasRequestBroaderPermission).isTrue() + } + + @Test + fun transformItem_notRequestNetworkSettingsPermission() = runTest { + with(packageManagers) { + whenever(APP.hasRequestPermission(PM_NETWORK_SETTINGS)).thenReturn(false) + } + + val record = listModel.transformItem(APP) + + assertThat(record.hasRequestBroaderPermission).isFalse() + } + + @Test + fun filter() = runTest { + val appNotRequestPermissionRecord = + AppOpPermissionRecord( + app = APP_NOT_REQUEST_PERMISSION, + hasRequestPermission = false, + hasRequestBroaderPermission = false, + appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT), + ) + val appRequestedNetworkSettingsRecord = + AppOpPermissionRecord( + app = APP_REQUESTED_NETWORK_SETTINGS, + hasRequestPermission = true, + hasRequestBroaderPermission = false, + appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT) + ) + + val recordListFlow = + listModel.filter( + flowOf(USER_ID), + flowOf(listOf(appNotRequestPermissionRecord, appRequestedNetworkSettingsRecord)) + ) + + val recordList = checkNotNull(recordListFlow.firstWithTimeoutOrNull()) + assertThat(recordList).containsExactly(appRequestedNetworkSettingsRecord) + } + + @Test + fun isAllowed_networkSettingsShouldTrump() { + val record = + AppOpPermissionRecord( + app = APP, + hasRequestPermission = false, + hasRequestBroaderPermission = true, + appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT), + ) + + val isAllowed = getIsAllowed(record) + + assertThat(isAllowed).isTrue() + } + + @Test + fun isAllowed_grantedChangeWifiState() { + val record = + AppOpPermissionRecord( + app = APP, + hasRequestPermission = true, + hasRequestBroaderPermission = false, + appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_ALLOWED), + ) + + val isAllowed = getIsAllowed(record) + + assertThat(isAllowed).isTrue() + } + + @Test + fun isAllowed_notAllowed() { + val record = + AppOpPermissionRecord( + app = APP, + hasRequestPermission = true, + hasRequestBroaderPermission = false, + appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_IGNORED), + ) + + val isAllowed = getIsAllowed(record) + + assertThat(isAllowed).isFalse() + } + + @Test + fun isChangeable_noRequestedPermission() { + val record = + AppOpPermissionRecord( + app = APP, + hasRequestPermission = false, + hasRequestBroaderPermission = false, + appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT), + ) + + val isChangeable = listModel.isChangeable(record) + + assertThat(isChangeable).isFalse() + } + + @Test + fun isChangeable_notChangableWhenRequestedNetworkSettingPermissions() { + val record = + AppOpPermissionRecord( + app = APP, + hasRequestPermission = false, + hasRequestBroaderPermission = true, + appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT), + ) + + val isChangeable = listModel.isChangeable(record) + + assertThat(isChangeable).isFalse() + } + + @Test + fun isChangeable_changableWhenRequestedChangeWifiStatePermission() { + val record = + AppOpPermissionRecord( + app = APP, + hasRequestPermission = true, + hasRequestBroaderPermission = false, + appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT), + ) + + val isChangeable = listModel.isChangeable(record) + + assertThat(isChangeable).isTrue() + } + + @Test + fun setAllowed_shouldCallController() { + val appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT) + val record = + AppOpPermissionRecord( + app = APP, + hasRequestPermission = true, + hasRequestBroaderPermission = false, + appOpsController = appOpsController, + ) + + listModel.setAllowed(record = record, newAllowed = true) + + assertThat(appOpsController.setAllowedCalledWith).isTrue() + } + + private fun getIsAllowed(record: AppOpPermissionRecord): Boolean? { + lateinit var isAllowedState: State + composeTestRule.setContent { isAllowedState = listModel.isAllowed(record) } + return isAllowedState.value + } + + private companion object { + const val USER_ID = 0 + const val PACKAGE_NAME = "package.name" + const val PM_CHANGE_WIFI_STATE = Manifest.permission.CHANGE_WIFI_STATE + const val PM_NETWORK_SETTINGS = Manifest.permission.NETWORK_SETTINGS + + val APP = ApplicationInfo().apply { packageName = PACKAGE_NAME } + + val APP_NOT_REQUEST_PERMISSION = + ApplicationInfo().apply { packageName = "app1.package.name" } + val APP_REQUESTED_NETWORK_SETTINGS = + ApplicationInfo().apply { packageName = "app2.package.name" } + val APP_REQUESTED_CHANGE_WIFI_STATE = + ApplicationInfo().apply { packageName = "app3.package.name" } + } +} + +private class FakeAppOpsController(private val fakeMode: Int) : IAppOpsController { + var setAllowedCalledWith: Boolean? = null + + override val mode = MutableLiveData(fakeMode) + + override fun setAllowed(allowed: Boolean) { + setAllowedCalledWith = allowed + } + + override fun getMode() = fakeMode +}