Add AppBatteryPreference for Spa
This is used in new App Info page.
To try:
1. adb shell am start -n com.android.settings/.spa.SpaActivity
2. Go to Apps -> All apps -> [One App] -> App battery usage
Bug: 236346018
Test: Unit test & Manual with Settings App
Change-Id: I4784e42f230534d8d843ec00de51032baffcb7e2
diff --git a/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetail.java b/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetail.java
index 32e2e2f..e3919b0 100644
--- a/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetail.java
+++ b/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetail.java
@@ -134,6 +134,14 @@
public static void startBatteryDetailPage(
Activity caller, InstrumentedPreferenceFragment fragment,
BatteryDiffEntry diffEntry, String usagePercent, String slotInformation) {
+ startBatteryDetailPage(
+ caller, fragment.getMetricsCategory(), diffEntry, usagePercent, slotInformation);
+ }
+
+ /** Launches battery details page for an individual battery consumer fragment. */
+ public static void startBatteryDetailPage(
+ Context context, int sourceMetricsCategory,
+ BatteryDiffEntry diffEntry, String usagePercent, String slotInformation) {
final BatteryHistEntry histEntry = diffEntry.mBatteryHistEntry;
final LaunchBatteryDetailPageArgs launchArgs = new LaunchBatteryDetailPageArgs();
// configure the launch argument.
@@ -147,7 +155,7 @@
launchArgs.mForegroundTimeMs = diffEntry.mForegroundUsageTimeInMs;
launchArgs.mBackgroundTimeMs = diffEntry.mBackgroundUsageTimeInMs;
launchArgs.mIsUserEntry = histEntry.isUserEntry();
- startBatteryDetailPage(caller, fragment, launchArgs);
+ startBatteryDetailPage(context, sourceMetricsCategory, launchArgs);
}
/** Launches battery details page for an individual battery consumer. */
@@ -165,11 +173,11 @@
launchArgs.mForegroundTimeMs = isValidToShowSummary ? entry.getTimeInForegroundMs() : 0;
launchArgs.mBackgroundTimeMs = isValidToShowSummary ? entry.getTimeInBackgroundMs() : 0;
launchArgs.mIsUserEntry = entry.isUserEntry();
- startBatteryDetailPage(caller, fragment, launchArgs);
+ startBatteryDetailPage(caller, fragment.getMetricsCategory(), launchArgs);
}
- private static void startBatteryDetailPage(Activity caller,
- InstrumentedPreferenceFragment fragment, LaunchBatteryDetailPageArgs launchArgs) {
+ private static void startBatteryDetailPage(
+ Context context, int sourceMetricsCategory, LaunchBatteryDetailPageArgs launchArgs) {
final Bundle args = new Bundle();
if (launchArgs.mPackageName == null) {
// populate data for system app
@@ -190,11 +198,11 @@
final int userId = launchArgs.mIsUserEntry ? ActivityManager.getCurrentUser()
: UserHandle.getUserId(launchArgs.mUid);
- new SubSettingLauncher(caller)
+ new SubSettingLauncher(context)
.setDestination(AdvancedPowerUsageDetail.class.getName())
.setTitleRes(R.string.battery_details_title)
.setArguments(args)
- .setSourceMetricsCategory(fragment.getMetricsCategory())
+ .setSourceMetricsCategory(sourceMetricsCategory)
.setUserHandle(new UserHandle(userId))
.launch();
}
diff --git a/src/com/android/settings/spa/app/appinfo/AppBatteryPreference.kt b/src/com/android/settings/spa/app/appinfo/AppBatteryPreference.kt
new file mode 100644
index 0000000..a2164b2
--- /dev/null
+++ b/src/com/android/settings/spa/app/appinfo/AppBatteryPreference.kt
@@ -0,0 +1,159 @@
+/*
+ * 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.appinfo
+
+import android.app.settings.SettingsEnums
+import android.content.Context
+import android.content.pm.ApplicationInfo
+import android.util.Log
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalLifecycleOwner
+import androidx.compose.ui.res.stringResource
+import androidx.core.os.bundleOf
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.repeatOnLifecycle
+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.batteryusage.BatteryChartPreferenceController
+import com.android.settings.fuelgauge.batteryusage.BatteryDiffEntry
+import com.android.settingslib.spa.widget.preference.Preference
+import com.android.settingslib.spa.widget.preference.PreferenceModel
+import com.android.settingslib.spaprivileged.model.app.installed
+import com.android.settingslib.spaprivileged.model.app.userId
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+@Composable
+fun AppBatteryPreference(app: ApplicationInfo) {
+ val context = LocalContext.current
+ val presenter = remember { AppBatteryPresenter(context, app) }
+ if (!presenter.isAvailable()) return
+
+ Preference(object : PreferenceModel {
+ override val title = stringResource(R.string.app_battery_usage_title)
+ override val summary = presenter.summary
+ override val enabled = presenter.enabled
+ override val onClick = presenter::startActivity
+ })
+
+ presenter.Updater()
+}
+
+private class AppBatteryPresenter(private val context: Context, private val app: ApplicationInfo) {
+ private var batteryDiffEntryState: LoadingState<BatteryDiffEntry?>
+ by mutableStateOf(LoadingState.Loading)
+
+ @Composable
+ fun isAvailable() = remember {
+ context.resources.getBoolean(R.bool.config_show_app_info_settings_battery)
+ }
+
+ @Composable
+ fun Updater() {
+ if (!app.installed) return
+ val current = LocalLifecycleOwner.current
+ LaunchedEffect(app) {
+ current.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ launch { batteryDiffEntryState = LoadingState.Done(getBatteryDiffEntry()) }
+ }
+ }
+ }
+
+ private suspend fun getBatteryDiffEntry(): BatteryDiffEntry? = withContext(Dispatchers.IO) {
+ BatteryChartPreferenceController.getAppBatteryUsageData(
+ context, app.packageName, app.userId
+ ).also {
+ Log.d(TAG, "loadBatteryDiffEntries():\n$it")
+ }
+ }
+
+ val enabled = derivedStateOf { batteryDiffEntryState is LoadingState.Done }
+
+ val summary = derivedStateOf<String> {
+ if (!app.installed) return@derivedStateOf ""
+ batteryDiffEntryState.let { batteryDiffEntryState ->
+ when (batteryDiffEntryState) {
+ is LoadingState.Loading -> context.getString(R.string.summary_placeholder)
+ is LoadingState.Done -> batteryDiffEntryState.result.getSummary()
+ }
+ }
+ }
+
+ private fun BatteryDiffEntry?.getSummary(): String =
+ this?.takeIf { mConsumePower > 0 }?.let {
+ context.getString(
+ R.string.battery_summary, Utils.formatPercentage(percentOfTotal, true)
+ )
+ } ?: context.getString(R.string.no_battery_summary)
+
+ fun startActivity() {
+ batteryDiffEntryState.resultOrNull?.run {
+ startBatteryDetailPage()
+ return
+ }
+
+ fallbackStartBatteryDetailPage()
+ }
+
+ private fun BatteryDiffEntry.startBatteryDetailPage() {
+ Log.i(TAG, "handlePreferenceTreeClick():\n$this")
+ AdvancedPowerUsageDetail.startBatteryDetailPage(
+ context,
+ SettingsEnums.APPLICATIONS_INSTALLED_APP_DETAILS,
+ this,
+ Utils.formatPercentage(percentOfTotal, true),
+ null,
+ )
+ }
+
+ private fun fallbackStartBatteryDetailPage() {
+ Log.i(TAG, "Launch : ${app.packageName} with package name")
+ val args = bundleOf(
+ AdvancedPowerUsageDetail.EXTRA_PACKAGE_NAME to app.packageName,
+ AdvancedPowerUsageDetail.EXTRA_POWER_USAGE_PERCENT to Utils.formatPercentage(0),
+ AdvancedPowerUsageDetail.EXTRA_UID to app.uid,
+ )
+ SubSettingLauncher(context)
+ .setDestination(AdvancedPowerUsageDetail::class.java.name)
+ .setTitleRes(R.string.battery_details_title)
+ .setArguments(args)
+ .setSourceMetricsCategory(SettingsEnums.APPLICATIONS_INSTALLED_APP_DETAILS)
+ .launch()
+ }
+
+ companion object {
+ private const val TAG = "AppBatteryPresenter"
+ }
+}
+
+private sealed class LoadingState<out T> {
+ object Loading : LoadingState<Nothing>()
+
+ data class Done<T>(val result: T) : LoadingState<T>()
+
+ val resultOrNull: T? get() = if (this is Done) result else null
+}
diff --git a/src/com/android/settings/spa/app/appinfo/AppInfoSettings.kt b/src/com/android/settings/spa/app/appinfo/AppInfoSettings.kt
index 9a286c7..b4b6945 100644
--- a/src/com/android/settings/spa/app/appinfo/AppInfoSettings.kt
+++ b/src/com/android/settings/spa/app/appinfo/AppInfoSettings.kt
@@ -95,7 +95,7 @@
// TODO: instant_app_launch_supported_domain_urls
// TODO: data_settings
AppTimeSpentPreference(app)
- // TODO: battery
+ AppBatteryPreference(app)
AppLocalePreference(app)
AppOpenByDefaultPreference(app)
DefaultAppShortcuts(app)
diff --git a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppBatteryPreferenceTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppBatteryPreferenceTest.kt
new file mode 100644
index 0000000..0657435
--- /dev/null
+++ b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppBatteryPreferenceTest.kt
@@ -0,0 +1,187 @@
+/*
+ * 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.appinfo
+
+import android.app.settings.SettingsEnums
+import android.content.Context
+import android.content.pm.ApplicationInfo
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsEnabled
+import androidx.compose.ui.test.assertIsNotDisplayed
+import androidx.compose.ui.test.assertIsNotEnabled
+import androidx.compose.ui.test.hasTextExactly
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.onRoot
+import androidx.compose.ui.test.performClick
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.dx.mockito.inline.extended.ExtendedMockito
+import com.android.settings.R
+import com.android.settings.fuelgauge.AdvancedPowerUsageDetail
+import com.android.settings.fuelgauge.batteryusage.BatteryChartPreferenceController
+import com.android.settings.fuelgauge.batteryusage.BatteryDiffEntry
+import com.android.settingslib.spaprivileged.model.app.userId
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+import org.mockito.MockitoSession
+import org.mockito.Spy
+import org.mockito.quality.Strictness
+import org.mockito.Mockito.`when` as whenever
+
+@RunWith(AndroidJUnit4::class)
+class AppBatteryPreferenceTest {
+ @get:Rule
+ val composeTestRule = createComposeRule()
+
+ private lateinit var mockSession: MockitoSession
+
+ @Spy
+ private val context: Context = ApplicationProvider.getApplicationContext()
+
+ @Spy
+ private val resources = context.resources
+
+ @Before
+ fun setUp() {
+ mockSession = ExtendedMockito.mockitoSession()
+ .initMocks(this)
+ .mockStatic(BatteryChartPreferenceController::class.java)
+ .mockStatic(AdvancedPowerUsageDetail::class.java)
+ .strictness(Strictness.LENIENT)
+ .startMocking()
+ whenever(context.resources).thenReturn(resources)
+ whenever(resources.getBoolean(R.bool.config_show_app_info_settings_battery))
+ .thenReturn(true)
+ }
+
+ private fun mockBatteryDiffEntry(batteryDiffEntry: BatteryDiffEntry?) {
+ whenever(BatteryChartPreferenceController.getAppBatteryUsageData(
+ context, PACKAGE_NAME, APP.userId
+ )).thenReturn(batteryDiffEntry)
+ }
+
+ @After
+ fun tearDown() {
+ mockSession.finishMocking()
+ }
+
+ @Test
+ fun whenConfigIsFalse_notDisplayed() {
+ whenever(resources.getBoolean(R.bool.config_show_app_info_settings_battery))
+ .thenReturn(false)
+
+ setContent()
+
+ composeTestRule.onRoot().assertIsNotDisplayed()
+ }
+
+ @Test
+ fun whenAppNotInstalled_noSummary() {
+ val notInstalledApp = ApplicationInfo()
+
+ setContent(notInstalledApp)
+
+ composeTestRule.onNode(hasTextExactly(context.getString(R.string.app_battery_usage_title)))
+ .assertIsDisplayed()
+ .assertIsNotEnabled()
+ }
+
+ @Test
+ fun batteryDiffEntryIsNull() {
+ mockBatteryDiffEntry(null)
+
+ setContent()
+
+ composeTestRule.onNode(
+ hasTextExactly(
+ context.getString(R.string.app_battery_usage_title),
+ context.getString(R.string.no_battery_summary),
+ ),
+ ).assertIsDisplayed().assertIsEnabled()
+ }
+
+ @Test
+ fun noConsumePower() {
+ val batteryDiffEntry = mock(BatteryDiffEntry::class.java).apply {
+ mConsumePower = 0.0
+ }
+ mockBatteryDiffEntry(batteryDiffEntry)
+
+ setContent()
+
+ composeTestRule.onNodeWithText(context.getString(R.string.no_battery_summary))
+ .assertIsDisplayed()
+ }
+
+ @Test
+ fun hasConsumePower() {
+ val batteryDiffEntry = mock(BatteryDiffEntry::class.java).apply {
+ mConsumePower = 12.3
+ }
+ whenever(batteryDiffEntry.percentOfTotal).thenReturn(45.6)
+ mockBatteryDiffEntry(batteryDiffEntry)
+
+ setContent()
+
+ composeTestRule.onNodeWithText("46% use since last full charge").assertIsDisplayed()
+ }
+
+ @Test
+ fun whenClick_openDetailsPage() {
+ val batteryDiffEntry = mock(BatteryDiffEntry::class.java)
+ whenever(batteryDiffEntry.percentOfTotal).thenReturn(10.0)
+ mockBatteryDiffEntry(batteryDiffEntry)
+
+ setContent()
+ composeTestRule.onRoot().performClick()
+
+ ExtendedMockito.verify {
+ AdvancedPowerUsageDetail.startBatteryDetailPage(
+ context,
+ SettingsEnums.APPLICATIONS_INSTALLED_APP_DETAILS,
+ batteryDiffEntry,
+ "10%",
+ null,
+ )
+ }
+ }
+
+ private fun setContent(app: ApplicationInfo = APP) {
+ composeTestRule.setContent {
+ CompositionLocalProvider(LocalContext provides context) {
+ AppBatteryPreference(app)
+ }
+ }
+ }
+
+ private companion object {
+ const val PACKAGE_NAME = "packageName"
+ const val UID = 123
+ val APP = ApplicationInfo().apply {
+ packageName = PACKAGE_NAME
+ uid = UID
+ flags = ApplicationInfo.FLAG_INSTALLED
+ }
+ }
+}