Merge "Add AppBatteryPreference for Spa"
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
+        }
+    }
+}