Merge "Add power stats viewer"

This commit is contained in:
Dmitri Plotnikov
2020-10-13 16:55:45 +00:00
committed by Android (Google) Code Review
11 changed files with 1203 additions and 0 deletions

View File

@@ -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",
}

View File

@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.frameworks.core.powerstatsviewer"
android:sharedUserId="android.uid.system">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.BATTERY_STATS"/>
<application
android:theme="@style/Theme"
android:label="Power Stats Viewer">
<activity android:name=".PowerStatsViewerActivity"
android:label="Power Stats Viewer"
android:launchMode="singleTop"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity android:name=".AppPickerActivity"
android:label="Power Stats - Select an App"/>
</application>
</manifest>

View File

@@ -0,0 +1,85 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2018 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.
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="?android:attr/selectableItemBackground"
android:gravity="center_vertical"
android:minHeight="?android:attr/listPreferredItemHeightSmall"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd">
<ImageView
android:id="@android:id/icon"
android:layout_width="@dimen/secondary_app_icon_size"
android:layout_height="@dimen/secondary_app_icon_size"
android:layout_marginEnd="12dp"
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp"
android:gravity="start|center_vertical"/>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:paddingTop="16dp"
android:paddingBottom="16dp">
<TextView
android:id="@android:id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="marquee"
android:fadingEdge="horizontal"
android:singleLine="true"
android:textAppearance="?android:attr/textAppearanceListItem"/>
<TextView
android:id="@+id/uid"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="marquee"
android:fadingEdge="horizontal"
android:singleLine="true"
android:textAppearance="?android:attr/textAppearanceSmall"/>
<TextView
android:id="@+id/packages"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textDirection="locale"
android:maxLines="3"
android:ellipsize="end"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorSecondary"/>
</LinearLayout>
<TextView
android:id="@+id/power_mah"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:layout_gravity="end|center_vertical"
android:layout_marginStart="16dp"
android:gravity="right"
android:textAppearance="?android:attr/textAppearanceListItem"
android:visibility="gone"/>
</LinearLayout>

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
-->
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/app_list_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"/>
<ProgressBar
style="?android:attr/progressBarStyleLarge"
android:id="@+id/loading_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="true"/>
</FrameLayout>

View File

@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:minHeight="?android:attr/listPreferredItemHeightSmall"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
android:paddingTop="8dp"
android:paddingBottom="8dp">
<TextView
android:id="@+id/title"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearanceBody"/>
<TextView
android:id="@+id/amount"
android:layout_width="0dp"
android:layout_weight="0.7"
android:layout_height="wrap_content"
android:gravity="right"
android:textAppearance="@style/TextAppearanceBody"/>
<TextView
android:id="@+id/percent"
android:layout_width="64dp"
android:layout_height="wrap_content"
android:gravity="right"
android:textAppearance="@style/TextAppearanceBody"/>
</LinearLayout>

View File

@@ -0,0 +1,77 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
-->
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.cardview.widget.CardView
style="@style/LoadTestCardView"
android:id="@+id/app_card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginEnd="10dp"
android:layout_marginBottom="10dp"
android:layout_marginStart="10dp"
android:padding="20dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:minHeight="80dp"
android:paddingStart="10dp"
android:paddingEnd="10dp">
<include layout="@layout/app_info_layout"/>
</LinearLayout>
</androidx.cardview.widget.CardView>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/power_stats_data_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
<TextView
android:id="@+id/empty_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:visibility="gone"
android:text="No power stats available"/>
</LinearLayout>
<FrameLayout
android:id="@+id/loading_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#AAFFFFFF">
<ProgressBar
style="?android:attr/progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="true"/>
</FrameLayout>
</FrameLayout>

View File

@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
-->
<resources>
<style name="Theme" parent="Theme.MaterialComponents.Light">
<item name="colorPrimary">#34a853</item>
<item name="android:windowActionBar">true</item>
<item name="android:windowNoTitle">false</item>
</style>
<style name="LoadTestCardView" parent="Widget.MaterialComponents.CardView">
<item name="cardBackgroundColor">#ceead6</item>
</style>
<style name="TextAppearanceBody" parent="android:TextAppearance.DeviceDefault">
<item name="android:textColor">#000000</item>
<item name="android:textSize">18sp</item>
</style>
</resources>

View File

@@ -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 = "<root>";
} 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;
}
}

View File

@@ -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<Void, Integer> CONTRACT =
new ActivityResultContract<Void, Integer>() {
@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<List<AppInfo>> {
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<AppInfo> loadInBackground() {
List<AppInfo> applicationList = new ArrayList<>();
mStatsHelper.refreshStats(BatteryStats.STATS_SINCE_CHARGED,
mUserManager.getUserProfiles());
final List<BatterySipper> 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<AppInfo> result) {
}
}
private class AppListLoaderCallbacks implements
LoaderManager.LoaderCallbacks<List<AppInfo>> {
@NonNull
@Override
public Loader<List<AppInfo>> onCreateLoader(int id, Bundle args) {
return new AppListLoader(AppPickerActivity.this);
}
@Override
public void onLoadFinished(@NonNull Loader<List<AppInfo>> loader,
List<AppInfo> applicationList) {
mAppListAdapter.setApplicationList(applicationList);
mAppList.setVisibility(View.VISIBLE);
mLoadingView.setVisibility(View.GONE);
}
@Override
public void onLoaderReset(@NonNull Loader<List<AppInfo>> loader) {
}
}
public class AppListAdapter extends RecyclerView.Adapter<AppViewHolder> {
private final OnAppSelectedListener mListener;
private List<AppInfo> mApplicationList;
public AppListAdapter(OnAppSelectedListener listener) {
mListener = listener;
}
void setApplicationList(List<AppInfo> 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);
}
}
}

View File

@@ -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<Entry> mEntries = new ArrayList<>();
public PowerStatsData(Context context, BatteryStatsHelper batteryStatsHelper,
int uid) {
List<BatterySipper> 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<Entry> getEntries() {
return mEntries;
}
}

View File

@@ -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<PowerStatsData> {
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<PowerStatsData> {
@NonNull
@Override
public Loader<PowerStatsData> onCreateLoader(int id, Bundle args) {
return new PowerStatsDataLoader(PowerStatsViewerActivity.this,
args.getInt(LOADER_ARG_UID, Process.INVALID_UID));
}
@Override
public void onLoadFinished(@NonNull Loader<PowerStatsData> 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<PowerStatsData> loader) {
}
}
private static class PowerStatsDataAdapter extends
RecyclerView.Adapter<PowerStatsDataAdapter.ViewHolder> {
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<PowerStatsData.Entry> mEntries = Collections.emptyList();
public void setEntries(List<PowerStatsData.Entry> 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));
}
}
}