blob: 1475034d75c40b8dcd9dc315eaff353254a265be [file] [log] [blame]
/*
* 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.app.Activity;
import android.graphics.drawable.Drawable;
import android.os.BatteryStats;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.Process;
import android.os.UserHandle;
import android.preference.Preference;
import android.preference.PreferenceGroup;
import android.preference.PreferenceScreen;
import android.text.TextUtils;
import android.util.SparseArray;
import android.util.TypedValue;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.os.BatterySipper;
import com.android.internal.os.BatterySipper.DrainType;
import com.android.internal.os.PowerProfile;
import com.android.settings.HelpUtils;
import com.android.settings.R;
import com.android.settings.Settings.HighPowerApplicationsActivity;
import com.android.settings.SettingsActivity;
import com.android.settings.applications.ManageApplications;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/**
* 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 PowerUsageBase {
private static final boolean DEBUG = false;
private static final boolean USE_FAKE_DATA = false;
static final String TAG = "PowerUsageSummary";
private static final String KEY_APP_LIST = "app_list";
private static final String KEY_BATTERY_HISTORY = "battery_history";
private static final int MENU_STATS_TYPE = Menu.FIRST;
private static final int MENU_BATTERY_SAVER = Menu.FIRST + 2;
private static final int MENU_HIGH_POWER_APPS = Menu.FIRST + 3;
private static final int MENU_HELP = Menu.FIRST + 4;
private BatteryHistoryPreference mHistPref;
private PreferenceGroup mAppListGroup;
private int mStatsType = BatteryStats.STATS_SINCE_CHARGED;
private static final int MIN_POWER_THRESHOLD_MILLI_AMP = 5;
private static final int MAX_ITEMS_TO_LIST = 10;
private static final int MIN_AVERAGE_POWER_THRESHOLD_MILLI_AMP = 10;
private static final int SECONDS_IN_HOUR = 60 * 60;
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
addPreferencesFromResource(R.xml.power_usage_summary);
mHistPref = (BatteryHistoryPreference) findPreference(KEY_BATTERY_HISTORY);
mAppListGroup = (PreferenceGroup) findPreference(KEY_APP_LIST);
}
@Override
protected int getMetricsCategory() {
return MetricsLogger.FUELGAUGE_POWER_USAGE_SUMMARY;
}
@Override
public void onResume() {
super.onResume();
refreshStats();
}
@Override
public void onPause() {
BatteryEntry.stopRequestQueue();
mHandler.removeMessages(BatteryEntry.MSG_UPDATE_NAME_ICON);
super.onPause();
}
@Override
public void onDestroy() {
super.onDestroy();
if (getActivity().isChangingConfigurations()) {
BatteryEntry.clearUidCache();
}
}
@Override
public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) {
if (!(preference instanceof PowerGaugePreference)) {
return false;
}
PowerGaugePreference pgp = (PowerGaugePreference) preference;
BatteryEntry entry = pgp.getInfo();
PowerUsageDetail.startBatteryDetailPage((SettingsActivity) getActivity(), mStatsHelper,
mStatsType, entry, true);
return super.onPreferenceTreeClick(preferenceScreen, preference);
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
if (DEBUG) {
menu.add(0, MENU_STATS_TYPE, 0, R.string.menu_stats_total)
.setIcon(com.android.internal.R.drawable.ic_menu_info_details)
.setAlphabeticShortcut('t');
}
MenuItem batterySaver = menu.add(0, MENU_BATTERY_SAVER, 0, R.string.battery_saver);
batterySaver.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
menu.add(0, MENU_HIGH_POWER_APPS, 0, R.string.high_power_apps);
String helpUrl;
if (!TextUtils.isEmpty(helpUrl = getResources().getString(R.string.help_url_battery))) {
final MenuItem help = menu.add(0, MENU_HELP, 0, R.string.help_label);
HelpUtils.prepareHelpMenuItem(getActivity(), help, helpUrl, getClass().getName());
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
final SettingsActivity sa = (SettingsActivity) getActivity();
switch (item.getItemId()) {
case MENU_STATS_TYPE:
if (mStatsType == BatteryStats.STATS_SINCE_CHARGED) {
mStatsType = BatteryStats.STATS_SINCE_UNPLUGGED;
} else {
mStatsType = BatteryStats.STATS_SINCE_CHARGED;
}
refreshStats();
return true;
case MENU_BATTERY_SAVER:
sa.startPreferencePanel(BatterySaverSettings.class.getName(), null,
R.string.battery_saver, null, null, 0);
return true;
case MENU_HIGH_POWER_APPS:
Bundle args = new Bundle();
args.putString(ManageApplications.EXTRA_CLASSNAME,
HighPowerApplicationsActivity.class.getName());
sa.startPreferencePanel(ManageApplications.class.getName(), args,
R.string.high_power_apps, null, null, 0);
return true;
default:
return super.onOptionsItemSelected(item);
}
}
private void addNotAvailableMessage() {
Preference notAvailable = new Preference(getActivity());
notAvailable.setTitle(R.string.power_usage_not_available);
mAppListGroup.addPreference(notAvailable);
}
/**
* We want to coalesce some UIDs. For example, dex2oat runs under a shared gid that
* exists for all users of the same app. We detect this case and merge the power use
* for dex2oat to the device OWNER's use of the app.
* @return A sorted list of app's using power.
*/
private static List<BatterySipper> getCoalescedUsageList(final List<BatterySipper> sippers) {
final SparseArray<BatterySipper> uidList = new SparseArray<>();
final ArrayList<BatterySipper> results = new ArrayList<>();
final int numSippers = sippers.size();
for (int i = 0; i < numSippers; i++) {
final BatterySipper sipper = sippers.get(i);
if (sipper.getUid() > 0) {
int realUid = sipper.getUid();
if (sipper.getUid() >= Process.FIRST_SHARED_APPLICATION_GID &&
sipper.getUid() <= Process.LAST_SHARED_APPLICATION_GID) {
// This is a shared gid being used to do work on behalf of an app across all
// users. But we'll blame the power on the device OWNER.
realUid = UserHandle.getUid(UserHandle.USER_OWNER,
UserHandle.getAppIdFromSharedAppGid(sipper.getUid()));
}
int index = uidList.indexOfKey(realUid);
if (index < 0) {
uidList.put(realUid, sipper);
} else {
BatterySipper existingSipper = uidList.valueAt(index);
if (existingSipper.getUid() >= Process.FIRST_SHARED_APPLICATION_GID &&
existingSipper.getUid() <= Process.FIRST_SHARED_APPLICATION_GID) {
// If the app already under this uid is a dex2oat run, then combine and
// substitute it with the actual app.
sipper.add(existingSipper);
uidList.setValueAt(index, sipper);
} else {
existingSipper.add(sipper);
}
}
} else {
results.add(sipper);
}
}
final int numUidSippers = uidList.size();
for (int i = 0; i < numUidSippers; i++) {
results.add(uidList.valueAt(i));
}
// The sort order must have changed, so re-sort based on total power use.
Collections.sort(results, new Comparator<BatterySipper>() {
@Override
public int compare(BatterySipper a, BatterySipper b) {
return Double.compare(b.totalPowerMah, a.totalPowerMah);
}
});
return results;
}
protected void refreshStats() {
super.refreshStats();
updatePreference(mHistPref);
mAppListGroup.removeAll();
mAppListGroup.setOrderingAsAdded(false);
boolean addedSome = false;
final PowerProfile powerProfile = mStatsHelper.getPowerProfile();
final BatteryStats stats = mStatsHelper.getStats();
final double averagePower = powerProfile.getAveragePower(PowerProfile.POWER_SCREEN_FULL);
TypedValue value = new TypedValue();
getContext().getTheme().resolveAttribute(android.R.attr.colorControlNormal, value, true);
int colorControl = getContext().getColor(value.resourceId);
if (averagePower >= MIN_AVERAGE_POWER_THRESHOLD_MILLI_AMP || USE_FAKE_DATA) {
final List<BatterySipper> usageList = getCoalescedUsageList(
USE_FAKE_DATA ? getFakeStats() : mStatsHelper.getUsageList());
final int dischargeAmount = USE_FAKE_DATA ? 5000
: stats != null ? stats.getDischargeAmount(mStatsType) : 0;
final int numSippers = usageList.size();
for (int i = 0; i < numSippers; i++) {
final BatterySipper sipper = usageList.get(i);
if ((sipper.totalPowerMah * SECONDS_IN_HOUR) < MIN_POWER_THRESHOLD_MILLI_AMP) {
continue;
}
double totalPower = USE_FAKE_DATA ? 4000 : mStatsHelper.getTotalPower();
final double percentOfTotal =
((sipper.totalPowerMah / totalPower) * dischargeAmount);
if (((int) (percentOfTotal + .5)) < 1) {
continue;
}
if (sipper.drainType == BatterySipper.DrainType.OVERCOUNTED) {
// Don't show over-counted unless it is at least 2/3 the size of
// the largest real entry, and its percent of total is more significant
if (sipper.totalPowerMah < ((mStatsHelper.getMaxRealPower()*2)/3)) {
continue;
}
if (percentOfTotal < 10) {
continue;
}
if ("user".equals(Build.TYPE)) {
continue;
}
}
if (sipper.drainType == BatterySipper.DrainType.UNACCOUNTED) {
// Don't show over-counted unless it is at least 1/2 the size of
// the largest real entry, and its percent of total is more significant
if (sipper.totalPowerMah < (mStatsHelper.getMaxRealPower()/2)) {
continue;
}
if (percentOfTotal < 5) {
continue;
}
if ("user".equals(Build.TYPE)) {
continue;
}
}
final UserHandle userHandle = new UserHandle(UserHandle.getUserId(sipper.getUid()));
final BatteryEntry entry = new BatteryEntry(getActivity(), mHandler, mUm, sipper);
final Drawable badgedIcon = mUm.getBadgedIconForUser(entry.getIcon(),
userHandle);
final CharSequence contentDescription = mUm.getBadgedLabelForUser(entry.getLabel(),
userHandle);
final PowerGaugePreference pref = new PowerGaugePreference(getActivity(),
badgedIcon, contentDescription, entry);
final double percentOfMax = (sipper.totalPowerMah * 100)
/ mStatsHelper.getMaxPower();
sipper.percent = percentOfTotal;
pref.setTitle(entry.getLabel());
pref.setOrder(i + 1);
pref.setPercent(percentOfMax, percentOfTotal);
if (sipper.uidObj != null) {
pref.setKey(Integer.toString(sipper.uidObj.getUid()));
}
if (sipper.drainType != DrainType.APP && sipper.drainType != DrainType.USER) {
pref.setTint(colorControl);
}
addedSome = true;
mAppListGroup.addPreference(pref);
if (mAppListGroup.getPreferenceCount() > (MAX_ITEMS_TO_LIST + 1)) {
break;
}
}
}
if (!addedSome) {
addNotAvailableMessage();
}
BatteryEntry.startRequestQueue();
}
private static List<BatterySipper> getFakeStats() {
ArrayList<BatterySipper> stats = new ArrayList<>();
float use = 5;
for (DrainType type : DrainType.values()) {
if (type == DrainType.APP) {
continue;
}
stats.add(new BatterySipper(type, null, use));
use += 5;
}
stats.add(new BatterySipper(DrainType.APP,
new FakeUid(Process.FIRST_APPLICATION_UID), use));
// Simulate dex2oat process.
BatterySipper sipper = new BatterySipper(DrainType.APP,
new FakeUid(UserHandle.getSharedAppGid(Process.FIRST_APPLICATION_UID)), 10.0f);
sipper.packageWithHighestDrain = "dex2oat";
stats.add(sipper);
return stats;
}
Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case BatteryEntry.MSG_UPDATE_NAME_ICON:
BatteryEntry entry = (BatteryEntry) msg.obj;
PowerGaugePreference pgp =
(PowerGaugePreference) findPreference(
Integer.toString(entry.sipper.uidObj.getUid()));
if (pgp != null) {
final int userId = UserHandle.getUserId(entry.sipper.getUid());
final UserHandle userHandle = new UserHandle(userId);
pgp.setIcon(mUm.getBadgedIconForUser(entry.getIcon(), userHandle));
pgp.setTitle(entry.name);
}
break;
case BatteryEntry.MSG_REPORT_FULLY_DRAWN:
Activity activity = getActivity();
if (activity != null) {
activity.reportFullyDrawn();
}
break;
}
super.handleMessage(msg);
}
};
}