1 /*
2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.settings.fuelgauge.batteryusage;
17 
18 import android.content.Context;
19 import android.content.pm.ApplicationInfo;
20 import android.content.pm.PackageManager;
21 import android.content.pm.PackageManager.NameNotFoundException;
22 import android.graphics.drawable.Drawable;
23 import android.os.UserHandle;
24 import android.os.UserManager;
25 import android.text.TextUtils;
26 import android.util.ArrayMap;
27 import android.util.Log;
28 import android.util.Pair;
29 
30 import androidx.annotation.GuardedBy;
31 import androidx.annotation.VisibleForTesting;
32 
33 import com.android.settings.R;
34 import com.android.settings.fuelgauge.BatteryUtils;
35 import com.android.settings.fuelgauge.batteryusage.BatteryEntry.NameAndIcon;
36 import com.android.settingslib.utils.StringUtil;
37 
38 import java.util.Comparator;
39 import java.util.Locale;
40 import java.util.Map;
41 
42 /** A container class to carry battery data in a specific time slot. */
43 public class BatteryDiffEntry {
44     private static final String TAG = "BatteryDiffEntry";
45     private static final Object sResourceCacheLock = new Object();
46     private static final Object sPackageNameAndUidCacheLock = new Object();
47     private static final Object sValidForRestrictionLock = new Object();
48 
49     static Locale sCurrentLocale = null;
50 
51     // Caches app label and icon to improve loading performance.
52     @GuardedBy("sResourceCacheLock")
53     static final Map<String, NameAndIcon> sResourceCache = new ArrayMap<>();
54 
55     // Caches package name and uid to improve loading performance.
56     @GuardedBy("sPackageNameAndUidCacheLock")
57     static final Map<String, Integer> sPackageNameAndUidCache = new ArrayMap<>();
58 
59     // Whether a specific item is valid to launch restriction page?
60     @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
61     @GuardedBy("sValidForRestrictionLock")
62     static final Map<String, Boolean> sValidForRestriction = new ArrayMap<>();
63 
64     /** A comparator for {@link BatteryDiffEntry} based on the sorting key. */
65     static final Comparator<BatteryDiffEntry> COMPARATOR =
66             (a, b) -> Double.compare(b.getSortingKey(), a.getSortingKey());
67 
68     static final String SYSTEM_APPS_KEY = "A|SystemApps";
69     static final String UNINSTALLED_APPS_KEY = "A|UninstalledApps";
70     static final String OTHERS_KEY = "S|Others";
71 
72     // key -> (label_id, icon_id)
73     private static final Map<String, Pair<Integer, Integer>> SPECIAL_ENTRY_MAP =
74             Map.of(
75                     SYSTEM_APPS_KEY,
76                     Pair.create(R.string.battery_usage_system_apps, R.drawable.ic_power_system),
77                     UNINSTALLED_APPS_KEY,
78                     Pair.create(
79                             R.string.battery_usage_uninstalled_apps,
80                             R.drawable.ic_battery_uninstalled),
81                     OTHERS_KEY,
82                     Pair.create(
83                             R.string.battery_usage_others,
84                             R.drawable.ic_settings_battery_usage_others));
85 
86     public long mUid;
87     public long mUserId;
88     public String mKey;
89     public boolean mIsHidden;
90     public int mComponentId;
91     public String mLegacyPackageName;
92     public String mLegacyLabel;
93     public int mConsumerType;
94     public long mForegroundUsageTimeInMs;
95     public long mForegroundServiceUsageTimeInMs;
96     public long mBackgroundUsageTimeInMs;
97     public long mScreenOnTimeInMs;
98     public double mConsumePower;
99     public double mForegroundUsageConsumePower;
100     public double mForegroundServiceUsageConsumePower;
101     public double mBackgroundUsageConsumePower;
102     public double mCachedUsageConsumePower;
103 
104     protected Context mContext;
105 
106     private double mTotalConsumePower;
107     private double mPercentage;
108     private int mAdjustPercentageOffset;
109     private UserManager mUserManager;
110     private String mDefaultPackageName = null;
111 
112     @VisibleForTesting int mAppIconId;
113     @VisibleForTesting String mAppLabel = null;
114     @VisibleForTesting Drawable mAppIcon = null;
115     @VisibleForTesting boolean mIsLoaded = false;
116     @VisibleForTesting boolean mValidForRestriction = true;
117 
BatteryDiffEntry( Context context, long uid, long userId, String key, boolean isHidden, int componentId, String legacyPackageName, String legacyLabel, int consumerType, long foregroundUsageTimeInMs, long foregroundServiceUsageTimeInMs, long backgroundUsageTimeInMs, long screenOnTimeInMs, double consumePower, double foregroundUsageConsumePower, double foregroundServiceUsageConsumePower, double backgroundUsageConsumePower, double cachedUsageConsumePower)118     public BatteryDiffEntry(
119             Context context,
120             long uid,
121             long userId,
122             String key,
123             boolean isHidden,
124             int componentId,
125             String legacyPackageName,
126             String legacyLabel,
127             int consumerType,
128             long foregroundUsageTimeInMs,
129             long foregroundServiceUsageTimeInMs,
130             long backgroundUsageTimeInMs,
131             long screenOnTimeInMs,
132             double consumePower,
133             double foregroundUsageConsumePower,
134             double foregroundServiceUsageConsumePower,
135             double backgroundUsageConsumePower,
136             double cachedUsageConsumePower) {
137         mContext = context;
138         mUid = uid;
139         mUserId = userId;
140         mKey = key;
141         mIsHidden = isHidden;
142         mComponentId = componentId;
143         mLegacyPackageName = legacyPackageName;
144         mLegacyLabel = legacyLabel;
145         mConsumerType = consumerType;
146         mForegroundUsageTimeInMs = foregroundUsageTimeInMs;
147         mForegroundServiceUsageTimeInMs = foregroundServiceUsageTimeInMs;
148         mBackgroundUsageTimeInMs = backgroundUsageTimeInMs;
149         mScreenOnTimeInMs = screenOnTimeInMs;
150         mConsumePower = consumePower;
151         mForegroundUsageConsumePower = foregroundUsageConsumePower;
152         mForegroundServiceUsageConsumePower = foregroundServiceUsageConsumePower;
153         mBackgroundUsageConsumePower = backgroundUsageConsumePower;
154         mCachedUsageConsumePower = cachedUsageConsumePower;
155         mUserManager = context.getSystemService(UserManager.class);
156     }
157 
BatteryDiffEntry(Context context, String key, String legacyLabel, int consumerType)158     public BatteryDiffEntry(Context context, String key, String legacyLabel, int consumerType) {
159         this(
160                 context,
161                 /* uid= */ 0,
162                 /* userId= */ 0,
163                 key,
164                 /* isHidden= */ false,
165                 /* componentId= */ -1,
166                 /* legacyPackageName= */ null,
167                 legacyLabel,
168                 consumerType,
169                 /* foregroundUsageTimeInMs= */ 0,
170                 /* foregroundServiceUsageTimeInMs= */ 0,
171                 /* backgroundUsageTimeInMs= */ 0,
172                 /* screenOnTimeInMs= */ 0,
173                 /* consumePower= */ 0,
174                 /* foregroundUsageConsumePower= */ 0,
175                 /* foregroundServiceUsageConsumePower= */ 0,
176                 /* backgroundUsageConsumePower= */ 0,
177                 /* cachedUsageConsumePower= */ 0);
178     }
179 
180     /** Sets the total consumed power in a specific time slot. */
setTotalConsumePower(double totalConsumePower)181     public void setTotalConsumePower(double totalConsumePower) {
182         mTotalConsumePower = totalConsumePower;
183         mPercentage = totalConsumePower == 0 ? 0 : (mConsumePower / mTotalConsumePower) * 100.0;
184         mAdjustPercentageOffset = 0;
185     }
186 
187     /** Gets the total consumed power in a specific time slot. */
getTotalConsumePower()188     public double getTotalConsumePower() {
189         return mTotalConsumePower;
190     }
191 
192     /** Gets the percentage of total consumed power. */
getPercentage()193     public double getPercentage() {
194         return mPercentage;
195     }
196 
197     /** Gets the percentage offset to adjust. */
getAdjustPercentageOffset()198     public double getAdjustPercentageOffset() {
199         return mAdjustPercentageOffset;
200     }
201 
202     /** Sets the percentage offset to adjust. */
setAdjustPercentageOffset(int offset)203     public void setAdjustPercentageOffset(int offset) {
204         mAdjustPercentageOffset = offset;
205     }
206 
207     /** Gets the key for sorting */
getSortingKey()208     public double getSortingKey() {
209         String key = getKey();
210         if (key == null) {
211             return getPercentage() + getAdjustPercentageOffset();
212         }
213 
214         // For special entries, put them to the end of the list.
215         switch (key) {
216             case UNINSTALLED_APPS_KEY:
217             case OTHERS_KEY:
218                 return -1;
219             case SYSTEM_APPS_KEY:
220                 return -2;
221             default:
222                 return getPercentage() + getAdjustPercentageOffset();
223         }
224     }
225 
226     /** Clones a new instance. */
clone()227     public BatteryDiffEntry clone() {
228         return new BatteryDiffEntry(
229                 this.mContext,
230                 this.mUid,
231                 this.mUserId,
232                 this.mKey,
233                 this.mIsHidden,
234                 this.mComponentId,
235                 this.mLegacyPackageName,
236                 this.mLegacyLabel,
237                 this.mConsumerType,
238                 this.mForegroundUsageTimeInMs,
239                 this.mForegroundServiceUsageTimeInMs,
240                 this.mBackgroundUsageTimeInMs,
241                 this.mScreenOnTimeInMs,
242                 this.mConsumePower,
243                 this.mForegroundUsageConsumePower,
244                 this.mForegroundServiceUsageConsumePower,
245                 this.mBackgroundUsageConsumePower,
246                 this.mCachedUsageConsumePower);
247     }
248 
249     /** Gets the app label name for this entry. */
getAppLabel()250     public String getAppLabel() {
251         loadLabelAndIcon();
252         // Returns default application label if we cannot find it.
253         return mAppLabel == null || mAppLabel.length() == 0 ? mLegacyLabel : mAppLabel;
254     }
255 
256     /** Gets the app icon {@link Drawable} for this entry. */
getAppIcon()257     public Drawable getAppIcon() {
258         loadLabelAndIcon();
259         return mAppIcon != null && mAppIcon.getConstantState() != null
260                 ? mAppIcon.getConstantState().newDrawable()
261                 : null;
262     }
263 
264     /** Gets the app icon id for this entry. */
getAppIconId()265     public int getAppIconId() {
266         loadLabelAndIcon();
267         return mAppIconId;
268     }
269 
270     /** Gets the searching package name for UID battery type. */
getPackageName()271     public String getPackageName() {
272         final String packageName =
273                 mDefaultPackageName != null ? mDefaultPackageName : mLegacyPackageName;
274         if (packageName == null) {
275             return packageName;
276         }
277         // Removes potential appended process name in the PackageName.
278         // From "com.opera.browser:privileged_process0" to "com.opera.browser"
279         final String[] splitPackageNames = packageName.split(":");
280         return splitPackageNames != null && splitPackageNames.length > 0
281                 ? splitPackageNames[0]
282                 : packageName;
283     }
284 
285     /** Whether this item is valid for users to launch restriction page? */
validForRestriction()286     public boolean validForRestriction() {
287         loadLabelAndIcon();
288         return mValidForRestriction;
289     }
290 
291     /** Whether the current BatteryDiffEntry is system component or not. */
isSystemEntry()292     public boolean isSystemEntry() {
293         if (mIsHidden) {
294             return false;
295         }
296         switch (mConsumerType) {
297             case ConvertUtils.CONSUMER_TYPE_USER_BATTERY:
298             case ConvertUtils.CONSUMER_TYPE_SYSTEM_BATTERY:
299                 return true;
300             case ConvertUtils.CONSUMER_TYPE_UID_BATTERY:
301             default:
302                 return false;
303         }
304     }
305 
306     /** Whether the current BatteryDiffEntry is uninstalled app or not. */
isUninstalledEntry()307     public boolean isUninstalledEntry() {
308         final String packageName = getPackageName();
309         if (TextUtils.isEmpty(packageName)
310                 || isSystemEntry()
311                 // Some special package UIDs could be 0. Those packages are not installed by users.
312                 || mUid == BatteryUtils.UID_ZERO) {
313             return false;
314         }
315 
316         final int uid = getPackageUid(packageName);
317         return uid == BatteryUtils.UID_REMOVED_APPS || uid == BatteryUtils.UID_NULL;
318     }
319 
getPackageUid(String packageName)320     private int getPackageUid(String packageName) {
321         synchronized (sPackageNameAndUidCacheLock) {
322             if (sPackageNameAndUidCache.containsKey(packageName)) {
323                 return sPackageNameAndUidCache.get(packageName);
324             }
325         }
326 
327         int uid =
328                 BatteryUtils.getInstance(mContext).getPackageUidAsUser(packageName, (int) mUserId);
329         synchronized (sPackageNameAndUidCacheLock) {
330             sPackageNameAndUidCache.put(packageName, uid);
331         }
332         return uid;
333     }
334 
loadLabelAndIcon()335     void loadLabelAndIcon() {
336         if (mIsLoaded) {
337             return;
338         }
339         // Checks whether we have cached data or not first before fetching.
340         final NameAndIcon nameAndIcon = getCache();
341         if (nameAndIcon != null) {
342             mAppLabel = nameAndIcon.mName;
343             mAppIcon = nameAndIcon.mIcon;
344             mAppIconId = nameAndIcon.mIconId;
345         }
346         Boolean validForRestriction = null;
347         synchronized (sValidForRestrictionLock) {
348             validForRestriction = sValidForRestriction.get(getKey());
349         }
350         if (validForRestriction != null) {
351             mValidForRestriction = validForRestriction;
352         }
353         // Both nameAndIcon and restriction configuration have cached data.
354         if (nameAndIcon != null && validForRestriction != null) {
355             return;
356         }
357         mIsLoaded = true;
358 
359         // Configures whether we can launch restriction page or not.
360         updateRestrictionFlagState();
361         synchronized (sValidForRestrictionLock) {
362             sValidForRestriction.put(getKey(), Boolean.valueOf(mValidForRestriction));
363         }
364 
365         if (getKey() != null && SPECIAL_ENTRY_MAP.containsKey(getKey())) {
366             Pair<Integer, Integer> pair = SPECIAL_ENTRY_MAP.get(getKey());
367             mAppLabel = mContext.getString(pair.first);
368             mAppIconId = pair.second;
369             mAppIcon = mContext.getDrawable(mAppIconId);
370             putResourceCache(getKey(), new NameAndIcon(mAppLabel, mAppIcon, mAppIconId));
371             return;
372         }
373 
374         // Loads application icon and label based on consumer type.
375         switch (mConsumerType) {
376             case ConvertUtils.CONSUMER_TYPE_USER_BATTERY:
377                 final NameAndIcon nameAndIconForUser =
378                         BatteryEntry.getNameAndIconFromUserId(mContext, (int) mUserId);
379                 if (nameAndIconForUser != null) {
380                     mAppIcon = nameAndIconForUser.mIcon;
381                     mAppLabel = nameAndIconForUser.mName;
382                     putResourceCache(
383                             getKey(), new NameAndIcon(mAppLabel, mAppIcon, /* iconId= */ 0));
384                 }
385                 break;
386             case ConvertUtils.CONSUMER_TYPE_SYSTEM_BATTERY:
387                 final NameAndIcon nameAndIconForSystem =
388                         BatteryEntry.getNameAndIconFromPowerComponent(mContext, mComponentId);
389                 if (nameAndIconForSystem != null) {
390                     mAppLabel = nameAndIconForSystem.mName;
391                     if (nameAndIconForSystem.mIconId != 0) {
392                         mAppIconId = nameAndIconForSystem.mIconId;
393                         mAppIcon = mContext.getDrawable(nameAndIconForSystem.mIconId);
394                     }
395                     putResourceCache(getKey(), new NameAndIcon(mAppLabel, mAppIcon, mAppIconId));
396                 }
397                 break;
398             case ConvertUtils.CONSUMER_TYPE_UID_BATTERY:
399                 loadNameAndIconForUid();
400                 // Uses application default icon if we cannot find it from package.
401                 if (mAppIcon == null) {
402                     mAppIcon = mContext.getPackageManager().getDefaultActivityIcon();
403                 }
404                 // Adds badge icon into app icon for work profile.
405                 mAppIcon = getBadgeIconForUser(mAppIcon);
406                 if (mAppLabel != null || mAppIcon != null) {
407                     putResourceCache(
408                             getKey(), new NameAndIcon(mAppLabel, mAppIcon, /* iconId= */ 0));
409                 }
410                 break;
411         }
412     }
413 
getKey()414     String getKey() {
415         return mKey;
416     }
417 
418     @VisibleForTesting
updateRestrictionFlagState()419     void updateRestrictionFlagState() {
420         if (isSystemEntry()) {
421             mValidForRestriction = false;
422             return;
423         }
424         final boolean isValidPackage =
425                 BatteryUtils.getInstance(mContext).getPackageUid(getPackageName())
426                         != BatteryUtils.UID_NULL;
427         if (!isValidPackage) {
428             mValidForRestriction = false;
429             return;
430         }
431         try {
432             mValidForRestriction =
433                     mContext.getPackageManager()
434                                     .getPackageInfo(
435                                             getPackageName(),
436                                             PackageManager.MATCH_DISABLED_COMPONENTS
437                                                     | PackageManager.MATCH_ANY_USER
438                                                     | PackageManager.GET_SIGNATURES
439                                                     | PackageManager.GET_PERMISSIONS)
440                             != null;
441         } catch (Exception e) {
442             Log.e(
443                     TAG,
444                     String.format(
445                             "getPackageInfo() error %s for package=%s",
446                             e.getCause(), getPackageName()));
447             mValidForRestriction = false;
448         }
449     }
450 
getCache()451     private NameAndIcon getCache() {
452         final Locale locale = Locale.getDefault();
453         if (sCurrentLocale != locale) {
454             Log.d(
455                     TAG,
456                     String.format(
457                             "clearCache() locale is changed from %s to %s",
458                             sCurrentLocale, locale));
459             sCurrentLocale = locale;
460             clearCache();
461         }
462         synchronized (sResourceCacheLock) {
463             return sResourceCache.get(getKey());
464         }
465     }
466 
loadNameAndIconForUid()467     private void loadNameAndIconForUid() {
468         final String packageName = getPackageName();
469         final PackageManager packageManager = mContext.getPackageManager();
470         // Gets the application label from PackageManager.
471         if (packageName != null && packageName.length() != 0) {
472             try {
473                 final ApplicationInfo appInfo =
474                         packageManager.getApplicationInfo(packageName, /*no flags*/ 0);
475                 if (appInfo != null) {
476                     mAppLabel = packageManager.getApplicationLabel(appInfo).toString();
477                     mAppIcon = packageManager.getApplicationIcon(appInfo);
478                 }
479             } catch (NameNotFoundException e) {
480                 Log.e(TAG, "failed to retrieve ApplicationInfo for: " + packageName);
481                 mAppLabel = packageName;
482             }
483         }
484         // Early return if we found the app label and icon resource.
485         if (mAppLabel != null && mAppIcon != null) {
486             return;
487         }
488 
489         final int uid = (int) mUid;
490         final String[] packages = packageManager.getPackagesForUid(uid);
491         // Loads special defined application label and icon if available.
492         if (packages == null || packages.length == 0) {
493             final NameAndIcon nameAndIcon =
494                     BatteryEntry.getNameAndIconFromUid(mContext, mAppLabel, uid);
495             mAppLabel = nameAndIcon.mName;
496             mAppIcon = nameAndIcon.mIcon;
497         }
498 
499         final NameAndIcon nameAndIcon =
500                 BatteryEntry.loadNameAndIcon(
501                         mContext, uid, /* batteryEntry= */ null, packageName, mAppLabel, mAppIcon);
502         // Clears BatteryEntry internal cache since we will have another one.
503         BatteryEntry.clearUidCache();
504         if (nameAndIcon != null) {
505             mAppLabel = nameAndIcon.mName;
506             mAppIcon = nameAndIcon.mIcon;
507             mDefaultPackageName = nameAndIcon.mPackageName;
508             if (mDefaultPackageName != null
509                     && !mDefaultPackageName.equals(nameAndIcon.mPackageName)) {
510                 Log.w(
511                         TAG,
512                         String.format(
513                                 "found different package: %s | %s",
514                                 mDefaultPackageName, nameAndIcon.mPackageName));
515             }
516         }
517     }
518 
519     @Override
toString()520     public String toString() {
521         final StringBuilder builder = new StringBuilder();
522         builder.append("BatteryDiffEntry{");
523         builder.append(
524                 String.format("\n\tname=%s restrictable=%b", mAppLabel, mValidForRestriction));
525         builder.append(
526                 String.format(
527                         "\n\tconsume=%.2f%% %f/%f",
528                         mPercentage, mConsumePower, mTotalConsumePower));
529         builder.append(
530                 String.format(
531                         "\n\tconsume power= foreground:%f foregroundService:%f",
532                         mForegroundUsageConsumePower, mForegroundServiceUsageConsumePower));
533         builder.append(
534                 String.format(
535                         "\n\tconsume power= background:%f cached:%f",
536                         mBackgroundUsageConsumePower, mCachedUsageConsumePower));
537         builder.append(
538                 String.format(
539                         "\n\ttime= foreground:%s foregroundService:%s "
540                                 + "background:%s screen-on:%s",
541                         StringUtil.formatElapsedTime(
542                                 mContext,
543                                 (double) mForegroundUsageTimeInMs,
544                                 /* withSeconds= */ true,
545                                 /* collapseTimeUnit= */ false),
546                         StringUtil.formatElapsedTime(
547                                 mContext,
548                                 (double) mForegroundServiceUsageTimeInMs,
549                                 /* withSeconds= */ true,
550                                 /* collapseTimeUnit= */ false),
551                         StringUtil.formatElapsedTime(
552                                 mContext,
553                                 (double) mBackgroundUsageTimeInMs,
554                                 /* withSeconds= */ true,
555                                 /* collapseTimeUnit= */ false),
556                         StringUtil.formatElapsedTime(
557                                 mContext,
558                                 (double) mScreenOnTimeInMs,
559                                 /* withSeconds= */ true,
560                                 /* collapseTimeUnit= */ false)));
561         builder.append(
562                 String.format(
563                         "\n\tpackage:%s|%s uid:%d userId:%d",
564                         mLegacyPackageName, getPackageName(), mUid, mUserId));
565         return builder.toString();
566     }
567 
568     /** Clears all cache data. */
clearCache()569     public static void clearCache() {
570         synchronized (sResourceCacheLock) {
571             sResourceCache.clear();
572         }
573         synchronized (sValidForRestrictionLock) {
574             sValidForRestriction.clear();
575         }
576         synchronized (sPackageNameAndUidCacheLock) {
577             sPackageNameAndUidCache.clear();
578         }
579     }
580 
putResourceCache(String key, NameAndIcon nameAndIcon)581     private static void putResourceCache(String key, NameAndIcon nameAndIcon) {
582         synchronized (sResourceCacheLock) {
583             sResourceCache.put(key, nameAndIcon);
584         }
585     }
586 
getBadgeIconForUser(Drawable icon)587     private Drawable getBadgeIconForUser(Drawable icon) {
588         final int userId = UserHandle.getUserId((int) mUid);
589         return userId == UserHandle.USER_OWNER
590                 ? icon
591                 : mUserManager.getBadgedIconForUser(icon, new UserHandle(userId));
592     }
593 }
594