Loading src/com/android/settings/applications/manageapplications/ManageApplicationsUtil.kt +2 −1 Original line number Diff line number Diff line Loading @@ -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_WRITE_SETTINGS 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.specialaccess.AlarmsAndRemindersAppListProvider import com.android.settings.spa.app.specialaccess.AllFilesAccessAppListProvider Loading @@ -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.TurnScreenOnAppsAppListProvider 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.system.AppLanguagesPageProvider Loading Loading @@ -127,6 +127,7 @@ object ManageApplicationsUtil { // TODO(b/292165031) enable once sorting is supported //LIST_TYPE_STORAGE -> StorageAppListPageProvider.Apps.name //LIST_TYPE_GAMES -> StorageAppListPageProvider.Games.name LIST_TYPE_BATTERY_OPTIMIZATION -> BatteryOptimizationModeAppListPageProvider.name else -> null } } Loading src/com/android/settings/fuelgauge/BatteryOptimizeUtils.java +19 −15 Original line number Diff line number Diff line Loading @@ -112,11 +112,28 @@ public class BatteryOptimizeUtils { /** Gets the {@link OptimizationMode} for associated app. */ @OptimizationMode public int getAppOptimizationMode() { refreshState(); public int getAppOptimizationMode(boolean refreshList) { 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); } /** Gets the {@link OptimizationMode} for associated app. */ @OptimizationMode public int getAppOptimizationMode() { return getAppOptimizationMode(true); } /** Resets optimization mode for all applications. */ public static void resetAppOptimizationMode( Context context, IPackageManager ipm, AppOpsManager aom) { Loading Loading @@ -336,19 +353,6 @@ public class BatteryOptimizeUtils { 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) { return appStandbyMode < 0 ? "Apply optimize setting ERROR" Loading src/com/android/settings/spa/SettingsSpaEnvironment.kt +2 −0 Original line number Diff line number Diff line Loading @@ -22,6 +22,7 @@ import com.android.settings.network.apn.ApnEditPageProvider import com.android.settings.spa.about.AboutPhonePageProvider import com.android.settings.spa.app.AllAppListPageProvider 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.appinfo.AppInfoSettingsProvider import com.android.settings.spa.app.appinfo.CloneAppInfoSettingsProvider Loading Loading @@ -116,6 +117,7 @@ open class SettingsSpaEnvironment(context: Context) : SpaEnvironment(context) { StorageAppListPageProvider.Games, ApnEditPageProvider, SimOnboardingPageProvider, BatteryOptimizationModeAppListPageProvider, ) override val logger = if (FeatureFlagUtils.isEnabled( Loading src/com/android/settings/spa/app/battery/BatteryOptimizationModeAppListPageProvider.kt 0 → 100644 +163 −0 Original line number Diff line number Diff line /* * 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); } tests/spa_unit/src/com/android/settings/spa/app/battery/BatteryOptimizationModeAppListPageProviderTest.kt 0 → 100644 +295 −0 Original line number Diff line number Diff line /* * 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 } } } Loading
src/com/android/settings/applications/manageapplications/ManageApplicationsUtil.kt +2 −1 Original line number Diff line number Diff line Loading @@ -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_WRITE_SETTINGS 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.specialaccess.AlarmsAndRemindersAppListProvider import com.android.settings.spa.app.specialaccess.AllFilesAccessAppListProvider Loading @@ -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.TurnScreenOnAppsAppListProvider 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.system.AppLanguagesPageProvider Loading Loading @@ -127,6 +127,7 @@ object ManageApplicationsUtil { // TODO(b/292165031) enable once sorting is supported //LIST_TYPE_STORAGE -> StorageAppListPageProvider.Apps.name //LIST_TYPE_GAMES -> StorageAppListPageProvider.Games.name LIST_TYPE_BATTERY_OPTIMIZATION -> BatteryOptimizationModeAppListPageProvider.name else -> null } } Loading
src/com/android/settings/fuelgauge/BatteryOptimizeUtils.java +19 −15 Original line number Diff line number Diff line Loading @@ -112,11 +112,28 @@ public class BatteryOptimizeUtils { /** Gets the {@link OptimizationMode} for associated app. */ @OptimizationMode public int getAppOptimizationMode() { refreshState(); public int getAppOptimizationMode(boolean refreshList) { 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); } /** Gets the {@link OptimizationMode} for associated app. */ @OptimizationMode public int getAppOptimizationMode() { return getAppOptimizationMode(true); } /** Resets optimization mode for all applications. */ public static void resetAppOptimizationMode( Context context, IPackageManager ipm, AppOpsManager aom) { Loading Loading @@ -336,19 +353,6 @@ public class BatteryOptimizeUtils { 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) { return appStandbyMode < 0 ? "Apply optimize setting ERROR" Loading
src/com/android/settings/spa/SettingsSpaEnvironment.kt +2 −0 Original line number Diff line number Diff line Loading @@ -22,6 +22,7 @@ import com.android.settings.network.apn.ApnEditPageProvider import com.android.settings.spa.about.AboutPhonePageProvider import com.android.settings.spa.app.AllAppListPageProvider 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.appinfo.AppInfoSettingsProvider import com.android.settings.spa.app.appinfo.CloneAppInfoSettingsProvider Loading Loading @@ -116,6 +117,7 @@ open class SettingsSpaEnvironment(context: Context) : SpaEnvironment(context) { StorageAppListPageProvider.Games, ApnEditPageProvider, SimOnboardingPageProvider, BatteryOptimizationModeAppListPageProvider, ) override val logger = if (FeatureFlagUtils.isEnabled( Loading
src/com/android/settings/spa/app/battery/BatteryOptimizationModeAppListPageProvider.kt 0 → 100644 +163 −0 Original line number Diff line number Diff line /* * 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); }
tests/spa_unit/src/com/android/settings/spa/app/battery/BatteryOptimizationModeAppListPageProviderTest.kt 0 → 100644 +295 −0 Original line number Diff line number Diff line /* * 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 } } }