From f0aaf697145a61795ab15a5b8ec10971940cf8fd Mon Sep 17 00:00:00 2001 From: Dmitri Plotnikov Date: Wed, 22 Jul 2020 18:15:35 -0700 Subject: [PATCH] Add power stats viewer Bug: 170134969 Test: mp :PowerStatsViewer && adb shell am start -n com.android.frameworks.core.powerstatsviewer/.PowerStatsViewerActivity Change-Id: I6874831884121042c2fcfd1ae98a33502e81c967 --- .../powertests/PowerStatsViewer/Android.bp | 13 + .../PowerStatsViewer/AndroidManifest.xml | 42 +++ .../res/layout/app_info_layout.xml | 85 ++++++ .../res/layout/app_picker_layout.xml | 35 +++ .../res/layout/power_stats_entry_layout.xml | 49 ++++ .../res/layout/power_stats_viewer_layout.xml | 77 +++++ .../PowerStatsViewer/res/values/styles.xml | 34 +++ .../core/powerstatsviewer/AppInfoHelper.java | 114 ++++++++ .../powerstatsviewer/AppPickerActivity.java | 251 +++++++++++++++++ .../core/powerstatsviewer/PowerStatsData.java | 240 ++++++++++++++++ .../PowerStatsViewerActivity.java | 263 ++++++++++++++++++ 11 files changed, 1203 insertions(+) create mode 100644 core/tests/powertests/PowerStatsViewer/Android.bp create mode 100644 core/tests/powertests/PowerStatsViewer/AndroidManifest.xml create mode 100644 core/tests/powertests/PowerStatsViewer/res/layout/app_info_layout.xml create mode 100644 core/tests/powertests/PowerStatsViewer/res/layout/app_picker_layout.xml create mode 100644 core/tests/powertests/PowerStatsViewer/res/layout/power_stats_entry_layout.xml create mode 100644 core/tests/powertests/PowerStatsViewer/res/layout/power_stats_viewer_layout.xml create mode 100644 core/tests/powertests/PowerStatsViewer/res/values/styles.xml create mode 100644 core/tests/powertests/PowerStatsViewer/src/com/android/frameworks/core/powerstatsviewer/AppInfoHelper.java create mode 100644 core/tests/powertests/PowerStatsViewer/src/com/android/frameworks/core/powerstatsviewer/AppPickerActivity.java create mode 100644 core/tests/powertests/PowerStatsViewer/src/com/android/frameworks/core/powerstatsviewer/PowerStatsData.java create mode 100644 core/tests/powertests/PowerStatsViewer/src/com/android/frameworks/core/powerstatsviewer/PowerStatsViewerActivity.java 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)); + } + } +}