Migrate battery optimization mode app list to SPA.

Bug: 284033422
Bug: 304923046
Test: manual
Change-Id: I7549b8a95508500bce098b42da04036869431060
This commit is contained in:
mxyyiyi
2024-01-22 18:05:57 +08:00
parent 55fc646388
commit 8dccd6947f
5 changed files with 481 additions and 16 deletions

View File

@@ -59,6 +59,7 @@ import com.android.settings.applications.manageapplications.ManageApplications.L
import com.android.settings.applications.manageapplications.ManageApplications.LIST_TYPE_WIFI_ACCESS import com.android.settings.applications.manageapplications.ManageApplications.LIST_TYPE_WIFI_ACCESS
import com.android.settings.applications.manageapplications.ManageApplications.LIST_TYPE_WRITE_SETTINGS import com.android.settings.applications.manageapplications.ManageApplications.LIST_TYPE_WRITE_SETTINGS
import com.android.settings.spa.app.AllAppListPageProvider import com.android.settings.spa.app.AllAppListPageProvider
import com.android.settings.spa.app.battery.BatteryOptimizationModeAppListPageProvider
import com.android.settings.spa.app.appcompat.UserAspectRatioAppsPageProvider import com.android.settings.spa.app.appcompat.UserAspectRatioAppsPageProvider
import com.android.settings.spa.app.specialaccess.AlarmsAndRemindersAppListProvider import com.android.settings.spa.app.specialaccess.AlarmsAndRemindersAppListProvider
import com.android.settings.spa.app.specialaccess.AllFilesAccessAppListProvider import com.android.settings.spa.app.specialaccess.AllFilesAccessAppListProvider
@@ -70,7 +71,6 @@ import com.android.settings.spa.app.specialaccess.ModifySystemSettingsAppListPro
import com.android.settings.spa.app.specialaccess.NfcTagAppsSettingsProvider import com.android.settings.spa.app.specialaccess.NfcTagAppsSettingsProvider
import com.android.settings.spa.app.specialaccess.TurnScreenOnAppsAppListProvider import com.android.settings.spa.app.specialaccess.TurnScreenOnAppsAppListProvider
import com.android.settings.spa.app.specialaccess.WifiControlAppListProvider import com.android.settings.spa.app.specialaccess.WifiControlAppListProvider
import com.android.settings.spa.app.storage.StorageAppListPageProvider
import com.android.settings.spa.notification.AppListNotificationsPageProvider import com.android.settings.spa.notification.AppListNotificationsPageProvider
import com.android.settings.spa.system.AppLanguagesPageProvider import com.android.settings.spa.system.AppLanguagesPageProvider
@@ -127,6 +127,7 @@ object ManageApplicationsUtil {
// TODO(b/292165031) enable once sorting is supported // TODO(b/292165031) enable once sorting is supported
//LIST_TYPE_STORAGE -> StorageAppListPageProvider.Apps.name //LIST_TYPE_STORAGE -> StorageAppListPageProvider.Apps.name
//LIST_TYPE_GAMES -> StorageAppListPageProvider.Games.name //LIST_TYPE_GAMES -> StorageAppListPageProvider.Games.name
LIST_TYPE_BATTERY_OPTIMIZATION -> BatteryOptimizationModeAppListPageProvider.name
else -> null else -> null
} }
} }

View File

@@ -112,11 +112,28 @@ public class BatteryOptimizeUtils {
/** Gets the {@link OptimizationMode} for associated app. */ /** Gets the {@link OptimizationMode} for associated app. */
@OptimizationMode @OptimizationMode
public int getAppOptimizationMode() { public int getAppOptimizationMode(boolean refreshList) {
refreshState(); if (refreshList) {
mPowerAllowListBackend.refreshList();
}
mAllowListed = mPowerAllowListBackend.isAllowlisted(mPackageName, mUid);
mMode =
mAppOpsManager.checkOpNoThrow(
AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, mUid, mPackageName);
Log.d(
TAG,
String.format(
"refresh %s state, allowlisted = %s, mode = %d",
mPackageName, mAllowListed, mMode));
return getAppOptimizationMode(mMode, mAllowListed); return getAppOptimizationMode(mMode, mAllowListed);
} }
/** Gets the {@link OptimizationMode} for associated app. */
@OptimizationMode
public int getAppOptimizationMode() {
return getAppOptimizationMode(true);
}
/** Resets optimization mode for all applications. */ /** Resets optimization mode for all applications. */
public static void resetAppOptimizationMode( public static void resetAppOptimizationMode(
Context context, IPackageManager ipm, AppOpsManager aom) { Context context, IPackageManager ipm, AppOpsManager aom) {
@@ -336,19 +353,6 @@ public class BatteryOptimizeUtils {
context, action, packageNameKey, createLogEvent(appStandbyMode, allowListed)); context, action, packageNameKey, createLogEvent(appStandbyMode, allowListed));
} }
private void refreshState() {
mPowerAllowListBackend.refreshList();
mAllowListed = mPowerAllowListBackend.isAllowlisted(mPackageName, mUid);
mMode =
mAppOpsManager.checkOpNoThrow(
AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, mUid, mPackageName);
Log.d(
TAG,
String.format(
"refresh %s state, allowlisted = %s, mode = %d",
mPackageName, mAllowListed, mMode));
}
private static String createLogEvent(int appStandbyMode, boolean allowListed) { private static String createLogEvent(int appStandbyMode, boolean allowListed) {
return appStandbyMode < 0 return appStandbyMode < 0
? "Apply optimize setting ERROR" ? "Apply optimize setting ERROR"

View File

@@ -22,6 +22,7 @@ import com.android.settings.network.apn.ApnEditPageProvider
import com.android.settings.spa.about.AboutPhonePageProvider import com.android.settings.spa.about.AboutPhonePageProvider
import com.android.settings.spa.app.AllAppListPageProvider import com.android.settings.spa.app.AllAppListPageProvider
import com.android.settings.spa.app.AppsMainPageProvider import com.android.settings.spa.app.AppsMainPageProvider
import com.android.settings.spa.app.battery.BatteryOptimizationModeAppListPageProvider
import com.android.settings.spa.app.appcompat.UserAspectRatioAppsPageProvider import com.android.settings.spa.app.appcompat.UserAspectRatioAppsPageProvider
import com.android.settings.spa.app.appinfo.AppInfoSettingsProvider import com.android.settings.spa.app.appinfo.AppInfoSettingsProvider
import com.android.settings.spa.app.appinfo.CloneAppInfoSettingsProvider import com.android.settings.spa.app.appinfo.CloneAppInfoSettingsProvider
@@ -116,6 +117,7 @@ open class SettingsSpaEnvironment(context: Context) : SpaEnvironment(context) {
StorageAppListPageProvider.Games, StorageAppListPageProvider.Games,
ApnEditPageProvider, ApnEditPageProvider,
SimOnboardingPageProvider, SimOnboardingPageProvider,
BatteryOptimizationModeAppListPageProvider,
) )
override val logger = if (FeatureFlagUtils.isEnabled( override val logger = if (FeatureFlagUtils.isEnabled(

View File

@@ -0,0 +1,163 @@
/*
* Copyright (C) 2024 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.battery
import android.app.AppOpsManager
import android.content.Context
import android.content.pm.ApplicationInfo
import android.os.Bundle
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import androidx.core.os.bundleOf
import com.android.settings.R
import com.android.settings.Utils
import com.android.settings.core.SubSettingLauncher
import com.android.settings.fuelgauge.AdvancedPowerUsageDetail
import com.android.settings.fuelgauge.BatteryOptimizeUtils
import com.android.settings.spa.app.AppRecordWithSize
import com.android.settings.spa.app.appinfo.AppInfoSettingsProvider
import com.android.settings.spa.app.rememberResetAppDialogPresenter
import com.android.settingslib.fuelgauge.PowerAllowlistBackend
import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
import com.android.settingslib.spa.framework.common.SettingsPageProvider
import com.android.settingslib.spa.framework.common.createSettingsPage
import com.android.settingslib.spa.framework.compose.navigator
import com.android.settingslib.spa.framework.compose.rememberContext
import com.android.settingslib.spa.framework.util.filterItem
import com.android.settingslib.spa.framework.util.mapItem
import com.android.settingslib.spa.widget.preference.Preference
import com.android.settingslib.spa.widget.preference.PreferenceModel
import com.android.settingslib.spa.widget.ui.SpinnerOption
import com.android.settingslib.spaprivileged.framework.compose.getPlaceholder
import com.android.settingslib.spaprivileged.model.app.AppListModel
import com.android.settingslib.spaprivileged.model.app.installed
import com.android.settingslib.spaprivileged.model.app.userHandle
import com.android.settingslib.spaprivileged.template.app.AppList
import com.android.settingslib.spaprivileged.template.app.AppListInput
import com.android.settingslib.spaprivileged.template.app.AppListItem
import com.android.settingslib.spaprivileged.template.app.AppListItemModel
import com.android.settingslib.spaprivileged.template.app.AppListPage
import kotlinx.coroutines.flow.Flow
object BatteryOptimizationModeAppListPageProvider : SettingsPageProvider {
override val name = "BatteryOptimizationModeAppList"
private val owner = createSettingsPage()
@Composable
override fun Page(arguments: Bundle?) {
BatteryOptimizationModeAppList()
}
fun buildInjectEntry() = SettingsEntryBuilder
.createInject(owner)
.setSearchDataFn { null }
.setUiLayoutFn {
Preference(object : PreferenceModel {
override val title = stringResource(R.string.app_battery_usage_title)
override val onClick = navigator(name)
})
}
}
@Composable
fun BatteryOptimizationModeAppList(
appList: @Composable AppListInput<AppRecordWithSize>.() -> Unit = { AppList() },
) {
AppListPage(
title = stringResource(R.string.app_battery_usage_title),
listModel = rememberContext(::BatteryOptimizationModeAppListModel),
appList = appList,
)
}
class BatteryOptimizationModeAppListModel(
private val context: Context,
) : AppListModel<AppRecordWithSize> {
override fun getSpinnerOptions(recordList: List<AppRecordWithSize>): List<SpinnerOption> =
OptimizationModeSpinnerItem.entries.map {
SpinnerOption(
id = it.ordinal,
text = context.getString(it.stringResId),
)
}
override fun transform(userIdFlow: Flow<Int>, appListFlow: Flow<List<ApplicationInfo>>) =
appListFlow.mapItem(::AppRecordWithSize)
override fun filter(
userIdFlow: Flow<Int>,
option: Int,
recordListFlow: Flow<List<AppRecordWithSize>>,
): Flow<List<AppRecordWithSize>> {
PowerAllowlistBackend.getInstance(context).refreshList()
return recordListFlow.filterItem {
val appOptimizationMode = BatteryOptimizeUtils(context, it.app.uid, it.app.packageName)
.getAppOptimizationMode(/* refreshList */ false);
when (OptimizationModeSpinnerItem.entries.getOrNull(option)) {
OptimizationModeSpinnerItem.Restricted ->
appOptimizationMode == BatteryOptimizeUtils.MODE_RESTRICTED
OptimizationModeSpinnerItem.Optimized ->
appOptimizationMode == BatteryOptimizeUtils.MODE_OPTIMIZED
OptimizationModeSpinnerItem.Unrestricted ->
appOptimizationMode == BatteryOptimizeUtils.MODE_UNRESTRICTED
else -> (true)
}
}
}
@Composable
override fun getSummary(option: Int, record: AppRecordWithSize): () -> String = {
var summary = String()
val app = record.app
when {
!app.installed && !app.isArchived -> {
summary += context.getString(R.string.not_installed)
}
!app.enabled -> {
summary += context.getString(com.android.settingslib.R.string.disabled)
}
}
summary
}
@Composable
override fun AppListItemModel<AppRecordWithSize>.AppItem() {
AppListItem(onClick = {
val args = bundleOf(
AdvancedPowerUsageDetail.EXTRA_PACKAGE_NAME to record.app.packageName,
AdvancedPowerUsageDetail.EXTRA_POWER_USAGE_PERCENT to Utils.formatPercentage(0),
AdvancedPowerUsageDetail.EXTRA_UID to record.app.uid,
)
SubSettingLauncher(context)
.setDestination(AdvancedPowerUsageDetail::class.java.name)
.setTitleRes(R.string.battery_details_title)
.setArguments(args)
.setUserHandle(record.app.userHandle)
.setSourceMetricsCategory(AppInfoSettingsProvider.METRICS_CATEGORY)
.launch()
})
}
}
private enum class OptimizationModeSpinnerItem(val stringResId: Int) {
All(R.string.filter_all_apps),
Restricted(R.string.filter_battery_restricted_title),
Optimized(R.string.filter_battery_optimized_title),
Unrestricted(R.string.filter_battery_unrestricted_title);
}

View File

@@ -0,0 +1,295 @@
/*
* Copyright (C) 2024 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
import android.content.Context
import android.content.Intent
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.graphics.drawable.Drawable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.settings.DisplaySettings
import com.android.settings.R
import com.android.settings.SettingsActivity
import com.android.settings.fuelgauge.AdvancedPowerUsageDetail
import com.android.settings.spa.app.battery.BatteryOptimizationModeAppList
import com.android.settings.spa.app.battery.BatteryOptimizationModeAppListModel
import com.android.settings.spa.app.battery.BatteryOptimizationModeAppListPageProvider
import com.android.settingslib.spa.testutils.FakeNavControllerWrapper
import com.android.settingslib.spa.testutils.firstWithTimeoutOrNull
import com.android.settingslib.spaprivileged.framework.compose.getPlaceholder
import com.android.settingslib.spaprivileged.template.app.AppListInput
import com.android.settingslib.spaprivileged.template.app.AppListItemModel
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.any
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.doNothing
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.spy
import org.mockito.kotlin.stub
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
@RunWith(AndroidJUnit4::class)
class BatteryOptimizationModeAppListPageProviderTest {
@get:Rule
val composeTestRule = createComposeRule()
private val fakeNavControllerWrapper = FakeNavControllerWrapper()
private val packageManager = mock<PackageManager> {
on { getPackagesForUid(USER_ID) } doReturn arrayOf(PACKAGE_NAME)
}
private val context: Context = spy(ApplicationProvider.getApplicationContext()) {
on { packageManager } doReturn packageManager
}
@Test
fun batteryOptimizationModeAppListPageProvider_name() {
assertThat(BatteryOptimizationModeAppListPageProvider.name)
.isEqualTo("BatteryOptimizationModeAppList")
}
@Test
fun injectEntry_title() {
setInjectEntry()
composeTestRule.onNodeWithText(context.getString(R.string.app_battery_usage_title))
.assertIsDisplayed()
}
@Test
fun injectEntry_onClick_navigate() {
setInjectEntry()
composeTestRule.onNodeWithText(context.getString(R.string.app_battery_usage_title))
.performClick()
assertThat(fakeNavControllerWrapper.navigateCalledWith)
.isEqualTo("BatteryOptimizationModeAppList")
}
@Test
fun title_displayed() {
composeTestRule.setContent {
BatteryOptimizationModeAppList {}
}
composeTestRule.onNodeWithText(context.getString(R.string.app_battery_usage_title))
.assertIsDisplayed()
}
@Test
fun showInstantApps_isFalse() {
val input = getAppListInput()
assertThat(input.config.showInstantApps).isFalse()
}
@Test
fun item_labelDisplayed() {
setItemContent()
composeTestRule.onNodeWithText(LABEL).assertIsDisplayed()
}
@Test
fun item_summaryDisplayed() {
setItemContent()
composeTestRule.onNodeWithText(SUMMARY).assertIsDisplayed()
}
@Test
fun item_onClick_navigate() {
setItemContent()
doNothing().whenever(context).startActivity(any())
composeTestRule.onNodeWithText(LABEL).performClick()
val intent = argumentCaptor<Intent> {
verify(context).startActivity(capture())
}.firstValue
assertThat(intent.getStringExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT))!!
.isEqualTo(AdvancedPowerUsageDetail::class.java.name)
val arguments = intent.getBundleExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS)!!
assertThat(arguments.getString(AdvancedPowerUsageDetail.EXTRA_PACKAGE_NAME))
.isEqualTo(PACKAGE_NAME)
}
@Test
fun BatteryOptimizationModeAppListModel_transform() = runTest {
val listModel = BatteryOptimizationModeAppListModel(context)
val recordListFlow = listModel.transform(flowOf(USER_ID), flowOf(listOf(APP)))
val recordList = recordListFlow.firstWithTimeoutOrNull()!!
assertThat(recordList).hasSize(1)
assertThat(recordList[0].app).isSameInstanceAs(APP)
}
@Test
fun listModelGetSummary_regular() {
val listModel = BatteryOptimizationModeAppListModel(context)
lateinit var summary: () -> String
composeTestRule.setContent {
summary = listModel.getSummary(option = 0, record = AppRecordWithSize(app = APP))
}
assertThat(summary()).isEmpty()
}
@Test
fun listModelGetSummary_disabled() {
val listModel = BatteryOptimizationModeAppListModel(context)
val disabledApp = ApplicationInfo().apply {
packageName = PACKAGE_NAME
flags = ApplicationInfo.FLAG_INSTALLED
enabled = false
}
lateinit var summary: () -> String
composeTestRule.setContent {
summary =
listModel.getSummary(option = 0, record = AppRecordWithSize(app = disabledApp))
}
assertThat(summary())
.isEqualTo(context.getString(com.android.settingslib.R.string.disabled))
}
@Test
fun listModelGetSummary_notInstalled() {
val listModel = BatteryOptimizationModeAppListModel(context)
val notInstalledApp = ApplicationInfo().apply {
packageName = PACKAGE_NAME
}
lateinit var summary: () -> String
composeTestRule.setContent {
summary =
listModel.getSummary(option = 0, record = AppRecordWithSize(app = notInstalledApp))
}
assertThat(summary()).isEqualTo(context.getString(R.string.not_installed))
}
@Test
fun batteryOptimizationModeAppListModel_archivedApp() {
val app = mock<ApplicationInfo> {
on { loadUnbadgedIcon(any()) } doReturn UNBADGED_ICON
on { loadLabel(any()) } doReturn LABEL
}
app.isArchived = true
packageManager.stub {
on {
getApplicationInfoAsUser(PACKAGE_NAME, 0, USER_ID)
} doReturn app
}
composeTestRule.setContent {
fakeNavControllerWrapper.Wrapper {
with(BatteryOptimizationModeAppListModel(context)) {
AppListItemModel(
record = AppRecordWithSize(app = app),
label = LABEL,
summary = { SUMMARY },
).AppItem()
}
}
}
composeTestRule.onNodeWithText(LABEL).assertIsDisplayed()
}
@Test
fun batteryOptimizationModeAppListModel_NoStorageSummary() {
val listModel = BatteryOptimizationModeAppListModel(context)
val archivedApp = ApplicationInfo().apply {
packageName = PACKAGE_NAME
isArchived = true
}
lateinit var summary: () -> String
composeTestRule.setContent {
summary =
listModel.getSummary(option = 0, record = AppRecordWithSize(app = archivedApp))
}
assertThat(summary()).isEmpty()
}
private fun setInjectEntry() {
composeTestRule.setContent {
fakeNavControllerWrapper.Wrapper {
BatteryOptimizationModeAppListPageProvider.buildInjectEntry().build().UiLayout()
}
}
}
private fun getAppListInput(): AppListInput<AppRecordWithSize> {
lateinit var input: AppListInput<AppRecordWithSize>
composeTestRule.setContent {
BatteryOptimizationModeAppList {
SideEffect {
input = this
}
}
}
return input
}
private fun setItemContent() {
composeTestRule.setContent {
fakeNavControllerWrapper.Wrapper {
with(BatteryOptimizationModeAppListModel(context)) {
AppListItemModel(
record = AppRecordWithSize(app = APP),
label = LABEL,
summary = { SUMMARY },
).AppItem()
}
}
}
}
private companion object {
const val USER_ID = 0
const val PACKAGE_NAME = "package.name"
const val LABEL = "Label"
const val SUMMARY = "Summary"
val UNBADGED_ICON = mock<Drawable>()
val APP = ApplicationInfo().apply {
packageName = PACKAGE_NAME
flags = ApplicationInfo.FLAG_INSTALLED
}
}
}