Merge "Fork the Wi-Fi control setting page to SPA."

This commit is contained in:
Yilin Cai
2023-01-04 08:46:14 +00:00
committed by Android (Google) Code Review
6 changed files with 413 additions and 63 deletions

View File

@@ -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,

View File

@@ -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<Int>, recordListFlow: Flow<List<InstallUnknownAppsRecord>>,
) = userIdFlow.map(::getPotentialPackageNames)
.combine(recordListFlow) { potentialPackageNames, recordList ->
recordList.filter { record ->
isChangeable(record, potentialPackageNames)
}
userIdFlow: Flow<Int>,
recordListFlow: Flow<List<InstallUnknownAppsRecord>>,
) =
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<String>,
) = record.appOpsController.getMode() != MODE_DEFAULT ||
) =
record.appOpsController.getMode() != MODE_DEFAULT ||
record.app.packageName in potentialPackageNames
private fun getPotentialPackageNames(userId: Int): Set<String> =
AppGlobals.getPackageManager().getAppOpPermissionPackages(
Manifest.permission.REQUEST_INSTALL_PACKAGES, userId
).toSet()
AppGlobals.getPackageManager()
.getAppOpPermissionPackages(Manifest.permission.REQUEST_INSTALL_PACKAGES, userId)
.toSet()
}
}

View File

@@ -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<PictureInPictureRecord> {
class PictureInPictureListModel(private val context: Context) :
TogglePermissionAppListModel<PictureInPictureRecord> {
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<Int>, appListFlow: Flow<List<ApplicationInfo>>) =
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<Int>, recordListFlow: Flow<List<PictureInPictureRecord>>) =
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<String> =
packageManager.getInstalledPackagesAsUser(GET_ACTIVITIES_FLAGS, userId)
packageManager
.getInstalledPackagesAsUser(GET_ACTIVITIES_FLAGS, userId)
.filter { it.supportsPictureInPicture() }
.map { it.packageName }
.toSet()

View File

@@ -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<SettingsEntry> {
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() }
}
}

View File

@@ -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
}

View File

@@ -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<Boolean?>
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
}