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