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