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.util.Log; 26 import android.util.Pair; 27 28 import androidx.annotation.VisibleForTesting; 29 30 import com.android.settings.R; 31 import com.android.settings.fuelgauge.BatteryUtils; 32 import com.android.settingslib.utils.StringUtil; 33 34 import java.util.Comparator; 35 import java.util.HashMap; 36 import java.util.Locale; 37 import java.util.Map; 38 39 /** A container class to carry battery data in a specific time slot. */ 40 public class BatteryDiffEntry { 41 private static final String TAG = "BatteryDiffEntry"; 42 43 static Locale sCurrentLocale = null; 44 // Caches app label and icon to improve loading performance. 45 static final Map<String, BatteryEntry.NameAndIcon> sResourceCache = new HashMap<>(); 46 // Whether a specific item is valid to launch restriction page? 47 @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) 48 static final Map<String, Boolean> sValidForRestriction = new HashMap<>(); 49 /** A comparator for {@link BatteryDiffEntry} based on the sorting key. */ 50 static final Comparator<BatteryDiffEntry> COMPARATOR = 51 (a, b) -> Double.compare(b.getSortingKey(), a.getSortingKey()); 52 static final String SYSTEM_APPS_KEY = "A|SystemApps"; 53 static final String OTHERS_KEY = "S|Others"; 54 55 // key -> (label_id, icon_id) 56 private static final Map<String, Pair<Integer, Integer>> SPECIAL_ENTRY_MAP = Map.of( 57 SYSTEM_APPS_KEY, 58 Pair.create(R.string.battery_usage_system_apps, R.drawable.ic_power_system), 59 OTHERS_KEY, 60 Pair.create(R.string.battery_usage_others, 61 R.drawable.ic_settings_battery_usage_others)); 62 63 public long mUid; 64 public long mUserId; 65 public String mKey; 66 public boolean mIsHidden; 67 public int mComponentId; 68 public String mLegacyPackageName; 69 public String mLegacyLabel; 70 public int mConsumerType; 71 public long mForegroundUsageTimeInMs; 72 public long mBackgroundUsageTimeInMs; 73 public long mScreenOnTimeInMs; 74 public double mConsumePower; 75 public double mForegroundUsageConsumePower; 76 public double mForegroundServiceUsageConsumePower; 77 public double mBackgroundUsageConsumePower; 78 public double mCachedUsageConsumePower; 79 80 protected Context mContext; 81 82 private double mTotalConsumePower; 83 private double mPercentage; 84 private int mAdjustPercentageOffset; 85 private UserManager mUserManager; 86 private String mDefaultPackageName = null; 87 88 @VisibleForTesting 89 int mAppIconId; 90 @VisibleForTesting 91 String mAppLabel = null; 92 @VisibleForTesting 93 Drawable mAppIcon = null; 94 @VisibleForTesting 95 boolean mIsLoaded = false; 96 @VisibleForTesting 97 boolean mValidForRestriction = true; 98 BatteryDiffEntry( Context context, long uid, long userId, String key, boolean isHidden, int componentId, String legacyPackageName, String legacyLabel, int consumerType, long foregroundUsageTimeInMs, long backgroundUsageTimeInMs, long screenOnTimeInMs, double consumePower, double foregroundUsageConsumePower, double foregroundServiceUsageConsumePower, double backgroundUsageConsumePower, double cachedUsageConsumePower)99 public BatteryDiffEntry( 100 Context context, 101 long uid, 102 long userId, 103 String key, 104 boolean isHidden, 105 int componentId, 106 String legacyPackageName, 107 String legacyLabel, 108 int consumerType, 109 long foregroundUsageTimeInMs, 110 long backgroundUsageTimeInMs, 111 long screenOnTimeInMs, 112 double consumePower, 113 double foregroundUsageConsumePower, 114 double foregroundServiceUsageConsumePower, 115 double backgroundUsageConsumePower, 116 double cachedUsageConsumePower) { 117 mContext = context; 118 mUid = uid; 119 mUserId = userId; 120 mKey = key; 121 mIsHidden = isHidden; 122 mComponentId = componentId; 123 mLegacyPackageName = legacyPackageName; 124 mLegacyLabel = legacyLabel; 125 mConsumerType = consumerType; 126 mForegroundUsageTimeInMs = foregroundUsageTimeInMs; 127 mBackgroundUsageTimeInMs = backgroundUsageTimeInMs; 128 mScreenOnTimeInMs = screenOnTimeInMs; 129 mConsumePower = consumePower; 130 mForegroundUsageConsumePower = foregroundUsageConsumePower; 131 mForegroundServiceUsageConsumePower = foregroundServiceUsageConsumePower; 132 mBackgroundUsageConsumePower = backgroundUsageConsumePower; 133 mCachedUsageConsumePower = cachedUsageConsumePower; 134 mUserManager = context.getSystemService(UserManager.class); 135 } 136 BatteryDiffEntry(Context context, String key, String legacyLabel, int consumerType)137 public BatteryDiffEntry(Context context, String key, String legacyLabel, int consumerType) { 138 this(context, /*uid=*/ 0, /*userId=*/ 0, key, /*isHidden=*/ false, /*componentId=*/ -1, 139 /*legacyPackageName=*/ null, legacyLabel, consumerType, 140 /*foregroundUsageTimeInMs=*/ 0, /*backgroundUsageTimeInMs=*/ 0, 141 /*screenOnTimeInMs=*/ 0, /*consumePower=*/ 0, /*foregroundUsageConsumePower=*/ 0, 142 /*foregroundServiceUsageConsumePower=*/ 0, /*backgroundUsageConsumePower=*/ 0, 143 /*cachedUsageConsumePower=*/ 0); 144 } 145 146 /** Sets the total consumed power in a specific time slot. */ setTotalConsumePower(double totalConsumePower)147 public void setTotalConsumePower(double totalConsumePower) { 148 mTotalConsumePower = totalConsumePower; 149 mPercentage = totalConsumePower == 0 150 ? 0 : (mConsumePower / mTotalConsumePower) * 100.0; 151 mAdjustPercentageOffset = 0; 152 } 153 154 /** Gets the total consumed power in a specific time slot. */ getTotalConsumePower()155 public double getTotalConsumePower() { 156 return mTotalConsumePower; 157 } 158 159 /** Gets the percentage of total consumed power. */ getPercentage()160 public double getPercentage() { 161 return mPercentage; 162 } 163 164 /** Gets the percentage offset to adjust. */ getAdjustPercentageOffset()165 public double getAdjustPercentageOffset() { 166 return mAdjustPercentageOffset; 167 } 168 169 /** Sets the percentage offset to adjust. */ setAdjustPercentageOffset(int offset)170 public void setAdjustPercentageOffset(int offset) { 171 mAdjustPercentageOffset = offset; 172 } 173 174 /** Gets the key for sorting */ getSortingKey()175 public double getSortingKey() { 176 return getKey() != null && SPECIAL_ENTRY_MAP.containsKey(getKey()) 177 ? -1 : getPercentage() + getAdjustPercentageOffset(); 178 } 179 180 /** Clones a new instance. */ clone()181 public BatteryDiffEntry clone() { 182 return new BatteryDiffEntry( 183 this.mContext, 184 this.mUid, 185 this.mUserId, 186 this.mKey, 187 this.mIsHidden, 188 this.mComponentId, 189 this.mLegacyPackageName, 190 this.mLegacyLabel, 191 this.mConsumerType, 192 this.mForegroundUsageTimeInMs, 193 this.mBackgroundUsageTimeInMs, 194 this.mScreenOnTimeInMs, 195 this.mConsumePower, 196 this.mForegroundUsageConsumePower, 197 this.mForegroundServiceUsageConsumePower, 198 this.mBackgroundUsageConsumePower, 199 this.mCachedUsageConsumePower); 200 } 201 202 /** Gets the app label name for this entry. */ getAppLabel()203 public String getAppLabel() { 204 loadLabelAndIcon(); 205 // Returns default application label if we cannot find it. 206 return mAppLabel == null || mAppLabel.length() == 0 ? mLegacyLabel : mAppLabel; 207 } 208 209 /** Gets the app icon {@link Drawable} for this entry. */ getAppIcon()210 public Drawable getAppIcon() { 211 loadLabelAndIcon(); 212 return mAppIcon != null && mAppIcon.getConstantState() != null 213 ? mAppIcon.getConstantState().newDrawable() 214 : null; 215 } 216 217 /** Gets the app icon id for this entry. */ getAppIconId()218 public int getAppIconId() { 219 loadLabelAndIcon(); 220 return mAppIconId; 221 } 222 223 /** Gets the searching package name for UID battery type. */ getPackageName()224 public String getPackageName() { 225 final String packageName = mDefaultPackageName != null 226 ? mDefaultPackageName : mLegacyPackageName; 227 if (packageName == null) { 228 return packageName; 229 } 230 // Removes potential appended process name in the PackageName. 231 // From "com.opera.browser:privileged_process0" to "com.opera.browser" 232 final String[] splitPackageNames = packageName.split(":"); 233 return splitPackageNames != null && splitPackageNames.length > 0 234 ? splitPackageNames[0] : packageName; 235 } 236 237 /** Whether this item is valid for users to launch restriction page? */ validForRestriction()238 public boolean validForRestriction() { 239 loadLabelAndIcon(); 240 return mValidForRestriction; 241 } 242 243 /** Whether the current BatteryDiffEntry is system component or not. */ isSystemEntry()244 public boolean isSystemEntry() { 245 if (mIsHidden) { 246 return false; 247 } 248 switch (mConsumerType) { 249 case ConvertUtils.CONSUMER_TYPE_USER_BATTERY: 250 case ConvertUtils.CONSUMER_TYPE_SYSTEM_BATTERY: 251 return true; 252 case ConvertUtils.CONSUMER_TYPE_UID_BATTERY: 253 default: 254 return false; 255 } 256 } 257 loadLabelAndIcon()258 void loadLabelAndIcon() { 259 if (mIsLoaded) { 260 return; 261 } 262 // Checks whether we have cached data or not first before fetching. 263 final BatteryEntry.NameAndIcon nameAndIcon = getCache(); 264 if (nameAndIcon != null) { 265 mAppLabel = nameAndIcon.mName; 266 mAppIcon = nameAndIcon.mIcon; 267 mAppIconId = nameAndIcon.mIconId; 268 } 269 final Boolean validForRestriction = sValidForRestriction.get(getKey()); 270 if (validForRestriction != null) { 271 mValidForRestriction = validForRestriction; 272 } 273 // Both nameAndIcon and restriction configuration have cached data. 274 if (nameAndIcon != null && validForRestriction != null) { 275 return; 276 } 277 mIsLoaded = true; 278 279 // Configures whether we can launch restriction page or not. 280 updateRestrictionFlagState(); 281 sValidForRestriction.put(getKey(), Boolean.valueOf(mValidForRestriction)); 282 283 if (getKey() != null && SPECIAL_ENTRY_MAP.containsKey(getKey())) { 284 Pair<Integer, Integer> pair = SPECIAL_ENTRY_MAP.get(getKey()); 285 mAppLabel = mContext.getString(pair.first); 286 mAppIconId = pair.second; 287 mAppIcon = mContext.getDrawable(mAppIconId); 288 sResourceCache.put( 289 getKey(), 290 new BatteryEntry.NameAndIcon(mAppLabel, mAppIcon, mAppIconId)); 291 return; 292 } 293 294 // Loads application icon and label based on consumer type. 295 switch (mConsumerType) { 296 case ConvertUtils.CONSUMER_TYPE_USER_BATTERY: 297 final BatteryEntry.NameAndIcon nameAndIconForUser = 298 BatteryEntry.getNameAndIconFromUserId(mContext, (int) mUserId); 299 if (nameAndIconForUser != null) { 300 mAppIcon = nameAndIconForUser.mIcon; 301 mAppLabel = nameAndIconForUser.mName; 302 sResourceCache.put( 303 getKey(), 304 new BatteryEntry.NameAndIcon(mAppLabel, mAppIcon, /*iconId=*/ 0)); 305 } 306 break; 307 case ConvertUtils.CONSUMER_TYPE_SYSTEM_BATTERY: 308 final BatteryEntry.NameAndIcon nameAndIconForSystem = 309 BatteryEntry.getNameAndIconFromPowerComponent(mContext, mComponentId); 310 if (nameAndIconForSystem != null) { 311 mAppLabel = nameAndIconForSystem.mName; 312 if (nameAndIconForSystem.mIconId != 0) { 313 mAppIconId = nameAndIconForSystem.mIconId; 314 mAppIcon = mContext.getDrawable(nameAndIconForSystem.mIconId); 315 } 316 sResourceCache.put( 317 getKey(), 318 new BatteryEntry.NameAndIcon(mAppLabel, mAppIcon, mAppIconId)); 319 } 320 break; 321 case ConvertUtils.CONSUMER_TYPE_UID_BATTERY: 322 loadNameAndIconForUid(); 323 // Uses application default icon if we cannot find it from package. 324 if (mAppIcon == null) { 325 mAppIcon = mContext.getPackageManager().getDefaultActivityIcon(); 326 } 327 // Adds badge icon into app icon for work profile. 328 mAppIcon = getBadgeIconForUser(mAppIcon); 329 if (mAppLabel != null || mAppIcon != null) { 330 sResourceCache.put( 331 getKey(), 332 new BatteryEntry.NameAndIcon(mAppLabel, mAppIcon, /*iconId=*/ 0)); 333 } 334 break; 335 } 336 } 337 getKey()338 String getKey() { 339 return mKey; 340 } 341 342 @VisibleForTesting updateRestrictionFlagState()343 void updateRestrictionFlagState() { 344 if (isSystemEntry()) { 345 mValidForRestriction = false; 346 return; 347 } 348 final boolean isValidPackage = 349 BatteryUtils.getInstance(mContext).getPackageUid(getPackageName()) 350 != BatteryUtils.UID_NULL; 351 if (!isValidPackage) { 352 mValidForRestriction = false; 353 return; 354 } 355 try { 356 mValidForRestriction = 357 mContext.getPackageManager().getPackageInfo( 358 getPackageName(), 359 PackageManager.MATCH_DISABLED_COMPONENTS 360 | PackageManager.MATCH_ANY_USER 361 | PackageManager.GET_SIGNATURES 362 | PackageManager.GET_PERMISSIONS) 363 != null; 364 } catch (Exception e) { 365 Log.e(TAG, String.format("getPackageInfo() error %s for package=%s", 366 e.getCause(), getPackageName())); 367 mValidForRestriction = false; 368 } 369 } 370 getCache()371 private BatteryEntry.NameAndIcon getCache() { 372 final Locale locale = Locale.getDefault(); 373 if (sCurrentLocale != locale) { 374 Log.d(TAG, String.format("clearCache() locale is changed from %s to %s", 375 sCurrentLocale, locale)); 376 sCurrentLocale = locale; 377 clearCache(); 378 } 379 return sResourceCache.get(getKey()); 380 } 381 loadNameAndIconForUid()382 private void loadNameAndIconForUid() { 383 final String packageName = getPackageName(); 384 final PackageManager packageManager = mContext.getPackageManager(); 385 // Gets the application label from PackageManager. 386 if (packageName != null && packageName.length() != 0) { 387 try { 388 final ApplicationInfo appInfo = 389 packageManager.getApplicationInfo(packageName, /*no flags*/ 0); 390 if (appInfo != null) { 391 mAppLabel = packageManager.getApplicationLabel(appInfo).toString(); 392 mAppIcon = packageManager.getApplicationIcon(appInfo); 393 } 394 } catch (NameNotFoundException e) { 395 Log.e(TAG, "failed to retrieve ApplicationInfo for: " + packageName); 396 mAppLabel = packageName; 397 } 398 } 399 // Early return if we found the app label and icon resource. 400 if (mAppLabel != null && mAppIcon != null) { 401 return; 402 } 403 404 final int uid = (int) mUid; 405 final String[] packages = packageManager.getPackagesForUid(uid); 406 // Loads special defined application label and icon if available. 407 if (packages == null || packages.length == 0) { 408 final BatteryEntry.NameAndIcon nameAndIcon = 409 BatteryEntry.getNameAndIconFromUid(mContext, mAppLabel, uid); 410 mAppLabel = nameAndIcon.mName; 411 mAppIcon = nameAndIcon.mIcon; 412 } 413 414 final BatteryEntry.NameAndIcon nameAndIcon = BatteryEntry.loadNameAndIcon( 415 mContext, uid, /*batteryEntry=*/ null, packageName, mAppLabel, mAppIcon); 416 // Clears BatteryEntry internal cache since we will have another one. 417 BatteryEntry.clearUidCache(); 418 if (nameAndIcon != null) { 419 mAppLabel = nameAndIcon.mName; 420 mAppIcon = nameAndIcon.mIcon; 421 mDefaultPackageName = nameAndIcon.mPackageName; 422 if (mDefaultPackageName != null 423 && !mDefaultPackageName.equals(nameAndIcon.mPackageName)) { 424 Log.w(TAG, String.format("found different package: %s | %s", 425 mDefaultPackageName, nameAndIcon.mPackageName)); 426 } 427 } 428 } 429 430 @Override toString()431 public String toString() { 432 final StringBuilder builder = new StringBuilder() 433 .append("BatteryDiffEntry{") 434 .append(String.format("\n\tname=%s restrictable=%b", 435 mAppLabel, mValidForRestriction)) 436 .append(String.format("\n\tconsume=%.2f%% %f/%f", 437 mPercentage, mConsumePower, mTotalConsumePower)) 438 .append(String.format("\n\tconsume power= foreground:%f foregroundService:%f", 439 mForegroundUsageConsumePower, mForegroundServiceUsageConsumePower)) 440 .append(String.format("\n\tconsume power= background:%f cached:%f", 441 mBackgroundUsageConsumePower, mCachedUsageConsumePower)) 442 .append(String.format("\n\ttime= foreground:%s background:%s screen-on:%s", 443 StringUtil.formatElapsedTime(mContext, (double) mForegroundUsageTimeInMs, 444 /*withSeconds=*/ true, /*collapseTimeUnit=*/ false), 445 StringUtil.formatElapsedTime(mContext, (double) mBackgroundUsageTimeInMs, 446 /*withSeconds=*/ true, /*collapseTimeUnit=*/ false), 447 StringUtil.formatElapsedTime(mContext, (double) mScreenOnTimeInMs, 448 /*withSeconds=*/ true, /*collapseTimeUnit=*/ false))) 449 .append(String.format("\n\tpackage:%s|%s uid:%d userId:%d", 450 mLegacyPackageName, getPackageName(), mUid, mUserId)); 451 return builder.toString(); 452 } 453 454 /** Clears app icon and label cache data. */ clearCache()455 public static void clearCache() { 456 sResourceCache.clear(); 457 sValidForRestriction.clear(); 458 } 459 getBadgeIconForUser(Drawable icon)460 private Drawable getBadgeIconForUser(Drawable icon) { 461 final int userId = UserHandle.getUserId((int) mUid); 462 return userId == UserHandle.USER_OWNER ? icon : 463 mUserManager.getBadgedIconForUser(icon, new UserHandle(userId)); 464 } 465 } 466