Home | History | Annotate | Download | only in fuelgauge
      1 /*
      2  * Copyright (C) 2009 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.settings.fuelgauge;
     18 
     19 import android.app.Activity;
     20 import android.content.Context;
     21 import android.graphics.drawable.Drawable;
     22 import android.os.BatteryStats;
     23 import android.os.Build;
     24 import android.os.Bundle;
     25 import android.os.Handler;
     26 import android.os.Message;
     27 import android.os.Process;
     28 import android.os.UserHandle;
     29 import android.support.v7.preference.Preference;
     30 import android.support.v7.preference.PreferenceGroup;
     31 import android.text.TextUtils;
     32 import android.util.SparseArray;
     33 import android.util.TypedValue;
     34 import android.view.Menu;
     35 import android.view.MenuInflater;
     36 import android.view.MenuItem;
     37 import com.android.internal.logging.MetricsProto.MetricsEvent;
     38 import com.android.internal.os.BatterySipper;
     39 import com.android.internal.os.BatterySipper.DrainType;
     40 import com.android.internal.os.PowerProfile;
     41 import com.android.settings.R;
     42 import com.android.settings.Settings.HighPowerApplicationsActivity;
     43 import com.android.settings.SettingsActivity;
     44 import com.android.settings.applications.ManageApplications;
     45 import com.android.settings.dashboard.SummaryLoader;
     46 import com.android.settingslib.BatteryInfo;
     47 
     48 import java.util.ArrayList;
     49 import java.util.Collections;
     50 import java.util.Comparator;
     51 import java.util.List;
     52 
     53 /**
     54  * Displays a list of apps and subsystems that consume power, ordered by how much power was
     55  * consumed since the last time it was unplugged.
     56  */
     57 public class PowerUsageSummary extends PowerUsageBase {
     58 
     59     private static final boolean DEBUG = false;
     60 
     61     private static final boolean USE_FAKE_DATA = false;
     62 
     63     static final String TAG = "PowerUsageSummary";
     64 
     65     private static final String KEY_APP_LIST = "app_list";
     66     private static final String KEY_BATTERY_HISTORY = "battery_history";
     67 
     68     private static final int MENU_STATS_TYPE = Menu.FIRST;
     69     private static final int MENU_HIGH_POWER_APPS = Menu.FIRST + 3;
     70     private static final int MENU_HELP = Menu.FIRST + 4;
     71 
     72     private BatteryHistoryPreference mHistPref;
     73     private PreferenceGroup mAppListGroup;
     74 
     75     private int mStatsType = BatteryStats.STATS_SINCE_CHARGED;
     76 
     77     private static final int MIN_POWER_THRESHOLD_MILLI_AMP = 5;
     78     private static final int MAX_ITEMS_TO_LIST = USE_FAKE_DATA ? 30 : 10;
     79     private static final int MIN_AVERAGE_POWER_THRESHOLD_MILLI_AMP = 10;
     80     private static final int SECONDS_IN_HOUR = 60 * 60;
     81 
     82     @Override
     83     public void onCreate(Bundle icicle) {
     84         super.onCreate(icicle);
     85         setAnimationAllowed(true);
     86 
     87         addPreferencesFromResource(R.xml.power_usage_summary);
     88         mHistPref = (BatteryHistoryPreference) findPreference(KEY_BATTERY_HISTORY);
     89         mAppListGroup = (PreferenceGroup) findPreference(KEY_APP_LIST);
     90     }
     91 
     92     @Override
     93     protected int getMetricsCategory() {
     94         return MetricsEvent.FUELGAUGE_POWER_USAGE_SUMMARY;
     95     }
     96 
     97     @Override
     98     public void onResume() {
     99         super.onResume();
    100         refreshStats();
    101     }
    102 
    103     @Override
    104     public void onPause() {
    105         BatteryEntry.stopRequestQueue();
    106         mHandler.removeMessages(BatteryEntry.MSG_UPDATE_NAME_ICON);
    107         super.onPause();
    108     }
    109 
    110     @Override
    111     public void onDestroy() {
    112         super.onDestroy();
    113         if (getActivity().isChangingConfigurations()) {
    114             BatteryEntry.clearUidCache();
    115         }
    116     }
    117 
    118     @Override
    119     public boolean onPreferenceTreeClick(Preference preference) {
    120         if (!(preference instanceof PowerGaugePreference)) {
    121             return super.onPreferenceTreeClick(preference);
    122         }
    123         PowerGaugePreference pgp = (PowerGaugePreference) preference;
    124         BatteryEntry entry = pgp.getInfo();
    125         PowerUsageDetail.startBatteryDetailPage((SettingsActivity) getActivity(), mStatsHelper,
    126                 mStatsType, entry, true, true);
    127         return super.onPreferenceTreeClick(preference);
    128     }
    129 
    130     @Override
    131     public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
    132         if (DEBUG) {
    133             menu.add(0, MENU_STATS_TYPE, 0, R.string.menu_stats_total)
    134                     .setIcon(com.android.internal.R.drawable.ic_menu_info_details)
    135                     .setAlphabeticShortcut('t');
    136         }
    137 
    138         menu.add(0, MENU_HIGH_POWER_APPS, 0, R.string.high_power_apps);
    139         super.onCreateOptionsMenu(menu, inflater);
    140     }
    141 
    142     @Override
    143     protected int getHelpResource() {
    144         return R.string.help_url_battery;
    145     }
    146 
    147     @Override
    148     public boolean onOptionsItemSelected(MenuItem item) {
    149         final SettingsActivity sa = (SettingsActivity) getActivity();
    150         switch (item.getItemId()) {
    151             case MENU_STATS_TYPE:
    152                 if (mStatsType == BatteryStats.STATS_SINCE_CHARGED) {
    153                     mStatsType = BatteryStats.STATS_SINCE_UNPLUGGED;
    154                 } else {
    155                     mStatsType = BatteryStats.STATS_SINCE_CHARGED;
    156                 }
    157                 refreshStats();
    158                 return true;
    159             case MENU_HIGH_POWER_APPS:
    160                 Bundle args = new Bundle();
    161                 args.putString(ManageApplications.EXTRA_CLASSNAME,
    162                         HighPowerApplicationsActivity.class.getName());
    163                 sa.startPreferencePanel(ManageApplications.class.getName(), args,
    164                         R.string.high_power_apps, null, null, 0);
    165                 return true;
    166             default:
    167                 return super.onOptionsItemSelected(item);
    168         }
    169     }
    170 
    171     private void addNotAvailableMessage() {
    172         final String NOT_AVAILABLE = "not_available";
    173         Preference notAvailable = getCachedPreference(NOT_AVAILABLE);
    174         if (notAvailable == null) {
    175             notAvailable = new Preference(getPrefContext());
    176             notAvailable.setKey(NOT_AVAILABLE);
    177             notAvailable.setTitle(R.string.power_usage_not_available);
    178             mAppListGroup.addPreference(notAvailable);
    179         }
    180     }
    181 
    182     private static boolean isSharedGid(int uid) {
    183         return UserHandle.getAppIdFromSharedAppGid(uid) > 0;
    184     }
    185 
    186     private static boolean isSystemUid(int uid) {
    187         return uid >= Process.SYSTEM_UID && uid < Process.FIRST_APPLICATION_UID;
    188     }
    189 
    190     /**
    191      * We want to coalesce some UIDs. For example, dex2oat runs under a shared gid that
    192      * exists for all users of the same app. We detect this case and merge the power use
    193      * for dex2oat to the device OWNER's use of the app.
    194      * @return A sorted list of apps using power.
    195      */
    196     private static List<BatterySipper> getCoalescedUsageList(final List<BatterySipper> sippers) {
    197         final SparseArray<BatterySipper> uidList = new SparseArray<>();
    198 
    199         final ArrayList<BatterySipper> results = new ArrayList<>();
    200         final int numSippers = sippers.size();
    201         for (int i = 0; i < numSippers; i++) {
    202             BatterySipper sipper = sippers.get(i);
    203             if (sipper.getUid() > 0) {
    204                 int realUid = sipper.getUid();
    205 
    206                 // Check if this UID is a shared GID. If so, we combine it with the OWNER's
    207                 // actual app UID.
    208                 if (isSharedGid(sipper.getUid())) {
    209                     realUid = UserHandle.getUid(UserHandle.USER_SYSTEM,
    210                             UserHandle.getAppIdFromSharedAppGid(sipper.getUid()));
    211                 }
    212 
    213                 // Check if this UID is a system UID (mediaserver, logd, nfc, drm, etc).
    214                 if (isSystemUid(realUid)
    215                         && !"mediaserver".equals(sipper.packageWithHighestDrain)) {
    216                     // Use the system UID for all UIDs running in their own sandbox that
    217                     // are not apps. We exclude mediaserver because we already are expected to
    218                     // report that as a separate item.
    219                     realUid = Process.SYSTEM_UID;
    220                 }
    221 
    222                 if (realUid != sipper.getUid()) {
    223                     // Replace the BatterySipper with a new one with the real UID set.
    224                     BatterySipper newSipper = new BatterySipper(sipper.drainType,
    225                             new FakeUid(realUid), 0.0);
    226                     newSipper.add(sipper);
    227                     newSipper.packageWithHighestDrain = sipper.packageWithHighestDrain;
    228                     newSipper.mPackages = sipper.mPackages;
    229                     sipper = newSipper;
    230                 }
    231 
    232                 int index = uidList.indexOfKey(realUid);
    233                 if (index < 0) {
    234                     // New entry.
    235                     uidList.put(realUid, sipper);
    236                 } else {
    237                     // Combine BatterySippers if we already have one with this UID.
    238                     final BatterySipper existingSipper = uidList.valueAt(index);
    239                     existingSipper.add(sipper);
    240                     if (existingSipper.packageWithHighestDrain == null
    241                             && sipper.packageWithHighestDrain != null) {
    242                         existingSipper.packageWithHighestDrain = sipper.packageWithHighestDrain;
    243                     }
    244 
    245                     final int existingPackageLen = existingSipper.mPackages != null ?
    246                             existingSipper.mPackages.length : 0;
    247                     final int newPackageLen = sipper.mPackages != null ?
    248                             sipper.mPackages.length : 0;
    249                     if (newPackageLen > 0) {
    250                         String[] newPackages = new String[existingPackageLen + newPackageLen];
    251                         if (existingPackageLen > 0) {
    252                             System.arraycopy(existingSipper.mPackages, 0, newPackages, 0,
    253                                     existingPackageLen);
    254                         }
    255                         System.arraycopy(sipper.mPackages, 0, newPackages, existingPackageLen,
    256                                 newPackageLen);
    257                         existingSipper.mPackages = newPackages;
    258                     }
    259                 }
    260             } else {
    261                 results.add(sipper);
    262             }
    263         }
    264 
    265         final int numUidSippers = uidList.size();
    266         for (int i = 0; i < numUidSippers; i++) {
    267             results.add(uidList.valueAt(i));
    268         }
    269 
    270         // The sort order must have changed, so re-sort based on total power use.
    271         Collections.sort(results, new Comparator<BatterySipper>() {
    272             @Override
    273             public int compare(BatterySipper a, BatterySipper b) {
    274                 return Double.compare(b.totalPowerMah, a.totalPowerMah);
    275             }
    276         });
    277         return results;
    278     }
    279 
    280     protected void refreshStats() {
    281         super.refreshStats();
    282         updatePreference(mHistPref);
    283         cacheRemoveAllPrefs(mAppListGroup);
    284         mAppListGroup.setOrderingAsAdded(false);
    285         boolean addedSome = false;
    286 
    287         final PowerProfile powerProfile = mStatsHelper.getPowerProfile();
    288         final BatteryStats stats = mStatsHelper.getStats();
    289         final double averagePower = powerProfile.getAveragePower(PowerProfile.POWER_SCREEN_FULL);
    290 
    291         TypedValue value = new TypedValue();
    292         getContext().getTheme().resolveAttribute(android.R.attr.colorControlNormal, value, true);
    293         int colorControl = getContext().getColor(value.resourceId);
    294 
    295         if (averagePower >= MIN_AVERAGE_POWER_THRESHOLD_MILLI_AMP || USE_FAKE_DATA) {
    296             final List<BatterySipper> usageList = getCoalescedUsageList(
    297                     USE_FAKE_DATA ? getFakeStats() : mStatsHelper.getUsageList());
    298 
    299             final int dischargeAmount = USE_FAKE_DATA ? 5000
    300                     : stats != null ? stats.getDischargeAmount(mStatsType) : 0;
    301             final int numSippers = usageList.size();
    302             for (int i = 0; i < numSippers; i++) {
    303                 final BatterySipper sipper = usageList.get(i);
    304                 if ((sipper.totalPowerMah * SECONDS_IN_HOUR) < MIN_POWER_THRESHOLD_MILLI_AMP) {
    305                     continue;
    306                 }
    307                 double totalPower = USE_FAKE_DATA ? 4000 : mStatsHelper.getTotalPower();
    308                 final double percentOfTotal =
    309                         ((sipper.totalPowerMah / totalPower) * dischargeAmount);
    310                 if (((int) (percentOfTotal + .5)) < 1) {
    311                     continue;
    312                 }
    313                 if (sipper.drainType == BatterySipper.DrainType.OVERCOUNTED) {
    314                     // Don't show over-counted unless it is at least 2/3 the size of
    315                     // the largest real entry, and its percent of total is more significant
    316                     if (sipper.totalPowerMah < ((mStatsHelper.getMaxRealPower()*2)/3)) {
    317                         continue;
    318                     }
    319                     if (percentOfTotal < 10) {
    320                         continue;
    321                     }
    322                     if ("user".equals(Build.TYPE)) {
    323                         continue;
    324                     }
    325                 }
    326                 if (sipper.drainType == BatterySipper.DrainType.UNACCOUNTED) {
    327                     // Don't show over-counted unless it is at least 1/2 the size of
    328                     // the largest real entry, and its percent of total is more significant
    329                     if (sipper.totalPowerMah < (mStatsHelper.getMaxRealPower()/2)) {
    330                         continue;
    331                     }
    332                     if (percentOfTotal < 5) {
    333                         continue;
    334                     }
    335                     if ("user".equals(Build.TYPE)) {
    336                         continue;
    337                     }
    338                 }
    339                 final UserHandle userHandle = new UserHandle(UserHandle.getUserId(sipper.getUid()));
    340                 final BatteryEntry entry = new BatteryEntry(getActivity(), mHandler, mUm, sipper);
    341                 final Drawable badgedIcon = mUm.getBadgedIconForUser(entry.getIcon(),
    342                         userHandle);
    343                 final CharSequence contentDescription = mUm.getBadgedLabelForUser(entry.getLabel(),
    344                         userHandle);
    345                 final String key = sipper.drainType == DrainType.APP ? sipper.getPackages() != null
    346                         ? TextUtils.concat(sipper.getPackages()).toString()
    347                         : String.valueOf(sipper.getUid())
    348                         : sipper.drainType.toString();
    349                 PowerGaugePreference pref = (PowerGaugePreference) getCachedPreference(key);
    350                 if (pref == null) {
    351                     pref = new PowerGaugePreference(getPrefContext(), badgedIcon,
    352                             contentDescription, entry);
    353                     pref.setKey(key);
    354                 }
    355 
    356                 final double percentOfMax = (sipper.totalPowerMah * 100)
    357                         / mStatsHelper.getMaxPower();
    358                 sipper.percent = percentOfTotal;
    359                 pref.setTitle(entry.getLabel());
    360                 pref.setOrder(i + 1);
    361                 pref.setPercent(percentOfMax, percentOfTotal);
    362                 if (sipper.uidObj != null) {
    363                     pref.setKey(Integer.toString(sipper.uidObj.getUid()));
    364                 }
    365                 if ((sipper.drainType != DrainType.APP || sipper.uidObj.getUid() == 0)
    366                          && sipper.drainType != DrainType.USER) {
    367                     pref.setTint(colorControl);
    368                 }
    369                 addedSome = true;
    370                 mAppListGroup.addPreference(pref);
    371                 if (mAppListGroup.getPreferenceCount() - getCachedCount()
    372                         > (MAX_ITEMS_TO_LIST + 1)) {
    373                     break;
    374                 }
    375             }
    376         }
    377         if (!addedSome) {
    378             addNotAvailableMessage();
    379         }
    380         removeCachedPrefs(mAppListGroup);
    381 
    382         BatteryEntry.startRequestQueue();
    383     }
    384 
    385     private static List<BatterySipper> getFakeStats() {
    386         ArrayList<BatterySipper> stats = new ArrayList<>();
    387         float use = 5;
    388         for (DrainType type : DrainType.values()) {
    389             if (type == DrainType.APP) {
    390                 continue;
    391             }
    392             stats.add(new BatterySipper(type, null, use));
    393             use += 5;
    394         }
    395         for (int i = 0; i < 100; i++) {
    396             stats.add(new BatterySipper(DrainType.APP,
    397                     new FakeUid(Process.FIRST_APPLICATION_UID + i), use));
    398         }
    399         stats.add(new BatterySipper(DrainType.APP,
    400                 new FakeUid(0), use));
    401 
    402         // Simulate dex2oat process.
    403         BatterySipper sipper = new BatterySipper(DrainType.APP,
    404                 new FakeUid(UserHandle.getSharedAppGid(Process.FIRST_APPLICATION_UID)), 10.0f);
    405         sipper.packageWithHighestDrain = "dex2oat";
    406         stats.add(sipper);
    407 
    408         sipper = new BatterySipper(DrainType.APP,
    409                 new FakeUid(UserHandle.getSharedAppGid(Process.FIRST_APPLICATION_UID + 1)), 10.0f);
    410         sipper.packageWithHighestDrain = "dex2oat";
    411         stats.add(sipper);
    412 
    413         sipper = new BatterySipper(DrainType.APP,
    414                 new FakeUid(UserHandle.getSharedAppGid(Process.LOG_UID)), 9.0f);
    415         stats.add(sipper);
    416 
    417         return stats;
    418     }
    419 
    420     Handler mHandler = new Handler() {
    421 
    422         @Override
    423         public void handleMessage(Message msg) {
    424             switch (msg.what) {
    425                 case BatteryEntry.MSG_UPDATE_NAME_ICON:
    426                     BatteryEntry entry = (BatteryEntry) msg.obj;
    427                     PowerGaugePreference pgp =
    428                             (PowerGaugePreference) findPreference(
    429                                     Integer.toString(entry.sipper.uidObj.getUid()));
    430                     if (pgp != null) {
    431                         final int userId = UserHandle.getUserId(entry.sipper.getUid());
    432                         final UserHandle userHandle = new UserHandle(userId);
    433                         pgp.setIcon(mUm.getBadgedIconForUser(entry.getIcon(), userHandle));
    434                         pgp.setTitle(entry.name);
    435                         if (entry.sipper.drainType == DrainType.APP) {
    436                             pgp.setContentDescription(entry.name);
    437                         }
    438                     }
    439                     break;
    440                 case BatteryEntry.MSG_REPORT_FULLY_DRAWN:
    441                     Activity activity = getActivity();
    442                     if (activity != null) {
    443                         activity.reportFullyDrawn();
    444                     }
    445                     break;
    446             }
    447             super.handleMessage(msg);
    448         }
    449     };
    450 
    451     private static class SummaryProvider implements SummaryLoader.SummaryProvider {
    452         private final Context mContext;
    453         private final SummaryLoader mLoader;
    454 
    455         private SummaryProvider(Context context, SummaryLoader loader) {
    456             mContext = context;
    457             mLoader = loader;
    458         }
    459 
    460         @Override
    461         public void setListening(boolean listening) {
    462             if (listening) {
    463                 // TODO: Listen.
    464                 BatteryInfo.getBatteryInfo(mContext, new BatteryInfo.Callback() {
    465                     @Override
    466                     public void onBatteryInfoLoaded(BatteryInfo info) {
    467                         mLoader.setSummary(SummaryProvider.this, info.mChargeLabelString);
    468                     }
    469                 });
    470             }
    471         }
    472     }
    473 
    474     public static final SummaryLoader.SummaryProviderFactory SUMMARY_PROVIDER_FACTORY
    475             = new SummaryLoader.SummaryProviderFactory() {
    476         @Override
    477         public SummaryLoader.SummaryProvider createSummaryProvider(Activity activity,
    478                                                                    SummaryLoader summaryLoader) {
    479             return new SummaryProvider(activity, summaryLoader);
    480         }
    481     };
    482 }
    483