diff --git a/core/tests/powertests/PowerStatsViewer/Android.bp b/core/tests/powertests/PowerStatsViewer/Android.bp
new file mode 100644
index 0000000000000..a3dc4fb4ff74b
--- /dev/null
+++ b/core/tests/powertests/PowerStatsViewer/Android.bp
@@ -0,0 +1,13 @@
+android_test {
+ name: "PowerStatsViewer",
+ srcs: ["src/**/*.java"],
+ defaults: ["SettingsLibDefaults"],
+ static_libs: [
+ "androidx.appcompat_appcompat",
+ "androidx.cardview_cardview",
+ "androidx.recyclerview_recyclerview",
+ "com.google.android.material_material",
+ ],
+ platform_apis: true,
+ certificate: "platform",
+}
diff --git a/core/tests/powertests/PowerStatsViewer/AndroidManifest.xml b/core/tests/powertests/PowerStatsViewer/AndroidManifest.xml
new file mode 100644
index 0000000000000..378d0350e347a
--- /dev/null
+++ b/core/tests/powertests/PowerStatsViewer/AndroidManifest.xml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/core/tests/powertests/PowerStatsViewer/res/layout/app_info_layout.xml b/core/tests/powertests/PowerStatsViewer/res/layout/app_info_layout.xml
new file mode 100644
index 0000000000000..fe6fe2d411558
--- /dev/null
+++ b/core/tests/powertests/PowerStatsViewer/res/layout/app_info_layout.xml
@@ -0,0 +1,85 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/core/tests/powertests/PowerStatsViewer/res/layout/app_picker_layout.xml b/core/tests/powertests/PowerStatsViewer/res/layout/app_picker_layout.xml
new file mode 100644
index 0000000000000..6f289996f9829
--- /dev/null
+++ b/core/tests/powertests/PowerStatsViewer/res/layout/app_picker_layout.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/tests/powertests/PowerStatsViewer/res/layout/power_stats_entry_layout.xml b/core/tests/powertests/PowerStatsViewer/res/layout/power_stats_entry_layout.xml
new file mode 100644
index 0000000000000..1ced825adf315
--- /dev/null
+++ b/core/tests/powertests/PowerStatsViewer/res/layout/power_stats_entry_layout.xml
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/core/tests/powertests/PowerStatsViewer/res/layout/power_stats_viewer_layout.xml b/core/tests/powertests/PowerStatsViewer/res/layout/power_stats_viewer_layout.xml
new file mode 100644
index 0000000000000..99494188f422d
--- /dev/null
+++ b/core/tests/powertests/PowerStatsViewer/res/layout/power_stats_viewer_layout.xml
@@ -0,0 +1,77 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/core/tests/powertests/PowerStatsViewer/res/values/styles.xml b/core/tests/powertests/PowerStatsViewer/res/values/styles.xml
new file mode 100644
index 0000000000000..629d729e7b9a1
--- /dev/null
+++ b/core/tests/powertests/PowerStatsViewer/res/values/styles.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/tests/powertests/PowerStatsViewer/src/com/android/frameworks/core/powerstatsviewer/AppInfoHelper.java b/core/tests/powertests/PowerStatsViewer/src/com/android/frameworks/core/powerstatsviewer/AppInfoHelper.java
new file mode 100644
index 0000000000000..85265613928fe
--- /dev/null
+++ b/core/tests/powertests/PowerStatsViewer/src/com/android/frameworks/core/powerstatsviewer/AppInfoHelper.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2020 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.frameworks.core.powerstatsviewer;
+
+import android.annotation.Nullable;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.os.Process;
+
+import com.android.internal.os.BatterySipper;
+
+class AppInfoHelper {
+
+ private static final String SYSTEM_SERVER_PACKAGE_NAME = "android";
+
+ public static class AppInfo {
+ public int uid;
+ public CharSequence label;
+ public double powerMah;
+ public ApplicationInfo iconInfo;
+ public CharSequence packages;
+ }
+
+ @Nullable
+ public static AppInfo makeApplicationInfo(PackageManager packageManager, int uid,
+ @Nullable BatterySipper sipper) {
+ if (sipper != null && sipper.drainType != BatterySipper.DrainType.APP) {
+ return null;
+ }
+
+ String packageWithHighestDrain = null;
+
+ AppInfo info = new AppInfo();
+ info.uid = uid;
+ if (sipper != null) {
+ sipper.sumPower();
+ info.powerMah = sipper.totalSmearedPowerMah;
+ packageWithHighestDrain = sipper.packageWithHighestDrain;
+ }
+ if (info.uid == Process.ROOT_UID) {
+ info.label = "";
+ } else {
+ String[] packages = packageManager.getPackagesForUid(info.uid);
+ String primaryPackageName = null;
+ if (info.uid == Process.SYSTEM_UID) {
+ primaryPackageName = SYSTEM_SERVER_PACKAGE_NAME;
+ } else if (packages != null) {
+ for (String name : packages) {
+ primaryPackageName = name;
+ if (name.equals(packageWithHighestDrain)) {
+ break;
+ }
+ }
+ }
+
+ if (primaryPackageName != null) {
+ try {
+ ApplicationInfo applicationInfo =
+ packageManager.getApplicationInfo(primaryPackageName, 0);
+ info.label = applicationInfo.loadLabel(packageManager);
+ info.iconInfo = applicationInfo;
+ } catch (PackageManager.NameNotFoundException e) {
+ info.label = primaryPackageName;
+ }
+ } else if (packageWithHighestDrain != null) {
+ info.label = packageWithHighestDrain;
+ }
+
+ if (packages != null && packages.length > 0) {
+ StringBuilder sb = new StringBuilder();
+ if (primaryPackageName != null) {
+ sb.append(primaryPackageName);
+ }
+ for (String packageName : packages) {
+ if (packageName.equals(primaryPackageName)) {
+ continue;
+ }
+
+ if (sb.length() != 0) {
+ sb.append(", ");
+ }
+ sb.append(packageName);
+ }
+
+ info.packages = sb;
+ }
+ }
+
+ // Default the app icon to System Server. This includes root, dex2oat and other UIDs.
+ if (info.iconInfo == null) {
+ try {
+ info.iconInfo =
+ packageManager.getApplicationInfo(SYSTEM_SERVER_PACKAGE_NAME, 0);
+ } catch (PackageManager.NameNotFoundException nameNotFoundException) {
+ // Won't happen
+ }
+ }
+ return info;
+ }
+}
diff --git a/core/tests/powertests/PowerStatsViewer/src/com/android/frameworks/core/powerstatsviewer/AppPickerActivity.java b/core/tests/powertests/PowerStatsViewer/src/com/android/frameworks/core/powerstatsviewer/AppPickerActivity.java
new file mode 100644
index 0000000000000..b4fc73c0f7448
--- /dev/null
+++ b/core/tests/powertests/PowerStatsViewer/src/com/android/frameworks/core/powerstatsviewer/AppPickerActivity.java
@@ -0,0 +1,251 @@
+/*
+ * Copyright (C) 2008 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.frameworks.core.powerstatsviewer;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.BatteryStats;
+import android.os.Bundle;
+import android.os.Process;
+import android.os.UserManager;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.activity.ComponentActivity;
+import androidx.activity.result.contract.ActivityResultContract;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.loader.app.LoaderManager;
+import androidx.loader.content.Loader;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.frameworks.core.powerstatsviewer.AppInfoHelper.AppInfo;
+import com.android.internal.os.BatterySipper;
+import com.android.internal.os.BatteryStatsHelper;
+import com.android.settingslib.utils.AsyncLoaderCompat;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Picker, showing a sorted list of applications consuming power. Returns the selected
+ * application UID or Process.INVALID_UID.
+ */
+public class AppPickerActivity extends ComponentActivity {
+ private static final String TAG = "AppPicker";
+
+ public static final ActivityResultContract CONTRACT =
+ new ActivityResultContract() {
+ @NonNull
+ @Override
+ public Intent createIntent(@NonNull Context context, Void aVoid) {
+ return new Intent(context, AppPickerActivity.class);
+ }
+
+ @Override
+ public Integer parseResult(int resultCode, @Nullable Intent intent) {
+ if (resultCode != RESULT_OK || intent == null) {
+ return Process.INVALID_UID;
+ }
+ return intent.getIntExtra(Intent.EXTRA_UID, Process.INVALID_UID);
+ }
+ };
+
+ private AppListAdapter mAppListAdapter;
+ private RecyclerView mAppList;
+ private View mLoadingView;
+
+ private interface OnAppSelectedListener {
+ void onAppSelected(int uid);
+ }
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+ getActionBar().setDisplayHomeAsUpEnabled(true);
+
+ setContentView(R.layout.app_picker_layout);
+
+ mLoadingView = findViewById(R.id.loading_view);
+
+ mAppList = findViewById(R.id.app_list_view);
+ mAppList.setLayoutManager(new LinearLayoutManager(this));
+ mAppListAdapter = new AppListAdapter(AppPickerActivity.this::setSelectedUid);
+ mAppList.setAdapter(mAppListAdapter);
+
+ LoaderManager.getInstance(this).initLoader(0, null,
+ new AppListLoaderCallbacks());
+ }
+
+ protected void setSelectedUid(int uid) {
+ Intent intent = new Intent();
+ intent.putExtra(Intent.EXTRA_UID, uid);
+ setResult(RESULT_OK, intent);
+ finish();
+ }
+
+ @Override
+ public boolean onNavigateUp() {
+ onBackPressed();
+ return true;
+ }
+
+ private static class AppListLoader extends AsyncLoaderCompat> {
+ private final BatteryStatsHelper mStatsHelper;
+ private final UserManager mUserManager;
+ private final PackageManager mPackageManager;
+
+ AppListLoader(Context context) {
+ super(context);
+ mUserManager = context.getSystemService(UserManager.class);
+ mStatsHelper = new BatteryStatsHelper(context, false /* collectBatteryBroadcast */);
+ mStatsHelper.create((Bundle) null);
+ mStatsHelper.clearStats();
+ mPackageManager = context.getPackageManager();
+ }
+
+ @Override
+ public List loadInBackground() {
+ List applicationList = new ArrayList<>();
+
+ mStatsHelper.refreshStats(BatteryStats.STATS_SINCE_CHARGED,
+ mUserManager.getUserProfiles());
+
+ final List usageList = mStatsHelper.getUsageList();
+ for (BatterySipper sipper : usageList) {
+ AppInfo info =
+ AppInfoHelper.makeApplicationInfo(mPackageManager, sipper.getUid(), sipper);
+ if (info != null) {
+ applicationList.add(info);
+ }
+ }
+
+ applicationList.sort(
+ Comparator.comparing((AppInfo a) -> a.powerMah).reversed());
+ return applicationList;
+ }
+
+ @Override
+ protected void onDiscardResult(List result) {
+ }
+ }
+
+ private class AppListLoaderCallbacks implements
+ LoaderManager.LoaderCallbacks> {
+
+ @NonNull
+ @Override
+ public Loader> onCreateLoader(int id, Bundle args) {
+ return new AppListLoader(AppPickerActivity.this);
+ }
+
+ @Override
+ public void onLoadFinished(@NonNull Loader> loader,
+ List applicationList) {
+ mAppListAdapter.setApplicationList(applicationList);
+ mAppList.setVisibility(View.VISIBLE);
+ mLoadingView.setVisibility(View.GONE);
+ }
+
+ @Override
+ public void onLoaderReset(@NonNull Loader> loader) {
+ }
+ }
+
+ public class AppListAdapter extends RecyclerView.Adapter {
+ private final OnAppSelectedListener mListener;
+ private List mApplicationList;
+
+ public AppListAdapter(OnAppSelectedListener listener) {
+ mListener = listener;
+ }
+
+ void setApplicationList(List applicationList) {
+ mApplicationList = applicationList;
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public int getItemCount() {
+ return mApplicationList.size();
+ }
+
+ @NonNull
+ @Override
+ public AppViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int position) {
+ LayoutInflater layoutInflater = LayoutInflater.from(viewGroup.getContext());
+ View view = layoutInflater.inflate(R.layout.app_info_layout, viewGroup, false);
+ return new AppViewHolder(view, mListener);
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull AppViewHolder appViewHolder, int position) {
+ AppInfo item = mApplicationList.get(position);
+ appViewHolder.uid = item.uid;
+ appViewHolder.titleView.setText(item.label);
+ appViewHolder.uidView.setText(
+ String.format(Locale.getDefault(), "UID: %d", item.uid));
+ appViewHolder.powerView.setText(
+ String.format(Locale.getDefault(), "%.1f mAh", item.powerMah));
+ appViewHolder.iconView.setImageDrawable(
+ item.iconInfo.loadIcon(getPackageManager()));
+ if (item.packages != null) {
+ appViewHolder.packagesView.setText(item.packages);
+ appViewHolder.packagesView.setVisibility(View.VISIBLE);
+ } else {
+ appViewHolder.packagesView.setVisibility(View.GONE);
+ }
+ }
+ }
+
+ // View Holder used when displaying apps
+ public static class AppViewHolder extends RecyclerView.ViewHolder
+ implements View.OnClickListener {
+ private final OnAppSelectedListener mListener;
+
+ public int uid;
+ public TextView titleView;
+ public TextView uidView;
+ public ImageView iconView;
+ public TextView packagesView;
+ public TextView powerView;
+
+ AppViewHolder(View view, OnAppSelectedListener listener) {
+ super(view);
+ mListener = listener;
+ view.setOnClickListener(this);
+ titleView = view.findViewById(android.R.id.title);
+ uidView = view.findViewById(R.id.uid);
+ iconView = view.findViewById(android.R.id.icon);
+ packagesView = view.findViewById(R.id.packages);
+ powerView = view.findViewById(R.id.power_mah);
+ powerView.setVisibility(View.VISIBLE);
+ }
+
+ @Override
+ public void onClick(View v) {
+ mListener.onAppSelected(uid);
+ }
+ }
+}
diff --git a/core/tests/powertests/PowerStatsViewer/src/com/android/frameworks/core/powerstatsviewer/PowerStatsData.java b/core/tests/powertests/PowerStatsViewer/src/com/android/frameworks/core/powerstatsviewer/PowerStatsData.java
new file mode 100644
index 0000000000000..09f20baf499c8
--- /dev/null
+++ b/core/tests/powertests/PowerStatsViewer/src/com/android/frameworks/core/powerstatsviewer/PowerStatsData.java
@@ -0,0 +1,240 @@
+/*
+ * Copyright (C) 2020 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.frameworks.core.powerstatsviewer;
+
+import android.content.Context;
+import android.os.Process;
+
+import com.android.internal.os.BatterySipper;
+import com.android.internal.os.BatteryStatsHelper;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class PowerStatsData {
+ private static final String PACKAGE_CALENDAR_PROVIDER = "com.android.providers.calendar";
+ private static final String PACKAGE_MEDIA_PROVIDER = "com.android.providers.media";
+ private static final String PACKAGE_SYSTEMUI = "com.android.systemui";
+ private static final String[] PACKAGES_SYSTEM = {PACKAGE_MEDIA_PROVIDER,
+ PACKAGE_CALENDAR_PROVIDER, PACKAGE_SYSTEMUI};
+
+ enum EntryType {
+ POWER,
+ DURATION,
+ }
+
+ public static class Entry {
+ public String title;
+ public EntryType entryType;
+ public double value;
+ public double total;
+ }
+
+ private final AppInfoHelper.AppInfo mAppInfo;
+ private final List mEntries = new ArrayList<>();
+
+ public PowerStatsData(Context context, BatteryStatsHelper batteryStatsHelper,
+ int uid) {
+ List usageList = batteryStatsHelper.getUsageList();
+
+ double totalPowerMah = 0;
+ double totalSmearedPowerMah = 0;
+ double totalPowerExcludeSystemMah = 0;
+ double totalScreenPower = 0;
+ double totalProportionalSmearMah = 0;
+ double totalCpuPowerMah = 0;
+ double totalSystemServiceCpuPowerMah = 0;
+ double totalUsagePowerMah = 0;
+ double totalWakeLockPowerMah = 0;
+ double totalMobileRadioPowerMah = 0;
+ double totalWifiPowerMah = 0;
+ double totalBluetoothPowerMah = 0;
+ double totalGpsPowerMah = 0;
+ double totalCameraPowerMah = 0;
+ double totalFlashlightPowerMah = 0;
+ double totalSensorPowerMah = 0;
+ double totalAudioPowerMah = 0;
+ double totalVideoPowerMah = 0;
+
+ long totalCpuTimeMs = 0;
+ long totalCpuFgTimeMs = 0;
+ long totalWakeLockTimeMs = 0;
+ long totalWifiRunningTimeMs = 0;
+ long totalBluetoothRunningTimeMs = 0;
+ long totalGpsTimeMs = 0;
+ long totalCameraTimeMs = 0;
+ long totalFlashlightTimeMs = 0;
+ long totalAudioTimeMs = 0;
+ long totalVideoTimeMs = 0;
+
+ BatterySipper uidSipper = null;
+ for (BatterySipper sipper : usageList) {
+ if (sipper.drainType == BatterySipper.DrainType.SCREEN) {
+ totalScreenPower = sipper.sumPower();
+ }
+
+ if (isHiddenDrainType(sipper.drainType)) {
+ continue;
+ }
+
+ if (sipper.drainType == BatterySipper.DrainType.APP && sipper.getUid() == uid) {
+ uidSipper = sipper;
+ }
+
+ totalPowerMah += sipper.sumPower();
+ totalSmearedPowerMah += sipper.totalSmearedPowerMah;
+ totalProportionalSmearMah += sipper.proportionalSmearMah;
+
+ if (!isSystemSipper(sipper)) {
+ totalPowerExcludeSystemMah += sipper.totalSmearedPowerMah;
+ }
+
+ totalCpuPowerMah += sipper.cpuPowerMah;
+ totalSystemServiceCpuPowerMah += sipper.systemServiceCpuPowerMah;
+ totalUsagePowerMah += sipper.usagePowerMah;
+ totalWakeLockPowerMah += sipper.wakeLockPowerMah;
+ totalMobileRadioPowerMah += sipper.mobileRadioPowerMah;
+ totalWifiPowerMah += sipper.wifiPowerMah;
+ totalBluetoothPowerMah += sipper.bluetoothPowerMah;
+ totalGpsPowerMah += sipper.gpsPowerMah;
+ totalCameraPowerMah += sipper.cameraPowerMah;
+ totalFlashlightPowerMah += sipper.flashlightPowerMah;
+ totalSensorPowerMah += sipper.sensorPowerMah;
+ totalAudioPowerMah += sipper.audioPowerMah;
+ totalVideoPowerMah += sipper.videoPowerMah;
+
+ totalCpuTimeMs += sipper.cpuTimeMs;
+ totalCpuFgTimeMs += sipper.cpuFgTimeMs;
+ totalWakeLockTimeMs += sipper.wakeLockTimeMs;
+ totalWifiRunningTimeMs += sipper.wifiRunningTimeMs;
+ totalBluetoothRunningTimeMs += sipper.bluetoothRunningTimeMs;
+ totalGpsTimeMs += sipper.gpsTimeMs;
+ totalCameraTimeMs += sipper.cameraTimeMs;
+ totalFlashlightTimeMs += sipper.flashlightTimeMs;
+ totalAudioTimeMs += sipper.audioTimeMs;
+ totalVideoTimeMs += sipper.videoTimeMs;
+ }
+
+ mAppInfo = AppInfoHelper.makeApplicationInfo(context.getPackageManager(), uid, uidSipper);
+
+ if (uidSipper == null) {
+ return;
+ }
+
+ addEntry("Total power", EntryType.POWER,
+ uidSipper.totalSmearedPowerMah, totalSmearedPowerMah);
+ addEntry("... excluding system", EntryType.POWER,
+ uidSipper.totalSmearedPowerMah, totalPowerExcludeSystemMah);
+ addEntry("Screen, smeared", EntryType.POWER,
+ uidSipper.screenPowerMah, totalScreenPower);
+ addEntry("Other, smeared", EntryType.POWER,
+ uidSipper.proportionalSmearMah, totalProportionalSmearMah);
+ addEntry("Excluding smeared", EntryType.POWER,
+ uidSipper.totalPowerMah, totalPowerMah);
+ addEntry("CPU", EntryType.POWER,
+ uidSipper.cpuPowerMah, totalCpuPowerMah);
+ addEntry("System services", EntryType.POWER,
+ uidSipper.systemServiceCpuPowerMah, totalSystemServiceCpuPowerMah);
+ addEntry("RAM", EntryType.POWER,
+ uidSipper.usagePowerMah, totalUsagePowerMah);
+ addEntry("Wake lock", EntryType.POWER,
+ uidSipper.wakeLockPowerMah, totalWakeLockPowerMah);
+ addEntry("Mobile radio", EntryType.POWER,
+ uidSipper.mobileRadioPowerMah, totalMobileRadioPowerMah);
+ addEntry("WiFi", EntryType.POWER,
+ uidSipper.wifiPowerMah, totalWifiPowerMah);
+ addEntry("Bluetooth", EntryType.POWER,
+ uidSipper.bluetoothPowerMah, totalBluetoothPowerMah);
+ addEntry("GPS", EntryType.POWER,
+ uidSipper.gpsPowerMah, totalGpsPowerMah);
+ addEntry("Camera", EntryType.POWER,
+ uidSipper.cameraPowerMah, totalCameraPowerMah);
+ addEntry("Flashlight", EntryType.POWER,
+ uidSipper.flashlightPowerMah, totalFlashlightPowerMah);
+ addEntry("Sensors", EntryType.POWER,
+ uidSipper.sensorPowerMah, totalSensorPowerMah);
+ addEntry("Audio", EntryType.POWER,
+ uidSipper.audioPowerMah, totalAudioPowerMah);
+ addEntry("Video", EntryType.POWER,
+ uidSipper.videoPowerMah, totalVideoPowerMah);
+
+ addEntry("CPU time", EntryType.DURATION,
+ uidSipper.cpuTimeMs, totalCpuTimeMs);
+ addEntry("CPU foreground time", EntryType.DURATION,
+ uidSipper.cpuFgTimeMs, totalCpuFgTimeMs);
+ addEntry("Wake lock time", EntryType.DURATION,
+ uidSipper.wakeLockTimeMs, totalWakeLockTimeMs);
+ addEntry("WiFi running time", EntryType.DURATION,
+ uidSipper.wifiRunningTimeMs, totalWifiRunningTimeMs);
+ addEntry("Bluetooth time", EntryType.DURATION,
+ uidSipper.bluetoothRunningTimeMs, totalBluetoothRunningTimeMs);
+ addEntry("GPS time", EntryType.DURATION,
+ uidSipper.gpsTimeMs, totalGpsTimeMs);
+ addEntry("Camera time", EntryType.DURATION,
+ uidSipper.cameraTimeMs, totalCameraTimeMs);
+ addEntry("Flashlight time", EntryType.DURATION,
+ uidSipper.flashlightTimeMs, totalFlashlightTimeMs);
+ addEntry("Audio time", EntryType.DURATION,
+ uidSipper.audioTimeMs, totalAudioTimeMs);
+ addEntry("Video time", EntryType.DURATION,
+ uidSipper.videoTimeMs, totalVideoTimeMs);
+ }
+
+ protected boolean isHiddenDrainType(BatterySipper.DrainType drainType) {
+ return drainType == BatterySipper.DrainType.IDLE
+ || drainType == BatterySipper.DrainType.CELL
+ || drainType == BatterySipper.DrainType.SCREEN
+ || drainType == BatterySipper.DrainType.UNACCOUNTED
+ || drainType == BatterySipper.DrainType.OVERCOUNTED
+ || drainType == BatterySipper.DrainType.BLUETOOTH
+ || drainType == BatterySipper.DrainType.WIFI;
+ }
+
+ private boolean isSystemSipper(BatterySipper sipper) {
+ final int uid = sipper.uidObj == null ? -1 : sipper.getUid();
+ if (uid >= Process.ROOT_UID && uid < Process.FIRST_APPLICATION_UID) {
+ return true;
+ } else if (sipper.mPackages != null) {
+ for (final String packageName : sipper.mPackages) {
+ for (final String systemPackage : PACKAGES_SYSTEM) {
+ if (systemPackage.equals(packageName)) {
+ return true;
+ }
+ }
+ }
+ }
+
+ return false;
+ }
+
+ private void addEntry(String title, EntryType entryType, double amount, double totalAmount) {
+ Entry entry = new Entry();
+ entry.title = title;
+ entry.entryType = entryType;
+ entry.value = amount;
+ entry.total = totalAmount;
+ mEntries.add(entry);
+ }
+
+ public AppInfoHelper.AppInfo getAppInfo() {
+ return mAppInfo;
+ }
+
+ public List getEntries() {
+ return mEntries;
+ }
+}
diff --git a/core/tests/powertests/PowerStatsViewer/src/com/android/frameworks/core/powerstatsviewer/PowerStatsViewerActivity.java b/core/tests/powertests/PowerStatsViewer/src/com/android/frameworks/core/powerstatsviewer/PowerStatsViewerActivity.java
new file mode 100644
index 0000000000000..1605e9cf1b76d
--- /dev/null
+++ b/core/tests/powertests/PowerStatsViewer/src/com/android/frameworks/core/powerstatsviewer/PowerStatsViewerActivity.java
@@ -0,0 +1,263 @@
+/*
+ * Copyright (C) 2020 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.frameworks.core.powerstatsviewer;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.BatteryStats;
+import android.os.Bundle;
+import android.os.Process;
+import android.os.UserManager;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.activity.ComponentActivity;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.loader.app.LoaderManager;
+import androidx.loader.app.LoaderManager.LoaderCallbacks;
+import androidx.loader.content.Loader;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.internal.os.BatteryStatsHelper;
+import com.android.settingslib.utils.AsyncLoaderCompat;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+
+public class PowerStatsViewerActivity extends ComponentActivity {
+ private static final int POWER_STATS_REFRESH_RATE_MILLIS = 60 * 1000;
+ public static final String PREF_SELECTED_UID = "selectedUid";
+ private static final String LOADER_ARG_UID = "uid";
+
+ private PowerStatsDataAdapter mPowerStatsDataAdapter;
+ private Runnable mPowerStatsRefresh = this::periodicPowerStatsRefresh;
+ private SharedPreferences mSharedPref;
+ private int mUid = Process.INVALID_UID;
+ private TextView mTitleView;
+ private TextView mUidView;
+ private ImageView mIconView;
+ private TextView mPackagesView;
+ private RecyclerView mPowerStatsDataView;
+ private View mLoadingView;
+ private View mEmptyView;
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ mSharedPref = getPreferences(Context.MODE_PRIVATE);
+
+ setContentView(R.layout.power_stats_viewer_layout);
+
+ View appCard = findViewById(R.id.app_card);
+ appCard.setOnClickListener((e) -> startAppPicker());
+
+ mTitleView = findViewById(android.R.id.title);
+ mUidView = findViewById(R.id.uid);
+ mIconView = findViewById(android.R.id.icon);
+ mPackagesView = findViewById(R.id.packages);
+
+ mPowerStatsDataView = findViewById(R.id.power_stats_data_view);
+ mPowerStatsDataView.setLayoutManager(new LinearLayoutManager(this));
+ mPowerStatsDataAdapter = new PowerStatsDataAdapter();
+ mPowerStatsDataView.setAdapter(mPowerStatsDataAdapter);
+
+ mLoadingView = findViewById(R.id.loading_view);
+ mEmptyView = findViewById(R.id.empty_view);
+
+ mUid = mSharedPref.getInt(PREF_SELECTED_UID, Process.INVALID_UID);
+ loadPowerStats();
+ if (mUid == Process.INVALID_UID) {
+ startAppPicker();
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ periodicPowerStatsRefresh();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ getMainThreadHandler().removeCallbacks(mPowerStatsRefresh);
+ }
+
+ private void startAppPicker() {
+ registerForActivityResult(AppPickerActivity.CONTRACT, this::onApplicationSelected)
+ .launch(null);
+ }
+
+ private void onApplicationSelected(int uid) {
+ if (uid == -1) {
+ if (mUid == Process.INVALID_UID) {
+ finish();
+ }
+ } else {
+ mUid = uid;
+ mSharedPref.edit().putInt(PREF_SELECTED_UID, mUid).apply();
+ mLoadingView.setVisibility(View.VISIBLE);
+ loadPowerStats();
+ }
+ }
+
+ private void periodicPowerStatsRefresh() {
+ loadPowerStats();
+ getMainThreadHandler().postDelayed(mPowerStatsRefresh, POWER_STATS_REFRESH_RATE_MILLIS);
+ }
+
+ private void loadPowerStats() {
+ Bundle args = new Bundle();
+ args.putInt(LOADER_ARG_UID, mUid);
+ LoaderManager.getInstance(this).restartLoader(0, args, new PowerStatsDataLoaderCallbacks());
+ }
+
+ private static class PowerStatsDataLoader extends AsyncLoaderCompat {
+ private final int mUid;
+ private final BatteryStatsHelper mBatteryStatsHelper;
+ private final UserManager mUserManager;
+
+ PowerStatsDataLoader(Context context, int uid) {
+ super(context);
+ mUid = uid;
+ mUserManager = context.getSystemService(UserManager.class);
+ mBatteryStatsHelper = new BatteryStatsHelper(context,
+ false /* collectBatteryBroadcast */);
+ mBatteryStatsHelper.create((Bundle) null);
+ mBatteryStatsHelper.clearStats();
+ }
+
+ @Override
+ public PowerStatsData loadInBackground() {
+ mBatteryStatsHelper.refreshStats(BatteryStats.STATS_SINCE_CHARGED,
+ mUserManager.getUserProfiles());
+ return new PowerStatsData(getContext(), mBatteryStatsHelper, mUid);
+ }
+
+ @Override
+ protected void onDiscardResult(PowerStatsData result) {
+ }
+ }
+
+ private class PowerStatsDataLoaderCallbacks implements LoaderCallbacks {
+ @NonNull
+ @Override
+ public Loader onCreateLoader(int id, Bundle args) {
+ return new PowerStatsDataLoader(PowerStatsViewerActivity.this,
+ args.getInt(LOADER_ARG_UID, Process.INVALID_UID));
+ }
+
+ @Override
+ public void onLoadFinished(@NonNull Loader loader,
+ PowerStatsData powerStatsData) {
+
+ AppInfoHelper.AppInfo appInfo = powerStatsData.getAppInfo();
+ mTitleView.setText(appInfo.label);
+ mUidView.setText(String.format(Locale.getDefault(), "UID: %d", appInfo.uid));
+ mIconView.setImageDrawable(appInfo.iconInfo.loadIcon(getPackageManager()));
+
+ if (appInfo.packages != null) {
+ mPackagesView.setText(appInfo.packages);
+ mPackagesView.setVisibility(View.VISIBLE);
+ } else {
+ mPackagesView.setVisibility(View.GONE);
+ }
+
+ mPowerStatsDataAdapter.setEntries(powerStatsData.getEntries());
+
+ if (powerStatsData.getEntries().isEmpty()) {
+ mEmptyView.setVisibility(View.VISIBLE);
+ mPowerStatsDataView.setVisibility(View.GONE);
+ } else {
+ mEmptyView.setVisibility(View.GONE);
+ mPowerStatsDataView.setVisibility(View.VISIBLE);
+ }
+
+ mLoadingView.setVisibility(View.GONE);
+ }
+
+ @Override
+ public void onLoaderReset(@NonNull Loader loader) {
+ }
+ }
+
+ private static class PowerStatsDataAdapter extends
+ RecyclerView.Adapter {
+ public static class ViewHolder extends RecyclerView.ViewHolder {
+ public TextView titleTextView;
+ public TextView amountTextView;
+ public TextView percentTextView;
+
+ ViewHolder(View itemView) {
+ super(itemView);
+
+ titleTextView = itemView.findViewById(R.id.title);
+ amountTextView = itemView.findViewById(R.id.amount);
+ percentTextView = itemView.findViewById(R.id.percent);
+ }
+ }
+
+ private List mEntries = Collections.emptyList();
+
+ public void setEntries(List entries) {
+ mEntries = entries;
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public int getItemCount() {
+ return mEntries.size();
+ }
+
+ @NonNull
+ @Override
+ public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int position) {
+ LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
+ View itemView = layoutInflater.inflate(R.layout.power_stats_entry_layout, parent,
+ false);
+ return new ViewHolder(itemView);
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull ViewHolder viewHolder, int position) {
+ PowerStatsData.Entry entry = mEntries.get(position);
+ switch (entry.entryType) {
+ case POWER:
+ viewHolder.titleTextView.setText(entry.title);
+ viewHolder.amountTextView.setText(
+ String.format(Locale.getDefault(), "%.1f mAh", entry.value));
+ break;
+ case DURATION:
+ viewHolder.titleTextView.setText(entry.title);
+ viewHolder.amountTextView.setText(
+ String.format(Locale.getDefault(), "%,d ms", (long) entry.value));
+ break;
+ }
+
+ double proportion = entry.total != 0 ? entry.value * 100 / entry.total : 0;
+ viewHolder.percentTextView.setText(String.format(Locale.getDefault(), "%.1f%%",
+ proportion));
+ }
+ }
+}