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.content.pm.PackageManager;
23 import android.graphics.drawable.Drawable;
24 import android.os.AggregateBatteryConsumer;
25 import android.os.BatteryConsumer;
26 import android.os.BatteryUsageStats;
27 import android.os.Handler;
28 import android.os.Looper;
29 import android.os.Message;
30 import android.os.Process;
31 import android.os.UidBatteryConsumer;
32 import android.os.UserBatteryConsumer;
33 import android.os.UserHandle;
34 import android.os.UserManager;
35 import android.text.TextUtils;
36 import android.text.format.DateUtils;
37 import android.util.ArrayMap;
38 import android.util.SparseArray;
39 
40 import androidx.annotation.VisibleForTesting;
41 import androidx.preference.Preference;
42 import androidx.preference.PreferenceGroup;
43 import androidx.preference.PreferenceScreen;
44 
45 import com.android.internal.os.PowerProfile;
46 import com.android.settings.R;
47 import com.android.settings.SettingsActivity;
48 import com.android.settings.core.InstrumentedPreferenceFragment;
49 import com.android.settings.core.PreferenceControllerMixin;
50 import com.android.settings.overlay.FeatureFactory;
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.Comparator;
60 import java.util.List;
61 
62 /**
63  * Controller that update the battery header view
64  */
65 public class BatteryAppListPreferenceController extends AbstractPreferenceController
66         implements PreferenceControllerMixin, LifecycleObserver, OnPause, OnDestroy {
67     @VisibleForTesting
68     static final boolean USE_FAKE_DATA = false;
69     private static final int MAX_ITEMS_TO_LIST = USE_FAKE_DATA ? 30 : 20;
70     private static final int MIN_AVERAGE_POWER_THRESHOLD_MILLI_AMP = 10;
71     private static final String MEDIASERVER_PACKAGE_NAME = "mediaserver";
72 
73     private final String mPreferenceKey;
74     @VisibleForTesting
75     PreferenceGroup mAppListGroup;
76     private BatteryUsageStats mBatteryUsageStats;
77     private ArrayMap<String, Preference> mPreferenceCache;
78     @VisibleForTesting
79     BatteryUtils mBatteryUtils;
80     private final UserManager mUserManager;
81     private final PackageManager mPackageManager;
82     private final SettingsActivity mActivity;
83     private final InstrumentedPreferenceFragment mFragment;
84     private Context mPrefContext;
85 
86     /**
87      * Battery attribution list configuration.
88      */
89     public interface Config {
90         /**
91          * Returns true if the attribution list should be shown.
92          */
shouldShowBatteryAttributionList(Context context)93         boolean shouldShowBatteryAttributionList(Context context);
94     }
95 
96     @VisibleForTesting
97     static Config sConfig = new Config() {
98         @Override
99         public boolean shouldShowBatteryAttributionList(Context context) {
100             if (USE_FAKE_DATA) {
101                 return true;
102             }
103 
104             PowerProfile powerProfile = new PowerProfile(context);
105             // Cheap hack to try to figure out if the power_profile.xml was populated.
106             return powerProfile.getAveragePowerForOrdinal(
107                     PowerProfile.POWER_GROUP_DISPLAY_SCREEN_FULL, 0)
108                     >= MIN_AVERAGE_POWER_THRESHOLD_MILLI_AMP;
109         }
110     };
111 
112     private final Handler mHandler = new Handler(Looper.getMainLooper()) {
113         @Override
114         public void handleMessage(Message msg) {
115             switch (msg.what) {
116                 case BatteryEntry.MSG_UPDATE_NAME_ICON:
117                     BatteryEntry entry = (BatteryEntry) msg.obj;
118                     PowerGaugePreference pgp = mAppListGroup.findPreference(entry.getKey());
119                     if (pgp != null) {
120                         final int userId = UserHandle.getUserId(entry.getUid());
121                         final UserHandle userHandle = new UserHandle(userId);
122                         pgp.setIcon(mUserManager.getBadgedIconForUser(entry.getIcon(), userHandle));
123                         pgp.setTitle(entry.name);
124                         if (entry.isAppEntry()) {
125                             pgp.setContentDescription(entry.name);
126                         }
127                     }
128                     break;
129                 case BatteryEntry.MSG_REPORT_FULLY_DRAWN:
130                     Activity activity = mActivity;
131                     if (activity != null) {
132                         activity.reportFullyDrawn();
133                     }
134                     break;
135             }
136             super.handleMessage(msg);
137         }
138     };
139 
BatteryAppListPreferenceController(Context context, String preferenceKey, Lifecycle lifecycle, SettingsActivity activity, InstrumentedPreferenceFragment fragment)140     public BatteryAppListPreferenceController(Context context, String preferenceKey,
141             Lifecycle lifecycle, SettingsActivity activity,
142             InstrumentedPreferenceFragment fragment) {
143         super(context);
144 
145         if (lifecycle != null) {
146             lifecycle.addObserver(this);
147         }
148 
149         mPreferenceKey = preferenceKey;
150         mBatteryUtils = BatteryUtils.getInstance(context);
151         mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
152         mPackageManager = context.getPackageManager();
153         mActivity = activity;
154         mFragment = fragment;
155     }
156 
157     @Override
onPause()158     public void onPause() {
159         BatteryEntry.stopRequestQueue();
160         mHandler.removeMessages(BatteryEntry.MSG_UPDATE_NAME_ICON);
161     }
162 
163     @Override
onDestroy()164     public void onDestroy() {
165         if (mActivity.isChangingConfigurations()) {
166             BatteryEntry.clearUidCache();
167         }
168     }
169 
170     @Override
displayPreference(PreferenceScreen screen)171     public void displayPreference(PreferenceScreen screen) {
172         super.displayPreference(screen);
173         mPrefContext = screen.getContext();
174         mAppListGroup = screen.findPreference(mPreferenceKey);
175         mAppListGroup.setTitle(mPrefContext.getString(R.string.power_usage_list_summary));
176     }
177 
178     @Override
isAvailable()179     public boolean isAvailable() {
180         return true;
181     }
182 
183     @Override
getPreferenceKey()184     public String getPreferenceKey() {
185         return mPreferenceKey;
186     }
187 
188     @Override
handlePreferenceTreeClick(Preference preference)189     public boolean handlePreferenceTreeClick(Preference preference) {
190         if (preference instanceof PowerGaugePreference) {
191             PowerGaugePreference pgp = (PowerGaugePreference) preference;
192             BatteryEntry entry = pgp.getInfo();
193             AdvancedPowerUsageDetail.startBatteryDetailPage(mActivity,
194                     mFragment, entry, pgp.getPercent(), /*isValidToShowSummary=*/ true);
195             return true;
196         }
197         return false;
198     }
199 
200     /**
201      * Refreshes the list of battery consumers using the supplied BatteryUsageStats.
202      */
refreshAppListGroup(BatteryUsageStats batteryUsageStats, boolean showAllApps)203     public void refreshAppListGroup(BatteryUsageStats batteryUsageStats, boolean showAllApps) {
204         if (!isAvailable()) {
205             return;
206         }
207 
208         mBatteryUsageStats = USE_FAKE_DATA ? getFakeStats() : batteryUsageStats;
209         mAppListGroup.setTitle(R.string.power_usage_list_summary);
210 
211         boolean addedSome = false;
212 
213         cacheRemoveAllPrefs(mAppListGroup);
214         mAppListGroup.setOrderingAsAdded(false);
215 
216         if (sConfig.shouldShowBatteryAttributionList(mContext)) {
217             final int dischargePercentage = getDischargePercentage(batteryUsageStats);
218             final List<BatteryEntry> usageList =
219                 getCoalescedUsageList(showAllApps, /*loadDataInBackground=*/ true);
220             final double totalPower = batteryUsageStats.getConsumedPower();
221             final int numSippers = usageList.size();
222             for (int i = 0; i < numSippers; i++) {
223                 final BatteryEntry entry = usageList.get(i);
224 
225                 final double percentOfTotal = mBatteryUtils.calculateBatteryPercent(
226                         entry.getConsumedPower(), totalPower, dischargePercentage);
227 
228                 if (((int) (percentOfTotal + .5)) < 1) {
229                     continue;
230                 }
231 
232                 final UserHandle userHandle = new UserHandle(UserHandle.getUserId(entry.getUid()));
233                 final Drawable badgedIcon = mUserManager.getBadgedIconForUser(entry.getIcon(),
234                         userHandle);
235                 final CharSequence contentDescription = mUserManager.getBadgedLabelForUser(
236                         entry.getLabel(), userHandle);
237 
238                 final String key = entry.getKey();
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                 entry.percent = percentOfTotal;
246                 pref.setTitle(entry.getLabel());
247                 pref.setOrder(i + 1);
248                 pref.setPercent(percentOfTotal);
249                 pref.shouldShowAnomalyIcon(false);
250                 setUsageSummary(pref, entry);
251                 addedSome = true;
252                 mAppListGroup.addPreference(pref);
253                 if (mAppListGroup.getPreferenceCount() - getCachedCount()
254                         > (MAX_ITEMS_TO_LIST + 1)) {
255                     break;
256                 }
257             }
258         }
259         if (!addedSome) {
260             addNotAvailableMessage();
261         }
262         removeCachedPrefs(mAppListGroup);
263 
264         BatteryEntry.startRequestQueue();
265     }
266 
267     /**
268      * Gets the BatteryEntry list by using the supplied BatteryUsageStats.
269      */
getBatteryEntryList( BatteryUsageStats batteryUsageStats, boolean showAllApps)270     public List<BatteryEntry> getBatteryEntryList(
271             BatteryUsageStats batteryUsageStats, boolean showAllApps) {
272         mBatteryUsageStats = USE_FAKE_DATA ? getFakeStats() : batteryUsageStats;
273         if (!sConfig.shouldShowBatteryAttributionList(mContext)) {
274             return null;
275         }
276         final int dischargePercentage = getDischargePercentage(batteryUsageStats);
277         final List<BatteryEntry> usageList =
278             getCoalescedUsageList(showAllApps, /*loadDataInBackground=*/ false);
279         final double totalPower = batteryUsageStats.getConsumedPower();
280         for (int i = 0; i < usageList.size(); i++) {
281             final BatteryEntry entry = usageList.get(i);
282             final double percentOfTotal = mBatteryUtils.calculateBatteryPercent(
283                     entry.getConsumedPower(), totalPower, dischargePercentage);
284             entry.percent = percentOfTotal;
285         }
286         return usageList;
287     }
288 
getDischargePercentage(BatteryUsageStats batteryUsageStats)289     private int getDischargePercentage(BatteryUsageStats batteryUsageStats) {
290         int dischargePercentage = batteryUsageStats.getDischargePercentage();
291         if (dischargePercentage < 0) {
292             dischargePercentage = 0;
293         }
294         return dischargePercentage;
295     }
296 
297     /**
298      * We want to coalesce some UIDs. For example, dex2oat runs under a shared gid that
299      * exists for all users of the same app. We detect this case and merge the power use
300      * for dex2oat to the device OWNER's use of the app.
301      *
302      * @return A sorted list of apps using power.
303      */
getCoalescedUsageList( boolean showAllApps, boolean loadDataInBackground)304     private List<BatteryEntry> getCoalescedUsageList(
305             boolean showAllApps, boolean loadDataInBackground) {
306         final SparseArray<BatteryEntry> batteryEntryList = new SparseArray<>();
307 
308         final ArrayList<BatteryEntry> results = new ArrayList<>();
309         final List<UidBatteryConsumer> uidBatteryConsumers =
310                 mBatteryUsageStats.getUidBatteryConsumers();
311 
312         // Sort to have all apps with "real" UIDs first, followed by apps that are supposed
313         // to be combined with the real ones.
314         uidBatteryConsumers.sort(Comparator.comparingInt(
315                 consumer -> consumer.getUid() == getRealUid(consumer) ? 0 : 1));
316 
317         for (int i = 0, size = uidBatteryConsumers.size(); i < size; i++) {
318             final UidBatteryConsumer consumer = uidBatteryConsumers.get(i);
319             final int uid = getRealUid(consumer);
320 
321             final String[] packages = mPackageManager.getPackagesForUid(uid);
322             if (mBatteryUtils.shouldHideUidBatteryConsumerUnconditionally(consumer, packages)) {
323                 continue;
324             }
325 
326             final boolean isHidden = mBatteryUtils.shouldHideUidBatteryConsumer(consumer, packages);
327             if (isHidden && !showAllApps) {
328                 continue;
329             }
330 
331             final int index = batteryEntryList.indexOfKey(uid);
332             if (index < 0) {
333                 // New entry.
334                 batteryEntryList.put(uid, new BatteryEntry(mContext, mHandler, mUserManager,
335                         consumer, isHidden, uid, packages, null, loadDataInBackground));
336             } else {
337                 // Combine BatterySippers if we already have one with this UID.
338                 final BatteryEntry existingSipper = batteryEntryList.valueAt(index);
339                 existingSipper.add(consumer);
340             }
341         }
342 
343         final BatteryConsumer deviceConsumer = mBatteryUsageStats.getAggregateBatteryConsumer(
344                 BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_DEVICE);
345         final BatteryConsumer appsConsumer = mBatteryUsageStats.getAggregateBatteryConsumer(
346                 BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_ALL_APPS);
347 
348         for (int componentId = 0; componentId < BatteryConsumer.POWER_COMPONENT_COUNT;
349                 componentId++) {
350             if (!showAllApps
351                     && mBatteryUtils.shouldHideDevicePowerComponent(deviceConsumer, componentId)) {
352                 continue;
353             }
354 
355             results.add(new BatteryEntry(mContext, componentId,
356                     deviceConsumer.getConsumedPower(componentId),
357                     appsConsumer.getConsumedPower(componentId),
358                     deviceConsumer.getUsageDurationMillis(componentId)));
359         }
360 
361         for (int componentId = BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID;
362                 componentId < BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID
363                         + deviceConsumer.getCustomPowerComponentCount();
364                 componentId++) {
365             if (!showAllApps) {
366                 continue;
367             }
368 
369             results.add(new BatteryEntry(mContext, componentId,
370                     deviceConsumer.getCustomPowerComponentName(componentId),
371                     deviceConsumer.getConsumedPowerForCustomComponent(componentId),
372                     appsConsumer.getConsumedPowerForCustomComponent(componentId)));
373         }
374 
375         if (showAllApps) {
376             final List<UserBatteryConsumer> userBatteryConsumers =
377                     mBatteryUsageStats.getUserBatteryConsumers();
378             for (int i = 0, size = userBatteryConsumers.size(); i < size; i++) {
379                 final UserBatteryConsumer consumer = userBatteryConsumers.get(i);
380                 results.add(new BatteryEntry(mContext, mHandler, mUserManager,
381                         consumer, /* isHidden */ true, Process.INVALID_UID, null, null,
382                         loadDataInBackground));
383             }
384         }
385 
386         final int numUidSippers = batteryEntryList.size();
387 
388         for (int i = 0; i < numUidSippers; i++) {
389             results.add(batteryEntryList.valueAt(i));
390         }
391 
392         // The sort order must have changed, so re-sort based on total power use.
393         results.sort(BatteryEntry.COMPARATOR);
394         return results;
395     }
396 
getRealUid(UidBatteryConsumer consumer)397     private int getRealUid(UidBatteryConsumer consumer) {
398         int realUid = consumer.getUid();
399 
400         // Check if this UID is a shared GID. If so, we combine it with the OWNER's
401         // actual app UID.
402         if (isSharedGid(consumer.getUid())) {
403             realUid = UserHandle.getUid(UserHandle.USER_SYSTEM,
404                     UserHandle.getAppIdFromSharedAppGid(consumer.getUid()));
405         }
406 
407         // Check if this UID is a system UID (mediaserver, logd, nfc, drm, etc).
408         if (isSystemUid(realUid)
409                 && !MEDIASERVER_PACKAGE_NAME.equals(consumer.getPackageWithHighestDrain())) {
410             // Use the system UID for all UIDs running in their own sandbox that
411             // are not apps. We exclude mediaserver because we already are expected to
412             // report that as a separate item.
413             realUid = Process.SYSTEM_UID;
414         }
415         return realUid;
416     }
417 
418     @VisibleForTesting
setUsageSummary(Preference preference, BatteryEntry entry)419     void setUsageSummary(Preference preference, BatteryEntry entry) {
420         // Only show summary when usage time is longer than one minute
421         final long usageTimeMs = entry.getTimeInForegroundMs();
422         if (shouldShowSummary(entry) && usageTimeMs >= DateUtils.MINUTE_IN_MILLIS) {
423             final CharSequence timeSequence =
424                     StringUtil.formatElapsedTime(mContext, usageTimeMs, false, false);
425             preference.setSummary(
426                     entry.isHidden()
427                             ? timeSequence
428                             : TextUtils.expandTemplate(mContext.getText(R.string.battery_used_for),
429                                     timeSequence));
430         }
431     }
432 
cacheRemoveAllPrefs(PreferenceGroup group)433     private void cacheRemoveAllPrefs(PreferenceGroup group) {
434         mPreferenceCache = new ArrayMap<>();
435         final int N = group.getPreferenceCount();
436         for (int i = 0; i < N; i++) {
437             Preference p = group.getPreference(i);
438             if (TextUtils.isEmpty(p.getKey())) {
439                 continue;
440             }
441             mPreferenceCache.put(p.getKey(), p);
442         }
443     }
444 
shouldShowSummary(BatteryEntry entry)445     private boolean shouldShowSummary(BatteryEntry entry) {
446         final CharSequence[] allowlistPackages =
447                 FeatureFactory.getFactory(mContext)
448                         .getPowerUsageFeatureProvider(mContext)
449                         .getHideApplicationSummary(mContext);
450         final String target = entry.getDefaultPackageName();
451 
452         for (CharSequence packageName : allowlistPackages) {
453             if (TextUtils.equals(target, packageName)) {
454                 return false;
455             }
456         }
457         return true;
458     }
459 
isSharedGid(int uid)460     private static boolean isSharedGid(int uid) {
461         return UserHandle.getAppIdFromSharedAppGid(uid) > 0;
462     }
463 
isSystemUid(int uid)464     private static boolean isSystemUid(int uid) {
465         final int appUid = UserHandle.getAppId(uid);
466         return appUid >= Process.SYSTEM_UID && appUid < Process.FIRST_APPLICATION_UID;
467     }
468 
getFakeStats()469     private BatteryUsageStats getFakeStats() {
470         BatteryUsageStats.Builder builder = new BatteryUsageStats.Builder(new String[0])
471                 .setDischargePercentage(100);
472 
473         float use = 500;
474         final AggregateBatteryConsumer.Builder appsBatteryConsumerBuilder =
475                 builder.getAggregateBatteryConsumerBuilder(
476                         BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_ALL_APPS);
477         final AggregateBatteryConsumer.Builder deviceBatteryConsumerBuilder =
478                 builder.getAggregateBatteryConsumerBuilder(
479                         BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_DEVICE);
480         for (@BatteryConsumer.PowerComponent int componentId : new int[]{
481                 BatteryConsumer.POWER_COMPONENT_AMBIENT_DISPLAY,
482                 BatteryConsumer.POWER_COMPONENT_BLUETOOTH,
483                 BatteryConsumer.POWER_COMPONENT_CAMERA,
484                 BatteryConsumer.POWER_COMPONENT_FLASHLIGHT,
485                 BatteryConsumer.POWER_COMPONENT_IDLE,
486                 BatteryConsumer.POWER_COMPONENT_MEMORY,
487                 BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO,
488                 BatteryConsumer.POWER_COMPONENT_PHONE,
489                 BatteryConsumer.POWER_COMPONENT_SCREEN,
490                 BatteryConsumer.POWER_COMPONENT_WIFI,
491         }) {
492             appsBatteryConsumerBuilder.setConsumedPower(componentId, use);
493             deviceBatteryConsumerBuilder.setConsumedPower(componentId, use * 2);
494             use += 5;
495         }
496 
497         use = 450;
498         for (int i = 0; i < 100; i++) {
499             builder.getOrCreateUidBatteryConsumerBuilder(
500                             new FakeUid(Process.FIRST_APPLICATION_UID + i))
501                     .setTimeInStateMs(UidBatteryConsumer.STATE_FOREGROUND, 10000 + i * 1000)
502                     .setTimeInStateMs(UidBatteryConsumer.STATE_BACKGROUND, 20000 + i * 2000)
503                     .setConsumedPower(BatteryConsumer.POWER_COMPONENT_CPU, use);
504             use += 1;
505         }
506 
507         // Simulate dex2oat process.
508         builder.getOrCreateUidBatteryConsumerBuilder(new FakeUid(Process.FIRST_APPLICATION_UID))
509                 .setUsageDurationMillis(BatteryConsumer.POWER_COMPONENT_CPU, 100000)
510                 .setConsumedPower(BatteryConsumer.POWER_COMPONENT_CPU, 1000.0)
511                 .setPackageWithHighestDrain("dex2oat");
512 
513         builder.getOrCreateUidBatteryConsumerBuilder(new FakeUid(Process.FIRST_APPLICATION_UID + 1))
514                 .setUsageDurationMillis(BatteryConsumer.POWER_COMPONENT_CPU, 100000)
515                 .setConsumedPower(BatteryConsumer.POWER_COMPONENT_CPU, 1000.0)
516                 .setPackageWithHighestDrain("dex2oat");
517 
518         builder.getOrCreateUidBatteryConsumerBuilder(
519                         new FakeUid(UserHandle.getSharedAppGid(Process.LOG_UID)))
520                 .setUsageDurationMillis(BatteryConsumer.POWER_COMPONENT_CPU, 100000)
521                 .setConsumedPower(BatteryConsumer.POWER_COMPONENT_CPU, 900.0);
522 
523         return builder.build();
524     }
525 
getCachedPreference(String key)526     private Preference getCachedPreference(String key) {
527         return mPreferenceCache != null ? mPreferenceCache.remove(key) : null;
528     }
529 
removeCachedPrefs(PreferenceGroup group)530     private void removeCachedPrefs(PreferenceGroup group) {
531         for (Preference p : mPreferenceCache.values()) {
532             group.removePreference(p);
533         }
534         mPreferenceCache = null;
535     }
536 
getCachedCount()537     private int getCachedCount() {
538         return mPreferenceCache != null ? mPreferenceCache.size() : 0;
539     }
540 
addNotAvailableMessage()541     private void addNotAvailableMessage() {
542         final String NOT_AVAILABLE = "not_available";
543         Preference notAvailable = getCachedPreference(NOT_AVAILABLE);
544         if (notAvailable == null) {
545             notAvailable = new Preference(mPrefContext);
546             notAvailable.setKey(NOT_AVAILABLE);
547             notAvailable.setTitle(R.string.power_usage_not_available);
548             notAvailable.setSelectable(false);
549             mAppListGroup.addPreference(notAvailable);
550         }
551     }
552 }
553