1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
5  * except in compliance with the License. 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 distributed under the
10  * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
11  * KIND, either express or implied. See the License for the specific language governing
12  * permissions and limitations under the License.
13  */
14 package com.android.settings.fuelgauge;
15 
16 import android.content.Context;
17 import android.content.pm.ApplicationInfo;
18 import android.content.pm.PackageManager;
19 import android.content.pm.PackageManager.NameNotFoundException;
20 import android.graphics.drawable.Drawable;
21 import android.os.Process;
22 import android.os.UserHandle;
23 import android.os.UserManager;
24 import android.util.Log;
25 
26 import androidx.annotation.VisibleForTesting;
27 
28 import com.android.settingslib.utils.StringUtil;
29 
30 import java.util.Comparator;
31 import java.util.HashMap;
32 import java.util.Locale;
33 import java.util.Map;
34 
35 /** A container class to carry battery data in a specific time slot. */
36 public class BatteryDiffEntry {
37     private static final String TAG = "BatteryDiffEntry";
38 
39     static Locale sCurrentLocale = null;
40     // Caches app label and icon to improve loading performance.
41     static final Map<String, BatteryEntry.NameAndIcon> sResourceCache = new HashMap<>();
42     // Whether a specific item is valid to launch restriction page?
43     @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
44     public static final Map<String, Boolean> sValidForRestriction = new HashMap<>();
45 
46     /** A comparator for {@link BatteryDiffEntry} based on consumed percentage. */
47     public static final Comparator<BatteryDiffEntry> COMPARATOR =
48             (a, b) -> Double.compare(b.getPercentOfTotal(), a.getPercentOfTotal());
49 
50     public long mForegroundUsageTimeInMs;
51     public long mBackgroundUsageTimeInMs;
52     public double mConsumePower;
53     // A BatteryHistEntry corresponding to this diff usage data.
54     public final BatteryHistEntry mBatteryHistEntry;
55     private double mTotalConsumePower;
56     private double mPercentOfTotal;
57 
58     private Context mContext;
59     private UserManager mUserManager;
60     private String mDefaultPackageName = null;
61 
62     @VisibleForTesting int mAppIconId;
63     @VisibleForTesting String mAppLabel = null;
64     @VisibleForTesting Drawable mAppIcon = null;
65     @VisibleForTesting boolean mIsLoaded = false;
66     @VisibleForTesting boolean mValidForRestriction = true;
67 
BatteryDiffEntry( Context context, long foregroundUsageTimeInMs, long backgroundUsageTimeInMs, double consumePower, BatteryHistEntry batteryHistEntry)68     public BatteryDiffEntry(
69             Context context,
70             long foregroundUsageTimeInMs,
71             long backgroundUsageTimeInMs,
72             double consumePower,
73             BatteryHistEntry batteryHistEntry) {
74         mContext = context;
75         mConsumePower = consumePower;
76         mForegroundUsageTimeInMs = foregroundUsageTimeInMs;
77         mBackgroundUsageTimeInMs = backgroundUsageTimeInMs;
78         mBatteryHistEntry = batteryHistEntry;
79         mUserManager = context.getSystemService(UserManager.class);
80     }
81 
82     /** Sets the total consumed power in a specific time slot. */
setTotalConsumePower(double totalConsumePower)83     public void setTotalConsumePower(double totalConsumePower) {
84         mTotalConsumePower = totalConsumePower;
85         mPercentOfTotal = totalConsumePower == 0
86             ? 0 : (mConsumePower / mTotalConsumePower) * 100.0;
87     }
88 
89     /** Gets the percentage of total consumed power. */
getPercentOfTotal()90     public double getPercentOfTotal() {
91         return mPercentOfTotal;
92     }
93 
94     /** Clones a new instance. */
clone()95     public BatteryDiffEntry clone() {
96         return new BatteryDiffEntry(
97             this.mContext,
98             this.mForegroundUsageTimeInMs,
99             this.mBackgroundUsageTimeInMs,
100             this.mConsumePower,
101             this.mBatteryHistEntry /*same instance*/);
102     }
103 
104     /** Gets the app label name for this entry. */
getAppLabel()105     public String getAppLabel() {
106         loadLabelAndIcon();
107         // Returns default applicationn label if we cannot find it.
108         return mAppLabel == null || mAppLabel.length() == 0
109             ? mBatteryHistEntry.mAppLabel
110             : mAppLabel;
111     }
112 
113     /** Gets the app icon {@link Drawable} for this entry. */
getAppIcon()114     public Drawable getAppIcon() {
115         loadLabelAndIcon();
116         return mAppIcon != null && mAppIcon.getConstantState() != null
117                 ? mAppIcon.getConstantState().newDrawable()
118                 : null;
119     }
120 
121     /** Gets the app icon id for this entry. */
getAppIconId()122     public int getAppIconId() {
123         loadLabelAndIcon();
124         return mAppIconId;
125     }
126 
127     /** Gets the searching package name for UID battery type. */
getPackageName()128     public String getPackageName() {
129         final String packageName = mDefaultPackageName != null
130             ? mDefaultPackageName : mBatteryHistEntry.mPackageName;
131         if (packageName == null) {
132             return packageName;
133         }
134         // Removes potential appended process name in the PackageName.
135         // From "com.opera.browser:privileged_process0" to "com.opera.browser"
136         final String[] splittedPackageNames = packageName.split(":");
137         return splittedPackageNames != null && splittedPackageNames.length > 0
138             ? splittedPackageNames[0] : packageName;
139     }
140 
141     /** Whether this item is valid for users to launch restriction page? */
validForRestriction()142     public boolean validForRestriction() {
143         loadLabelAndIcon();
144         return mValidForRestriction;
145     }
146 
147     /** Whether the current BatteryDiffEntry is system component or not. */
isSystemEntry()148     public boolean isSystemEntry() {
149         switch (mBatteryHistEntry.mConsumerType) {
150             case ConvertUtils.CONSUMER_TYPE_USER_BATTERY:
151             case ConvertUtils.CONSUMER_TYPE_SYSTEM_BATTERY:
152                 return true;
153             case ConvertUtils.CONSUMER_TYPE_UID_BATTERY:
154                 return isSystemUid((int) mBatteryHistEntry.mUid)
155                     || mBatteryHistEntry.mIsHidden;
156         }
157         return false;
158     }
159 
loadLabelAndIcon()160     void loadLabelAndIcon() {
161         if (mIsLoaded) {
162             return;
163         }
164         // Checks whether we have cached data or not first before fetching.
165         final BatteryEntry.NameAndIcon nameAndIcon = getCache();
166         if (nameAndIcon != null) {
167             mAppLabel = nameAndIcon.name;
168             mAppIcon = nameAndIcon.icon;
169             mAppIconId = nameAndIcon.iconId;
170         }
171         final Boolean validForRestriction = sValidForRestriction.get(getKey());
172         if (validForRestriction != null) {
173             mValidForRestriction = validForRestriction;
174         }
175         // Both nameAndIcon and restriction configuration have cached data.
176         if (nameAndIcon != null && validForRestriction != null) {
177             return;
178         }
179         mIsLoaded = true;
180 
181         // Configures whether we can launch restriction page or not.
182         updateRestrictionFlagState();
183         sValidForRestriction.put(getKey(), Boolean.valueOf(mValidForRestriction));
184 
185         // Loads application icon and label based on consumer type.
186         switch (mBatteryHistEntry.mConsumerType) {
187             case ConvertUtils.CONSUMER_TYPE_USER_BATTERY:
188                 final BatteryEntry.NameAndIcon nameAndIconForUser =
189                     BatteryEntry.getNameAndIconFromUserId(
190                         mContext, (int) mBatteryHistEntry.mUserId);
191                 if (nameAndIconForUser != null) {
192                     mAppIcon = nameAndIconForUser.icon;
193                     mAppLabel = nameAndIconForUser.name;
194                     sResourceCache.put(
195                         getKey(),
196                         new BatteryEntry.NameAndIcon(mAppLabel, mAppIcon, /*iconId=*/ 0));
197                 }
198                 break;
199             case ConvertUtils.CONSUMER_TYPE_SYSTEM_BATTERY:
200                 final BatteryEntry.NameAndIcon nameAndIconForSystem =
201                     BatteryEntry.getNameAndIconFromPowerComponent(
202                         mContext, mBatteryHistEntry.mDrainType);
203                 if (nameAndIconForSystem != null) {
204                     mAppLabel = nameAndIconForSystem.name;
205                     if (nameAndIconForSystem.iconId != 0) {
206                         mAppIconId = nameAndIconForSystem.iconId;
207                         mAppIcon = mContext.getDrawable(nameAndIconForSystem.iconId);
208                     }
209                     sResourceCache.put(
210                         getKey(),
211                         new BatteryEntry.NameAndIcon(mAppLabel, mAppIcon, mAppIconId));
212                 }
213                 break;
214             case ConvertUtils.CONSUMER_TYPE_UID_BATTERY:
215                 loadNameAndIconForUid();
216                 // Uses application default icon if we cannot find it from package.
217                 if (mAppIcon == null) {
218                     mAppIcon = mContext.getPackageManager().getDefaultActivityIcon();
219                 }
220                 // Adds badge icon into app icon for work profile.
221                 mAppIcon = getBadgeIconForUser(mAppIcon);
222                 if (mAppLabel != null || mAppIcon != null) {
223                     sResourceCache.put(
224                         getKey(),
225                         new BatteryEntry.NameAndIcon(mAppLabel, mAppIcon, /*iconId=*/ 0));
226                 }
227                 break;
228         }
229     }
230 
231     @VisibleForTesting
getKey()232     String getKey() {
233         return mBatteryHistEntry.getKey();
234     }
235 
236     @VisibleForTesting
updateRestrictionFlagState()237     void updateRestrictionFlagState() {
238         mValidForRestriction = true;
239         if (!mBatteryHistEntry.isAppEntry()) {
240             return;
241         }
242         final boolean isValidPackage =
243                 BatteryUtils.getInstance(mContext).getPackageUid(getPackageName())
244             != BatteryUtils.UID_NULL;
245         if (!isValidPackage) {
246             mValidForRestriction = false;
247             return;
248         }
249         try {
250             mValidForRestriction =
251                 mContext.getPackageManager().getPackageInfo(
252                     getPackageName(),
253                     PackageManager.MATCH_DISABLED_COMPONENTS
254                         | PackageManager.MATCH_ANY_USER
255                         | PackageManager.GET_SIGNATURES
256                         | PackageManager.GET_PERMISSIONS)
257                 != null;
258         } catch (Exception e) {
259             Log.e(TAG, String.format("getPackageInfo() error %s for package=%s",
260                 e.getCause(), getPackageName()));
261             mValidForRestriction = false;
262         }
263     }
264 
getCache()265     private BatteryEntry.NameAndIcon getCache() {
266         final Locale locale = Locale.getDefault();
267         if (sCurrentLocale != locale) {
268             Log.d(TAG, String.format("clearCache() locale is changed from %s to %s",
269                 sCurrentLocale, locale));
270             sCurrentLocale = locale;
271             clearCache();
272         }
273         return sResourceCache.get(getKey());
274     }
275 
loadNameAndIconForUid()276     private void loadNameAndIconForUid() {
277         final String packageName = getPackageName();
278         final PackageManager packageManager = mContext.getPackageManager();
279         // Gets the application label from PackageManager.
280         if (packageName != null && packageName.length() != 0) {
281             try {
282                 final ApplicationInfo appInfo =
283                     packageManager.getApplicationInfo(packageName, /*no flags*/ 0);
284                 if (appInfo != null) {
285                     mAppLabel = packageManager.getApplicationLabel(appInfo).toString();
286                     mAppIcon = packageManager.getApplicationIcon(appInfo);
287                 }
288             } catch (NameNotFoundException e) {
289                 Log.e(TAG, "failed to retrieve ApplicationInfo for: " + packageName);
290                 mAppLabel = packageName;
291             }
292         }
293         // Early return if we found the app label and icon resource.
294         if (mAppLabel != null && mAppIcon != null) {
295             return;
296         }
297 
298         final int uid = (int) mBatteryHistEntry.mUid;
299         final String[] packages = packageManager.getPackagesForUid(uid);
300         // Loads special defined application label and icon if available.
301         if (packages == null || packages.length == 0) {
302             final BatteryEntry.NameAndIcon nameAndIcon =
303                 BatteryEntry.getNameAndIconFromUid(mContext, mAppLabel, uid);
304             mAppLabel = nameAndIcon.name;
305             mAppIcon = nameAndIcon.icon;
306         }
307 
308         final BatteryEntry.NameAndIcon nameAndIcon =
309             BatteryEntry.loadNameAndIcon(
310                 mContext, uid, /*handler=*/ null, /*batteryEntry=*/ null,
311                 packageName, mAppLabel, mAppIcon);
312         // Clears BatteryEntry internal cache since we will have another one.
313         BatteryEntry.clearUidCache();
314         if (nameAndIcon != null) {
315             mAppLabel = nameAndIcon.name;
316             mAppIcon = nameAndIcon.icon;
317             mDefaultPackageName = nameAndIcon.packageName;
318             if (mDefaultPackageName != null
319                     && !mDefaultPackageName.equals(nameAndIcon.packageName)) {
320                 Log.w(TAG, String.format("found different package: %s | %s",
321                     mDefaultPackageName, nameAndIcon.packageName));
322             }
323         }
324     }
325 
326     @Override
toString()327     public String toString() {
328         final StringBuilder builder = new StringBuilder()
329             .append("BatteryDiffEntry{")
330             .append(String.format("\n\tname=%s restrictable=%b",
331                   mAppLabel, mValidForRestriction))
332             .append(String.format("\n\tconsume=%.2f%% %f/%f",
333                   mPercentOfTotal, mConsumePower, mTotalConsumePower))
334             .append(String.format("\n\tforeground:%s background:%s",
335                   StringUtil.formatElapsedTime(mContext, mForegroundUsageTimeInMs,
336                       /*withSeconds=*/ true, /*collapseTimeUnit=*/ false),
337                   StringUtil.formatElapsedTime(mContext, mBackgroundUsageTimeInMs,
338                       /*withSeconds=*/ true, /*collapseTimeUnit=*/ false)))
339             .append(String.format("\n\tpackage:%s|%s uid:%d userId:%d",
340                   mBatteryHistEntry.mPackageName, getPackageName(),
341                   mBatteryHistEntry.mUid, mBatteryHistEntry.mUserId));
342         return builder.toString();
343     }
344 
345     /** Clears app icon and label cache data. */
clearCache()346     public static void clearCache() {
347         sResourceCache.clear();
348         sValidForRestriction.clear();
349     }
350 
getBadgeIconForUser(Drawable icon)351     private Drawable getBadgeIconForUser(Drawable icon) {
352         final int userId = UserHandle.getUserId((int) mBatteryHistEntry.mUid);
353         return userId == UserHandle.USER_OWNER ? icon :
354             mUserManager.getBadgedIconForUser(icon, new UserHandle(userId));
355     }
356 
isSystemUid(int uid)357     private static boolean isSystemUid(int uid) {
358         final int appUid = UserHandle.getAppId(uid);
359         return appUid >= Process.SYSTEM_UID && appUid < Process.FIRST_APPLICATION_UID;
360     }
361 }
362