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.ModifySystemSettingsAppListProvider
import com.android.settings.spa.app.specialaccess.PictureInPictureListProvider import com.android.settings.spa.app.specialaccess.PictureInPictureListProvider
import com.android.settings.spa.app.specialaccess.SpecialAppAccessPageProvider 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.development.UsageStatsPageProvider
import com.android.settings.spa.home.HomePageProvider import com.android.settings.spa.home.HomePageProvider
import com.android.settings.spa.notification.AppListNotificationsPageProvider 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) { open class SettingsSpaEnvironment(context: Context) : SpaEnvironment(context) {
override val pageProviderRepository = lazy { override val pageProviderRepository = lazy {
val togglePermissionAppListTemplate = TogglePermissionAppListTemplate( val togglePermissionAppListTemplate =
allProviders = listOf( TogglePermissionAppListTemplate(
AllFilesAccessAppListProvider, allProviders =
DisplayOverOtherAppsAppListProvider, listOf(
MediaManagementAppsAppListProvider, AllFilesAccessAppListProvider,
ModifySystemSettingsAppListProvider, DisplayOverOtherAppsAppListProvider,
PictureInPictureListProvider, MediaManagementAppsAppListProvider,
InstallUnknownAppsListProvider, ModifySystemSettingsAppListProvider,
AlarmsAndRemindersAppListProvider, PictureInPictureListProvider,
), InstallUnknownAppsListProvider,
) AlarmsAndRemindersAppListProvider,
WifiControlAppListProvider,
),
)
SettingsPageProviderRepository( SettingsPageProviderRepository(
allPageProviders = listOf( allPageProviders = listOf(
HomePageProvider, HomePageProvider,

View File

@@ -19,6 +19,7 @@ package com.android.settings.spa.app.specialaccess
import android.Manifest import android.Manifest
import android.app.AppGlobals import android.app.AppGlobals
import android.app.AppOpsManager.MODE_DEFAULT import android.app.AppOpsManager.MODE_DEFAULT
import android.app.AppOpsManager.MODE_ERRORED
import android.app.AppOpsManager.OP_REQUEST_INSTALL_PACKAGES import android.app.AppOpsManager.OP_REQUEST_INSTALL_PACKAGES
import android.content.Context import android.content.Context
import android.content.pm.ApplicationInfo 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 pageTitleResId = R.string.install_other_apps
override val switchTitleResId = R.string.external_source_switch_title override val switchTitleResId = R.string.external_source_switch_title
override val footerResId = R.string.install_all_warning override val footerResId = R.string.install_all_warning
override val switchRestrictionKeys = listOf( override val switchRestrictionKeys =
UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES, listOf(
UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES_GLOBALLY, UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES,
) UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES_GLOBALLY,
)
override fun transformItem(app: ApplicationInfo) = InstallUnknownAppsRecord( override fun transformItem(app: ApplicationInfo) =
app = app, InstallUnknownAppsRecord(
appOpsController = AppOpsController(
context = context,
app = app, app = app,
op = OP_REQUEST_INSTALL_PACKAGES, appOpsController =
), AppOpsController(
) context = context,
app = app,
op = OP_REQUEST_INSTALL_PACKAGES,
modeForNotAllowed = MODE_ERRORED
),
)
override fun filter( override fun filter(
userIdFlow: Flow<Int>, recordListFlow: Flow<List<InstallUnknownAppsRecord>>, userIdFlow: Flow<Int>,
) = userIdFlow.map(::getPotentialPackageNames) recordListFlow: Flow<List<InstallUnknownAppsRecord>>,
.combine(recordListFlow) { potentialPackageNames, recordList -> ) =
recordList.filter { record -> userIdFlow.map(::getPotentialPackageNames).combine(recordListFlow) {
isChangeable(record, potentialPackageNames) potentialPackageNames,
} recordList ->
recordList.filter { record -> isChangeable(record, potentialPackageNames) }
} }
@Composable @Composable
@@ -88,12 +94,13 @@ class InstallUnknownAppsListModel(private val context: Context) :
private fun isChangeable( private fun isChangeable(
record: InstallUnknownAppsRecord, record: InstallUnknownAppsRecord,
potentialPackageNames: Set<String>, potentialPackageNames: Set<String>,
) = record.appOpsController.getMode() != MODE_DEFAULT || ) =
record.appOpsController.getMode() != MODE_DEFAULT ||
record.app.packageName in potentialPackageNames record.app.packageName in potentialPackageNames
private fun getPotentialPackageNames(userId: Int): Set<String> = private fun getPotentialPackageNames(userId: Int): Set<String> =
AppGlobals.getPackageManager().getAppOpPermissionPackages( AppGlobals.getPackageManager()
Manifest.permission.REQUEST_INSTALL_PACKAGES, userId .getAppOpPermissionPackages(Manifest.permission.REQUEST_INSTALL_PACKAGES, userId)
).toSet() .toSet()
} }
} }

View File

@@ -16,6 +16,7 @@
package com.android.settings.spa.app.specialaccess package com.android.settings.spa.app.specialaccess
import android.app.AppOpsManager.MODE_ERRORED
import android.app.AppOpsManager.OP_PICTURE_IN_PICTURE import android.app.AppOpsManager.OP_PICTURE_IN_PICTURE
import android.content.Context import android.content.Context
import android.content.pm.ActivityInfo import android.content.pm.ActivityInfo
@@ -46,8 +47,8 @@ data class PictureInPictureRecord(
val appOpsController: AppOpsController, val appOpsController: AppOpsController,
) : AppRecord ) : AppRecord
class PictureInPictureListModel(private val context: Context) class PictureInPictureListModel(private val context: Context) :
: TogglePermissionAppListModel<PictureInPictureRecord> { TogglePermissionAppListModel<PictureInPictureRecord> {
override val pageTitleResId = R.string.picture_in_picture_title override val pageTitleResId = R.string.picture_in_picture_title
override val switchTitleResId = R.string.picture_in_picture_app_detail_switch override val switchTitleResId = R.string.picture_in_picture_app_detail_switch
override val footerResId = R.string.picture_in_picture_app_detail_summary 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 private val packageManager = context.packageManager
override fun transform(userIdFlow: Flow<Int>, appListFlow: Flow<List<ApplicationInfo>>) = override fun transform(userIdFlow: Flow<Int>, appListFlow: Flow<List<ApplicationInfo>>) =
userIdFlow.map(::getPictureInPicturePackages) userIdFlow.map(::getPictureInPicturePackages).combine(appListFlow) {
.combine(appListFlow) { pictureInPicturePackages, appList -> pictureInPicturePackages,
appList.map { app -> appList ->
createPictureInPictureRecord( appList.map { app ->
app = app, createPictureInPictureRecord(
isSupport = app.packageName in pictureInPicturePackages, app = app,
) isSupport = app.packageName in pictureInPicturePackages,
} )
} }
}
override fun transformItem(app: ApplicationInfo): PictureInPictureRecord { override fun transformItem(app: ApplicationInfo): PictureInPictureRecord {
val packageInfo = val packageInfo =
@@ -78,17 +80,17 @@ class PictureInPictureListModel(private val context: Context)
PictureInPictureRecord( PictureInPictureRecord(
app = app, app = app,
isSupport = isSupport, isSupport = isSupport,
appOpsController = AppOpsController( appOpsController =
context = context, AppOpsController(
app = app, context = context,
op = OP_PICTURE_IN_PICTURE, app = app,
), op = OP_PICTURE_IN_PICTURE,
modeForNotAllowed = MODE_ERRORED,
),
) )
override fun filter(userIdFlow: Flow<Int>, recordListFlow: Flow<List<PictureInPictureRecord>>) = override fun filter(userIdFlow: Flow<Int>, recordListFlow: Flow<List<PictureInPictureRecord>>) =
recordListFlow.map { recordList -> recordListFlow.map { recordList -> recordList.filter { it.isSupport } }
recordList.filter { it.isSupport }
}
@Composable @Composable
override fun isAllowed(record: PictureInPictureRecord) = override fun isAllowed(record: PictureInPictureRecord) =
@@ -101,7 +103,8 @@ class PictureInPictureListModel(private val context: Context)
} }
private fun getPictureInPicturePackages(userId: Int): Set<String> = private fun getPictureInPicturePackages(userId: Int): Set<String> =
packageManager.getInstalledPackagesAsUser(GET_ACTIVITIES_FLAGS, userId) packageManager
.getInstalledPackagesAsUser(GET_ACTIVITIES_FLAGS, userId)
.filter { it.supportsPictureInPicture() } .filter { it.supportsPictureInPicture() }
.map { it.packageName } .map { it.packageName }
.toSet() .toSet()

View File

@@ -43,10 +43,12 @@ object SpecialAppAccessPageProvider : SettingsPageProvider {
@Composable @Composable
fun EntryItem() { fun EntryItem() {
Preference(object : PreferenceModel { Preference(
override val title = stringResource(R.string.special_access) object : PreferenceModel {
override val onClick = navigator(name) override val title = stringResource(R.string.special_access)
}) override val onClick = navigator(name)
}
)
} }
fun buildInjectEntry() = SettingsEntryBuilder.createInject(owner = SettingsPage.create(name)) fun buildInjectEntry() = SettingsEntryBuilder.createInject(owner = SettingsPage.create(name))
@@ -54,13 +56,15 @@ object SpecialAppAccessPageProvider : SettingsPageProvider {
override fun buildEntry(arguments: Bundle?): List<SettingsEntry> { override fun buildEntry(arguments: Bundle?): List<SettingsEntry> {
val owner = SettingsPage.create(name, parameter = parameter, arguments = arguments) val owner = SettingsPage.create(name, parameter = parameter, arguments = arguments)
return listOf( return listOf(
AllFilesAccessAppListProvider, AllFilesAccessAppListProvider,
DisplayOverOtherAppsAppListProvider, DisplayOverOtherAppsAppListProvider,
MediaManagementAppsAppListProvider, MediaManagementAppsAppListProvider,
ModifySystemSettingsAppListProvider, ModifySystemSettingsAppListProvider,
PictureInPictureListProvider, PictureInPictureListProvider,
InstallUnknownAppsListProvider, InstallUnknownAppsListProvider,
AlarmsAndRemindersAppListProvider, AlarmsAndRemindersAppListProvider,
).map { it.buildAppListInjectEntry().setLink(fromPage = owner).build() } 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
}