From 2a2daf960c43a2c97b1c6da430138e08e9caa54f Mon Sep 17 00:00:00 2001 From: Amith Yamasani Date: Fri, 22 May 2009 15:18:46 -0700 Subject: [PATCH] Top level screen for App Fuel Gauge. Shows sorted list of power usage (with some debug information) by the top 10 apps and subsystems. Doesn't yet take into account certain subsystems such as data network usage, audio/video DSP usage, Bluetooth and lights other than screen backlight. Screen, Idle, Wifi and Voice usage and CPU time are accounted for. Also need to add detail screens for each item and suggested actions. --- AndroidManifest.xml | 6 + res/drawable/app_gauge.9.png | Bin 0 -> 253 bytes res/layout/preference_powergauge.xml | 65 ++++ res/values/strings.xml | 21 ++ res/xml/application_settings.xml | 8 + res/xml/power_usage_summary.xml | 24 ++ .../fuelgauge/PowerGaugePreference.java | 112 ++++++ .../settings/fuelgauge/PowerUsageSummary.java | 352 ++++++++++++++++++ 8 files changed, 588 insertions(+) create mode 100644 res/drawable/app_gauge.9.png create mode 100644 res/layout/preference_powergauge.xml create mode 100644 res/xml/power_usage_summary.xml create mode 100644 src/com/android/settings/fuelgauge/PowerGaugePreference.java create mode 100644 src/com/android/settings/fuelgauge/PowerUsageSummary.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 88915522bfe..60d9616f6cf 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -505,6 +505,12 @@ + + + + + + diff --git a/res/drawable/app_gauge.9.png b/res/drawable/app_gauge.9.png new file mode 100644 index 0000000000000000000000000000000000000000..4d4db24b531adb995de791a1fa5cba4d5400c6f8 GIT binary patch literal 253 zcmVyPm~n1Pahtq}+lL$g}otLehco9?G2r6DWzpP4*_BDK438 z23+%`V30`b&wyti6~tE}|3x-fw&L+_xynqMzXpS%cm_2$gM5fUnXnpwa{a&SU}jLg zL + + + + + + + + + + + + + + + + + diff --git a/res/values/strings.xml b/res/values/strings.xml index 5b04e51f641..4879db227f1 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -1574,6 +1574,7 @@ found in the list of installed applications. Usage time + Accessibility @@ -1599,6 +1600,26 @@ found in the list of installed applications. selects to disable accessibility. This avoids accidental disabling. --> Disable accessibility? + + + + + Power usage summary + + Device awake time + + WiFi on time + + + Screen on + + WiFi + + Cell + + Voice + + Standby diff --git a/res/xml/application_settings.xml b/res/xml/application_settings.xml index 8d0a7cb5153..4da20365050 100644 --- a/res/xml/application_settings.xml +++ b/res/xml/application_settings.xml @@ -35,6 +35,14 @@ android:targetClass="com.android.settings.quicklaunch.QuickLaunchSettings" /> + + + + diff --git a/res/xml/power_usage_summary.xml b/res/xml/power_usage_summary.xml new file mode 100644 index 00000000000..0c35905bdbf --- /dev/null +++ b/res/xml/power_usage_summary.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/src/com/android/settings/fuelgauge/PowerGaugePreference.java b/src/com/android/settings/fuelgauge/PowerGaugePreference.java new file mode 100644 index 00000000000..5e89535805d --- /dev/null +++ b/src/com/android/settings/fuelgauge/PowerGaugePreference.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.fuelgauge; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.PixelFormat; +import android.graphics.drawable.Drawable; +import android.preference.Preference; +import android.view.View; +import android.widget.ImageView; + +import com.android.settings.R; + +/** + * Custom preference for displaying power consumption as a bar and an icon on the left for the + * subsystem/app type. + * + */ +public class PowerGaugePreference extends Preference { + + private Drawable mIcon; + private GaugeDrawable mGauge; + private double mValue; + + public PowerGaugePreference(Context context, Drawable icon) { + super(context); + setLayoutResource(R.layout.preference_powergauge); + mIcon = icon; + mGauge = new GaugeDrawable(); + mGauge.bar = context.getResources().getDrawable(R.drawable.app_gauge); + } + + /** + * Sets the width of the gauge in percentage (0 - 100) + * @param percent + */ + void setGaugeValue(double percent) { + mValue = percent; + mGauge.percent = mValue; + } + + @Override + protected void onBindView(View view) { + super.onBindView(view); + + ImageView appIcon = (ImageView) view.findViewById(R.id.appIcon); + if (mIcon == null) { + mIcon = getContext().getResources().getDrawable(android.R.drawable.sym_def_app_icon); + } + appIcon.setImageDrawable(mIcon); + + ImageView appGauge = (ImageView) view.findViewById(R.id.appGauge); + appGauge.setImageDrawable(mGauge); + } + + static class GaugeDrawable extends Drawable { + Drawable bar; + double percent; + int lastWidth = -1; + + @Override + public void draw(Canvas canvas) { + if (lastWidth == -1) { + lastWidth = getBarWidth(); + bar.setBounds(0, 0, lastWidth, bar.getIntrinsicHeight()); + } + bar.draw(canvas); + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } + + @Override + public void setAlpha(int alpha) { + // Ignore + } + + @Override + public void setColorFilter(ColorFilter cf) { + // Ignore + } + + private int getBarWidth() { + int width = (int) ((this.getBounds().width() * percent) / 100); + int intrinsicWidth = bar.getIntrinsicWidth(); + return Math.max(width, intrinsicWidth); + } + + @Override + public int getIntrinsicHeight() { + return bar.getIntrinsicHeight(); + } + } +} diff --git a/src/com/android/settings/fuelgauge/PowerUsageSummary.java b/src/com/android/settings/fuelgauge/PowerUsageSummary.java new file mode 100644 index 00000000000..3abd858fe4b --- /dev/null +++ b/src/com/android/settings/fuelgauge/PowerUsageSummary.java @@ -0,0 +1,352 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.fuelgauge; + +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.graphics.drawable.Drawable; +import android.os.BatteryStats; +import android.os.Bundle; +import android.os.Parcel; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.SystemClock; +import android.os.BatteryStats.Uid; +import android.preference.PreferenceActivity; +import android.preference.PreferenceGroup; +import android.util.Log; +import android.util.SparseArray; + +import com.android.internal.app.IBatteryStats; +import com.android.internal.os.BatteryStatsImpl; +import com.android.internal.os.PowerProfile; +import com.android.settings.R; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * Displays a list of apps and subsystems that consume power, ordered by how much power was + * consumed since the last time it was unplugged. + */ +public class PowerUsageSummary extends PreferenceActivity { + + private static final boolean DEBUG = true; + + private static final String TAG = "PowerUsageSummary"; + private static final String PREF_APP_LIST = "app_list"; + + IBatteryStats mBatteryInfo; + BatteryStatsImpl mStats; + private List mUsageList = new ArrayList(); + + private PreferenceGroup mAppListGroup; + + private int mStatsType = BatteryStats.STATS_UNPLUGGED; + + private static final int MIN_POWER_THRESHOLD = 5; + private static final int MAX_ITEMS_TO_LIST = 10; + + private double mMaxPower = 1; + private double mTotalPower; + + private boolean mScaleByMax = true; + + private PowerProfile mPowerProfile; + + private static final long BATTERY_SIZE = 1200; + + @Override + protected void onCreate(Bundle icicle) { + super.onCreate(icicle); + + addPreferencesFromResource(R.xml.power_usage_summary); + mBatteryInfo = IBatteryStats.Stub.asInterface( + ServiceManager.getService("batteryinfo")); + mAppListGroup = (PreferenceGroup) findPreference(PREF_APP_LIST); + mPowerProfile = new PowerProfile(this, "power_profile_default"); + } + + @Override + protected void onResume() { + super.onResume(); + + updateAppsList(); + } + + private void updateAppsList() { + if (mStats == null) { + load(); + } + + mAppListGroup.removeAll(); + mUsageList.clear(); + processCpuUsage(); + processMiscUsage(); + + mAppListGroup.setOrderingAsAdded(false); + + Collections.sort(mUsageList); + for (BatterySipper g : mUsageList) { + if (g.getSortValue() < MIN_POWER_THRESHOLD) continue; + double percent = ((g.getSortValue() / mTotalPower) * 100 / BATTERY_SIZE); + PowerGaugePreference pref = new PowerGaugePreference(this, g.getIcon()); + double scaleByMax = (g.getSortValue() * 100) / mTotalPower; + pref.setSummary(g.getLabel() + " ( " + (int) g.getSortValue() + "mA, " + + String.format("%3.2f", scaleByMax) + "% )"); + pref.setOrder(Integer.MAX_VALUE - (int) g.getSortValue()); // Invert the order + pref.setGaugeValue(mScaleByMax ? scaleByMax : percent); + mAppListGroup.addPreference(pref); + if (mAppListGroup.getPreferenceCount() > MAX_ITEMS_TO_LIST) break; + } + } + + private void processCpuUsage() { + final int which = mStatsType; + final double powerCpuNormal = mPowerProfile.getAveragePower(PowerProfile.POWER_CPU_NORMAL); + long uSecTime = mStats.computeBatteryRealtime(SystemClock.elapsedRealtime(), which) * 1000; + SparseArray uidStats = mStats.getUidStats(); + final int NU = uidStats.size(); + if (DEBUG) Log.i(TAG, "uidStats size = " + NU); + for (int iu = 0; iu < NU; iu++) { + Uid u = uidStats.valueAt(iu); + double power = 0; + //mUsageList.add(new AppUsage(u.getUid(), new double[] {power})); + Map processStats = u.getProcessStats(); + if (processStats.size() > 0) { + for (Map.Entry ent + : processStats.entrySet()) { + + Uid.Proc ps = ent.getValue(); + long userTime = ps.getUserTime(which); + long systemTime = ps.getSystemTime(which); + //long starts = ps.getStarts(which); + power += (userTime + systemTime) * 10 /* convert to milliseconds */ + * powerCpuNormal; + + } + } + power /= 1000; + + Map sensorStats = u.getSensorStats(); + for (Map.Entry sensorEntry + : sensorStats.entrySet()) { + Uid.Sensor sensor = sensorEntry.getValue(); + int sensorType = sensor.getHandle(); + BatteryStats.Timer timer = sensor.getSensorTime(); + long sensorTime = timer.getTotalTimeLocked(uSecTime, which); + double multiplier = 0; + switch (sensorType) { + case Uid.Sensor.GPS: + multiplier = mPowerProfile.getAveragePower(PowerProfile.POWER_GPS_ON); + break; + } + power += multiplier * sensorTime; + } + if (power != 0) { + BatterySipper app = new BatterySipper(null, 0, u.getUid(), new double[] {power}); + mUsageList.add(app); + } + if (power > mMaxPower) mMaxPower = power; + mTotalPower += power; + if (DEBUG) Log.i(TAG, "Added power = " + power); + } + } + + private double getPhoneOnPower(long uSecNow) { + return mPowerProfile.getAveragePower(PowerProfile.POWER_RADIO_ACTIVE) + * mStats.getPhoneOnTime(uSecNow, mStatsType) / 1000 / 1000; + } + + private double getScreenOnPower(long uSecNow) { + double power = 0; + power += mStats.getScreenOnTime(uSecNow, mStatsType) + * mPowerProfile.getAveragePower(PowerProfile.POWER_SCREEN_ON) / 1000; // millis + final double screenFullPower = + mPowerProfile.getAveragePower(PowerProfile.POWER_SCREEN_FULL); + for (int i = 0; i < BatteryStats.NUM_SCREEN_BRIGHTNESS_BINS; i++) { + double screenBinPower = screenFullPower * i / BatteryStats.NUM_SCREEN_BRIGHTNESS_BINS; + long brightnessTime = mStats.getScreenBrightnessTime(i, uSecNow, mStatsType) / 1000; + power += screenBinPower * brightnessTime; + if (DEBUG) { + Log.i(TAG, "Screen bin power = " + (int) screenBinPower + ", time = " + + brightnessTime); + } + } + return power / 1000; + } + + private double getRadioPower(long uSecNow, int which) { + double power = 0; + final int BINS = BatteryStats.NUM_SIGNAL_STRENGTH_BINS; + for (int i = 0; i < BINS; i++) { + power += mStats.getPhoneSignalStrengthTime(i, uSecNow, which) / 1000 / 1000 * + mPowerProfile.getAveragePower(PowerProfile.POWER_RADIO_ON) + * ((BINS - i) / (double) BINS); + } + return power; + } + + private void processMiscUsage() { + final int which = mStatsType; + long uSecTime = SystemClock.elapsedRealtime() * 1000; + final long uSecNow = mStats.computeBatteryRealtime(uSecTime, which); + final long timeSinceUnplugged = uSecNow; + if (DEBUG) { + Log.i(TAG, "Uptime since last unplugged = " + (timeSinceUnplugged / 1000)); + } + + double phoneOnPower = getPhoneOnPower(uSecNow); + addEntry(getString(R.string.power_phone), android.R.drawable.ic_menu_call, phoneOnPower); + + double screenOnPower = getScreenOnPower(uSecNow); + addEntry(getString(R.string.power_screen), android.R.drawable.ic_menu_view, screenOnPower); + + double wifiPower = (mStats.getWifiOnTime(uSecNow, which) * 0 /* TODO */ + * mPowerProfile.getAveragePower(PowerProfile.POWER_WIFI_ON) + + mStats.getWifiRunningTime(uSecNow, which) + * mPowerProfile.getAveragePower(PowerProfile.POWER_WIFI_ON)) / 1000 / 1000; + addEntry(getString(R.string.power_wifi), R.drawable.ic_wifi_signal_4, wifiPower); + + double idlePower = ((timeSinceUnplugged - mStats.getScreenOnTime(uSecNow, mStatsType)) + * mPowerProfile.getAveragePower(PowerProfile.POWER_CPU_IDLE)) / 1000 / 1000; + addEntry(getString(R.string.power_idle), android.R.drawable.ic_lock_power_off, idlePower); + + double radioPower = getRadioPower(uSecNow, which); + addEntry(getString(R.string.power_cell), + android.R.drawable.ic_menu_sort_by_size, radioPower); + } + + private void addEntry(String label, int iconId, double power) { + if (power > mMaxPower) mMaxPower = power; + mTotalPower += power; + BatterySipper bs = new BatterySipper(label, iconId, 0, new double[] {power}); + mUsageList.add(bs); + } + + private void load() { + try { + byte[] data = mBatteryInfo.getStatistics(); + Parcel parcel = Parcel.obtain(); + parcel.unmarshall(data, 0, data.length); + parcel.setDataPosition(0); + mStats = com.android.internal.os.BatteryStatsImpl.CREATOR + .createFromParcel(parcel); + } catch (RemoteException e) { + Log.e(TAG, "RemoteException:", e); + } + } + + class BatterySipper implements Comparable { + String mLabel; + Drawable mIcon; + int mUid; + double mValue; + double[] mValues; + + BatterySipper(String label, int iconId, int uid, double[] values) { + mValues = values; + mLabel = label; + if (iconId > 0) { + mIcon = getResources().getDrawable(iconId); + } + if (mValues != null) mValue = mValues[0]; + //if (uid > 0 && (mLabel == null || mIcon == null) // TODO: + if ((label == null || iconId == 0) && uid > 0) { + getNameForUid(uid); + } + } + + double getSortValue() { + return mValue; + } + + double[] getValues() { + return mValues; + } + + Drawable getIcon() { + return mIcon; + } + + public int compareTo(BatterySipper other) { + // Return the flipped value because we want the items in descending order + return (int) (other.getSortValue() - getSortValue()); + } + + String getLabel() { + return mLabel; + } + + /** + * Sets mLabel and mIcon + * @param uid Uid of the application + */ + void getNameForUid(int uid) { + // TODO: Do this on a separate thread + PackageManager pm = getPackageManager(); + String[] packages = pm.getPackagesForUid(uid); + if (packages == null) { + mLabel = Integer.toString(uid); + return; + } + + String[] packageNames = new String[packages.length]; + System.arraycopy(packages, 0, packageNames, 0, packages.length); + + // Convert package names to user-facing labels where possible + for (int i = 0; i < packageNames.length; i++) { + //packageNames[i] = PowerUsageSummary.getLabel(packageNames[i], pm); + try { + ApplicationInfo ai = pm.getApplicationInfo(packageNames[i], 0); + CharSequence label = ai.loadLabel(pm); + if (label != null) { + packageNames[i] = label.toString(); + } + if (mIcon == null) { + mIcon = ai.loadIcon(pm); + } + } catch (NameNotFoundException e) { + } + } + + if (packageNames.length == 1) { + mLabel = packageNames[0]; + } else { + // Look for an official name for this UID. + for (String name : packages) { + try { + PackageInfo pi = pm.getPackageInfo(name, 0); + if (pi.sharedUserLabel != 0) { + CharSequence nm = pm.getText(name, + pi.sharedUserLabel, pi.applicationInfo); + if (nm != null) { + mLabel = nm.toString(); + break; + } + } + } catch (PackageManager.NameNotFoundException e) { + } + } + } + } + } +} \ No newline at end of file