Home | History | Annotate | Download | only in fuelgauge
      1 /*
      2  * Copyright (C) 2017 The Android Open Source Project
      3  * Licensed under the Apache License, Version 2.0 (the "License");
      4  * you may not use this file except in compliance with the License.
      5  * You may obtain a copy of the License at
      6  *
      7  *      http://www.apache.org/licenses/LICENSE-2.0
      8  *
      9  * Unless required by applicable law or agreed to in writing, software
     10  * distributed under the License is distributed on an "AS IS" BASIS,
     11  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     12  * See the License for the specific language governing permissions and
     13  * limitations under the License.
     14  *
     15  *
     16  */
     17 
     18 package com.android.settings.fuelgauge;
     19 
     20 import android.app.Activity;
     21 import android.content.Context;
     22 import android.graphics.drawable.Drawable;
     23 import android.os.BatteryStats;
     24 import android.os.Handler;
     25 import android.os.Looper;
     26 import android.os.Message;
     27 import android.os.Process;
     28 import android.os.UserHandle;
     29 import android.os.UserManager;
     30 import android.support.annotation.VisibleForTesting;
     31 import android.support.v7.preference.Preference;
     32 import android.support.v7.preference.PreferenceGroup;
     33 import android.support.v7.preference.PreferenceScreen;
     34 import android.text.TextUtils;
     35 import android.text.format.DateUtils;
     36 import android.util.ArrayMap;
     37 import android.util.FeatureFlagUtils;
     38 import android.util.Log;
     39 import android.util.SparseArray;
     40 
     41 import com.android.internal.os.BatterySipper;
     42 import com.android.internal.os.BatterySipper.DrainType;
     43 import com.android.internal.os.BatteryStatsHelper;
     44 import com.android.internal.os.PowerProfile;
     45 import com.android.settings.R;
     46 import com.android.settings.SettingsActivity;
     47 import com.android.settings.core.FeatureFlags;
     48 import com.android.settings.core.InstrumentedPreferenceFragment;
     49 import com.android.settings.core.PreferenceControllerMixin;
     50 import com.android.settings.fuelgauge.anomaly.Anomaly;
     51 import com.android.settingslib.core.AbstractPreferenceController;
     52 import com.android.settingslib.core.lifecycle.Lifecycle;
     53 import com.android.settingslib.core.lifecycle.LifecycleObserver;
     54 import com.android.settingslib.core.lifecycle.events.OnDestroy;
     55 import com.android.settingslib.core.lifecycle.events.OnPause;
     56 import com.android.settingslib.utils.StringUtil;
     57 
     58 import java.util.ArrayList;
     59 import java.util.List;
     60 
     61 /**
     62  * Controller that update the battery header view
     63  */
     64 public class BatteryAppListPreferenceController extends AbstractPreferenceController
     65         implements PreferenceControllerMixin, LifecycleObserver, OnPause, OnDestroy {
     66     @VisibleForTesting
     67     static final boolean USE_FAKE_DATA = false;
     68     private static final int MAX_ITEMS_TO_LIST = USE_FAKE_DATA ? 30 : 10;
     69     private static final int MIN_AVERAGE_POWER_THRESHOLD_MILLI_AMP = 10;
     70     private static final int STATS_TYPE = BatteryStats.STATS_SINCE_CHARGED;
     71 
     72     private final String mPreferenceKey;
     73     @VisibleForTesting
     74     PreferenceGroup mAppListGroup;
     75     private BatteryStatsHelper mBatteryStatsHelper;
     76     private ArrayMap<String, Preference> mPreferenceCache;
     77     @VisibleForTesting
     78     BatteryUtils mBatteryUtils;
     79     private UserManager mUserManager;
     80     private SettingsActivity mActivity;
     81     private InstrumentedPreferenceFragment mFragment;
     82     private Context mPrefContext;
     83     SparseArray<List<Anomaly>> mAnomalySparseArray;
     84 
     85     private Handler mHandler = new Handler(Looper.getMainLooper()) {
     86         @Override
     87         public void handleMessage(Message msg) {
     88             switch (msg.what) {
     89                 case BatteryEntry.MSG_UPDATE_NAME_ICON:
     90                     BatteryEntry entry = (BatteryEntry) msg.obj;
     91                     PowerGaugePreference pgp =
     92                             (PowerGaugePreference) mAppListGroup.findPreference(
     93                                     Integer.toString(entry.sipper.uidObj.getUid()));
     94                     if (pgp != null) {
     95                         final int userId = UserHandle.getUserId(entry.sipper.getUid());
     96                         final UserHandle userHandle = new UserHandle(userId);
     97                         pgp.setIcon(mUserManager.getBadgedIconForUser(entry.getIcon(), userHandle));
     98                         pgp.setTitle(entry.name);
     99                         if (entry.sipper.drainType == DrainType.APP) {
    100                             pgp.setContentDescription(entry.name);
    101                         }
    102                     }
    103                     break;
    104                 case BatteryEntry.MSG_REPORT_FULLY_DRAWN:
    105                     Activity activity = mActivity;
    106                     if (activity != null) {
    107                         activity.reportFullyDrawn();
    108                     }
    109                     break;
    110             }
    111             super.handleMessage(msg);
    112         }
    113     };
    114 
    115     public BatteryAppListPreferenceController(Context context, String preferenceKey,
    116             Lifecycle lifecycle, SettingsActivity activity,
    117             InstrumentedPreferenceFragment fragment) {
    118         super(context);
    119 
    120         if (lifecycle != null) {
    121             lifecycle.addObserver(this);
    122         }
    123 
    124         mPreferenceKey = preferenceKey;
    125         mBatteryUtils = BatteryUtils.getInstance(context);
    126         mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
    127         mActivity = activity;
    128         mFragment = fragment;
    129     }
    130 
    131     @Override
    132     public void onPause() {
    133         BatteryEntry.stopRequestQueue();
    134         mHandler.removeMessages(BatteryEntry.MSG_UPDATE_NAME_ICON);
    135     }
    136 
    137     @Override
    138     public void onDestroy() {
    139         if (mActivity.isChangingConfigurations()) {
    140             BatteryEntry.clearUidCache();
    141         }
    142     }
    143 
    144     @Override
    145     public void displayPreference(PreferenceScreen screen) {
    146         super.displayPreference(screen);
    147         mPrefContext = screen.getContext();
    148         mAppListGroup = (PreferenceGroup) screen.findPreference(mPreferenceKey);
    149     }
    150 
    151     @Override
    152     public boolean isAvailable() {
    153         return true;
    154     }
    155 
    156     @Override
    157     public String getPreferenceKey() {
    158         return mPreferenceKey;
    159     }
    160 
    161     @Override
    162     public boolean handlePreferenceTreeClick(Preference preference) {
    163         if (preference instanceof PowerGaugePreference) {
    164             PowerGaugePreference pgp = (PowerGaugePreference) preference;
    165             BatteryEntry entry = pgp.getInfo();
    166             AdvancedPowerUsageDetail.startBatteryDetailPage(mActivity,
    167                     mFragment, mBatteryStatsHelper, STATS_TYPE, entry, pgp.getPercent(),
    168                     mAnomalySparseArray != null ? mAnomalySparseArray.get(entry.sipper.getUid())
    169                             : null);
    170             return true;
    171         }
    172         return false;
    173     }
    174 
    175     public void refreshAnomalyIcon(final SparseArray<List<Anomaly>> anomalySparseArray) {
    176         if (!isAvailable()) {
    177             return;
    178         }
    179         mAnomalySparseArray = anomalySparseArray;
    180         for (int i = 0, size = anomalySparseArray.size(); i < size; i++) {
    181             final String key = extractKeyFromUid(anomalySparseArray.keyAt(i));
    182             final PowerGaugePreference pref = (PowerGaugePreference) mAppListGroup.findPreference(
    183                     key);
    184             if (pref != null) {
    185                 pref.shouldShowAnomalyIcon(true);
    186             }
    187         }
    188     }
    189 
    190     public void refreshAppListGroup(BatteryStatsHelper statsHelper, boolean showAllApps) {
    191         if (!isAvailable()) {
    192             return;
    193         }
    194 
    195         mBatteryStatsHelper = statsHelper;
    196         mAppListGroup.setTitle(R.string.power_usage_list_summary);
    197 
    198         final PowerProfile powerProfile = statsHelper.getPowerProfile();
    199         final BatteryStats stats = statsHelper.getStats();
    200         final double averagePower = powerProfile.getAveragePower(PowerProfile.POWER_SCREEN_FULL);
    201         boolean addedSome = false;
    202         final int dischargeAmount = USE_FAKE_DATA ? 5000
    203                 : stats != null ? stats.getDischargeAmount(STATS_TYPE) : 0;
    204 
    205         cacheRemoveAllPrefs(mAppListGroup);
    206         mAppListGroup.setOrderingAsAdded(false);
    207 
    208         if (averagePower >= MIN_AVERAGE_POWER_THRESHOLD_MILLI_AMP || USE_FAKE_DATA) {
    209             final List<BatterySipper> usageList = getCoalescedUsageList(
    210                     USE_FAKE_DATA ? getFakeStats() : statsHelper.getUsageList());
    211             double hiddenPowerMah = showAllApps ? 0 :
    212                     mBatteryUtils.removeHiddenBatterySippers(usageList);
    213             mBatteryUtils.sortUsageList(usageList);
    214 
    215             final int numSippers = usageList.size();
    216             for (int i = 0; i < numSippers; i++) {
    217                 final BatterySipper sipper = usageList.get(i);
    218                 double totalPower = USE_FAKE_DATA ? 4000 : statsHelper.getTotalPower();
    219 
    220                 final double percentOfTotal = mBatteryUtils.calculateBatteryPercent(
    221                         sipper.totalPowerMah, totalPower, hiddenPowerMah, dischargeAmount);
    222 
    223                 if (((int) (percentOfTotal + .5)) < 1) {
    224                     continue;
    225                 }
    226                 if (shouldHideSipper(sipper)) {
    227                     continue;
    228                 }
    229                 final UserHandle userHandle = new UserHandle(UserHandle.getUserId(sipper.getUid()));
    230                 final BatteryEntry entry = new BatteryEntry(mActivity, mHandler, mUserManager,
    231                         sipper);
    232                 final Drawable badgedIcon = mUserManager.getBadgedIconForUser(entry.getIcon(),
    233                         userHandle);
    234                 final CharSequence contentDescription = mUserManager.getBadgedLabelForUser(
    235                         entry.getLabel(),
    236                         userHandle);
    237 
    238                 final String key = extractKeyFromSipper(sipper);
    239                 PowerGaugePreference pref = (PowerGaugePreference) getCachedPreference(key);
    240                 if (pref == null) {
    241                     pref = new PowerGaugePreference(mPrefContext, badgedIcon,
    242                             contentDescription, entry);
    243                     pref.setKey(key);
    244                 }
    245                 sipper.percent = percentOfTotal;
    246                 pref.setTitle(entry.getLabel());
    247                 pref.setOrder(i + 1);
    248                 pref.setPercent(percentOfTotal);
    249                 pref.shouldShowAnomalyIcon(false);
    250                 if (sipper.usageTimeMs == 0 && sipper.drainType == DrainType.APP) {
    251                     sipper.usageTimeMs = mBatteryUtils.getProcessTimeMs(
    252                             BatteryUtils.StatusType.FOREGROUND, sipper.uidObj, STATS_TYPE);
    253                 }
    254                 setUsageSummary(pref, sipper);
    255                 addedSome = true;
    256                 mAppListGroup.addPreference(pref);
    257                 if (mAppListGroup.getPreferenceCount() - getCachedCount()
    258                         > (MAX_ITEMS_TO_LIST + 1)) {
    259                     break;
    260                 }
    261             }
    262         }
    263         if (!addedSome) {
    264             addNotAvailableMessage();
    265         }
    266         removeCachedPrefs(mAppListGroup);
    267 
    268         BatteryEntry.startRequestQueue();
    269     }
    270 
    271     /**
    272      * We want to coalesce some UIDs. For example, dex2oat runs under a shared gid that
    273      * exists for all users of the same app. We detect this case and merge the power use
    274      * for dex2oat to the device OWNER's use of the app.
    275      *
    276      * @return A sorted list of apps using power.
    277      */
    278     private List<BatterySipper> getCoalescedUsageList(final List<BatterySipper> sippers) {
    279         final SparseArray<BatterySipper> uidList = new SparseArray<>();
    280 
    281         final ArrayList<BatterySipper> results = new ArrayList<>();
    282         final int numSippers = sippers.size();
    283         for (int i = 0; i < numSippers; i++) {
    284             BatterySipper sipper = sippers.get(i);
    285             if (sipper.getUid() > 0) {
    286                 int realUid = sipper.getUid();
    287 
    288                 // Check if this UID is a shared GID. If so, we combine it with the OWNER's
    289                 // actual app UID.
    290                 if (isSharedGid(sipper.getUid())) {
    291                     realUid = UserHandle.getUid(UserHandle.USER_SYSTEM,
    292                             UserHandle.getAppIdFromSharedAppGid(sipper.getUid()));
    293                 }
    294 
    295                 // Check if this UID is a system UID (mediaserver, logd, nfc, drm, etc).
    296                 if (isSystemUid(realUid)
    297                         && !"mediaserver".equals(sipper.packageWithHighestDrain)) {
    298                     // Use the system UID for all UIDs running in their own sandbox that
    299                     // are not apps. We exclude mediaserver because we already are expected to
    300                     // report that as a separate item.
    301                     realUid = Process.SYSTEM_UID;
    302                 }
    303 
    304                 if (realUid != sipper.getUid()) {
    305                     // Replace the BatterySipper with a new one with the real UID set.
    306                     BatterySipper newSipper = new BatterySipper(sipper.drainType,
    307                             new FakeUid(realUid), 0.0);
    308                     newSipper.add(sipper);
    309                     newSipper.packageWithHighestDrain = sipper.packageWithHighestDrain;
    310                     newSipper.mPackages = sipper.mPackages;
    311                     sipper = newSipper;
    312                 }
    313 
    314                 int index = uidList.indexOfKey(realUid);
    315                 if (index < 0) {
    316                     // New entry.
    317                     uidList.put(realUid, sipper);
    318                 } else {
    319                     // Combine BatterySippers if we already have one with this UID.
    320                     final BatterySipper existingSipper = uidList.valueAt(index);
    321                     existingSipper.add(sipper);
    322                     if (existingSipper.packageWithHighestDrain == null
    323                             && sipper.packageWithHighestDrain != null) {
    324                         existingSipper.packageWithHighestDrain = sipper.packageWithHighestDrain;
    325                     }
    326 
    327                     final int existingPackageLen = existingSipper.mPackages != null ?
    328                             existingSipper.mPackages.length : 0;
    329                     final int newPackageLen = sipper.mPackages != null ?
    330                             sipper.mPackages.length : 0;
    331                     if (newPackageLen > 0) {
    332                         String[] newPackages = new String[existingPackageLen + newPackageLen];
    333                         if (existingPackageLen > 0) {
    334                             System.arraycopy(existingSipper.mPackages, 0, newPackages, 0,
    335                                     existingPackageLen);
    336                         }
    337                         System.arraycopy(sipper.mPackages, 0, newPackages, existingPackageLen,
    338                                 newPackageLen);
    339                         existingSipper.mPackages = newPackages;
    340                     }
    341                 }
    342             } else {
    343                 results.add(sipper);
    344             }
    345         }
    346 
    347         final int numUidSippers = uidList.size();
    348         for (int i = 0; i < numUidSippers; i++) {
    349             results.add(uidList.valueAt(i));
    350         }
    351 
    352         // The sort order must have changed, so re-sort based on total power use.
    353         mBatteryUtils.sortUsageList(results);
    354         return results;
    355     }
    356 
    357     @VisibleForTesting
    358     void setUsageSummary(Preference preference, BatterySipper sipper) {
    359         // Only show summary when usage time is longer than one minute
    360         final long usageTimeMs = sipper.usageTimeMs;
    361         if (usageTimeMs >= DateUtils.MINUTE_IN_MILLIS) {
    362             final CharSequence timeSequence =
    363                     StringUtil.formatElapsedTime(mContext, usageTimeMs, false);
    364             preference.setSummary(
    365                     (sipper.drainType != DrainType.APP || mBatteryUtils.shouldHideSipper(sipper))
    366                             ? timeSequence
    367                             : TextUtils.expandTemplate(mContext.getText(R.string.battery_used_for),
    368                                     timeSequence));
    369         }
    370     }
    371 
    372     @VisibleForTesting
    373     boolean shouldHideSipper(BatterySipper sipper) {
    374         // Don't show over-counted and unaccounted in any condition
    375         return sipper.drainType == BatterySipper.DrainType.OVERCOUNTED
    376                 || sipper.drainType == BatterySipper.DrainType.UNACCOUNTED;
    377     }
    378 
    379     @VisibleForTesting
    380     String extractKeyFromSipper(BatterySipper sipper) {
    381         if (sipper.uidObj != null) {
    382             return extractKeyFromUid(sipper.getUid());
    383         } else if (sipper.drainType == DrainType.USER) {
    384             return sipper.drainType.toString() + sipper.userId;
    385         } else if (sipper.drainType != DrainType.APP) {
    386             return sipper.drainType.toString();
    387         } else if (sipper.getPackages() != null) {
    388             return TextUtils.concat(sipper.getPackages()).toString();
    389         } else {
    390             Log.w(TAG, "Inappropriate BatterySipper without uid and package names: " + sipper);
    391             return "-1";
    392         }
    393     }
    394 
    395     @VisibleForTesting
    396     String extractKeyFromUid(int uid) {
    397         return Integer.toString(uid);
    398     }
    399 
    400     private void cacheRemoveAllPrefs(PreferenceGroup group) {
    401         mPreferenceCache = new ArrayMap<>();
    402         final int N = group.getPreferenceCount();
    403         for (int i = 0; i < N; i++) {
    404             Preference p = group.getPreference(i);
    405             if (TextUtils.isEmpty(p.getKey())) {
    406                 continue;
    407             }
    408             mPreferenceCache.put(p.getKey(), p);
    409         }
    410     }
    411 
    412     private static boolean isSharedGid(int uid) {
    413         return UserHandle.getAppIdFromSharedAppGid(uid) > 0;
    414     }
    415 
    416     private static boolean isSystemUid(int uid) {
    417         final int appUid = UserHandle.getAppId(uid);
    418         return appUid >= Process.SYSTEM_UID && appUid < Process.FIRST_APPLICATION_UID;
    419     }
    420 
    421     private static List<BatterySipper> getFakeStats() {
    422         ArrayList<BatterySipper> stats = new ArrayList<>();
    423         float use = 5;
    424         for (DrainType type : DrainType.values()) {
    425             if (type == DrainType.APP) {
    426                 continue;
    427             }
    428             stats.add(new BatterySipper(type, null, use));
    429             use += 5;
    430         }
    431         for (int i = 0; i < 100; i++) {
    432             stats.add(new BatterySipper(DrainType.APP,
    433                     new FakeUid(Process.FIRST_APPLICATION_UID + i), use));
    434         }
    435         stats.add(new BatterySipper(DrainType.APP,
    436                 new FakeUid(0), use));
    437 
    438         // Simulate dex2oat process.
    439         BatterySipper sipper = new BatterySipper(DrainType.APP,
    440                 new FakeUid(UserHandle.getSharedAppGid(Process.FIRST_APPLICATION_UID)), 10.0f);
    441         sipper.packageWithHighestDrain = "dex2oat";
    442         stats.add(sipper);
    443 
    444         sipper = new BatterySipper(DrainType.APP,
    445                 new FakeUid(UserHandle.getSharedAppGid(Process.FIRST_APPLICATION_UID + 1)), 10.0f);
    446         sipper.packageWithHighestDrain = "dex2oat";
    447         stats.add(sipper);
    448 
    449         sipper = new BatterySipper(DrainType.APP,
    450                 new FakeUid(UserHandle.getSharedAppGid(Process.LOG_UID)), 9.0f);
    451         stats.add(sipper);
    452 
    453         return stats;
    454     }
    455 
    456     private Preference getCachedPreference(String key) {
    457         return mPreferenceCache != null ? mPreferenceCache.remove(key) : null;
    458     }
    459 
    460     private void removeCachedPrefs(PreferenceGroup group) {
    461         for (Preference p : mPreferenceCache.values()) {
    462             group.removePreference(p);
    463         }
    464         mPreferenceCache = null;
    465     }
    466 
    467     private int getCachedCount() {
    468         return mPreferenceCache != null ? mPreferenceCache.size() : 0;
    469     }
    470 
    471     private void addNotAvailableMessage() {
    472         final String NOT_AVAILABLE = "not_available";
    473         Preference notAvailable = getCachedPreference(NOT_AVAILABLE);
    474         if (notAvailable == null) {
    475             notAvailable = new Preference(mPrefContext);
    476             notAvailable.setKey(NOT_AVAILABLE);
    477             notAvailable.setTitle(R.string.power_usage_not_available);
    478             notAvailable.setSelectable(false);
    479             mAppListGroup.addPreference(notAvailable);
    480         }
    481     }
    482 }
    483