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