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