1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * 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 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 * 15 * 16 */ 17 18 package com.android.settings.fuelgauge; 19 20 import android.app.Activity; 21 import android.content.Context; 22 import android.content.pm.PackageManager; 23 import android.graphics.drawable.Drawable; 24 import android.os.AggregateBatteryConsumer; 25 import android.os.BatteryConsumer; 26 import android.os.BatteryUsageStats; 27 import android.os.Handler; 28 import android.os.Looper; 29 import android.os.Message; 30 import android.os.Process; 31 import android.os.UidBatteryConsumer; 32 import android.os.UserBatteryConsumer; 33 import android.os.UserHandle; 34 import android.os.UserManager; 35 import android.text.TextUtils; 36 import android.text.format.DateUtils; 37 import android.util.ArrayMap; 38 import android.util.SparseArray; 39 40 import androidx.annotation.VisibleForTesting; 41 import androidx.preference.Preference; 42 import androidx.preference.PreferenceGroup; 43 import androidx.preference.PreferenceScreen; 44 45 import com.android.internal.os.PowerProfile; 46 import com.android.settings.R; 47 import com.android.settings.SettingsActivity; 48 import com.android.settings.core.InstrumentedPreferenceFragment; 49 import com.android.settings.core.PreferenceControllerMixin; 50 import com.android.settings.overlay.FeatureFactory; 51 import com.android.settingslib.core.AbstractPreferenceController; 52 import com.android.settingslib.core.lifecycle.Lifecycle; 53 import com.android.settingslib.core.lifecycle.LifecycleObserver; 54 import com.android.settingslib.core.lifecycle.events.OnDestroy; 55 import com.android.settingslib.core.lifecycle.events.OnPause; 56 import com.android.settingslib.utils.StringUtil; 57 58 import java.util.ArrayList; 59 import java.util.Comparator; 60 import java.util.List; 61 62 /** 63 * Controller that update the battery header view 64 */ 65 public class BatteryAppListPreferenceController extends AbstractPreferenceController 66 implements PreferenceControllerMixin, LifecycleObserver, OnPause, OnDestroy { 67 @VisibleForTesting 68 static final boolean USE_FAKE_DATA = false; 69 private static final int MAX_ITEMS_TO_LIST = USE_FAKE_DATA ? 30 : 20; 70 private static final int MIN_AVERAGE_POWER_THRESHOLD_MILLI_AMP = 10; 71 private static final String MEDIASERVER_PACKAGE_NAME = "mediaserver"; 72 73 private final String mPreferenceKey; 74 @VisibleForTesting 75 PreferenceGroup mAppListGroup; 76 private BatteryUsageStats mBatteryUsageStats; 77 private ArrayMap<String, Preference> mPreferenceCache; 78 @VisibleForTesting 79 BatteryUtils mBatteryUtils; 80 private final UserManager mUserManager; 81 private final PackageManager mPackageManager; 82 private final SettingsActivity mActivity; 83 private final InstrumentedPreferenceFragment mFragment; 84 private Context mPrefContext; 85 86 /** 87 * Battery attribution list configuration. 88 */ 89 public interface Config { 90 /** 91 * Returns true if the attribution list should be shown. 92 */ shouldShowBatteryAttributionList(Context context)93 boolean shouldShowBatteryAttributionList(Context context); 94 } 95 96 @VisibleForTesting 97 static Config sConfig = new Config() { 98 @Override 99 public boolean shouldShowBatteryAttributionList(Context context) { 100 if (USE_FAKE_DATA) { 101 return true; 102 } 103 104 PowerProfile powerProfile = new PowerProfile(context); 105 // Cheap hack to try to figure out if the power_profile.xml was populated. 106 return powerProfile.getAveragePowerForOrdinal( 107 PowerProfile.POWER_GROUP_DISPLAY_SCREEN_FULL, 0) 108 >= MIN_AVERAGE_POWER_THRESHOLD_MILLI_AMP; 109 } 110 }; 111 112 private final Handler mHandler = new Handler(Looper.getMainLooper()) { 113 @Override 114 public void handleMessage(Message msg) { 115 switch (msg.what) { 116 case BatteryEntry.MSG_UPDATE_NAME_ICON: 117 BatteryEntry entry = (BatteryEntry) msg.obj; 118 PowerGaugePreference pgp = mAppListGroup.findPreference(entry.getKey()); 119 if (pgp != null) { 120 final int userId = UserHandle.getUserId(entry.getUid()); 121 final UserHandle userHandle = new UserHandle(userId); 122 pgp.setIcon(mUserManager.getBadgedIconForUser(entry.getIcon(), userHandle)); 123 pgp.setTitle(entry.name); 124 if (entry.isAppEntry()) { 125 pgp.setContentDescription(entry.name); 126 } 127 } 128 break; 129 case BatteryEntry.MSG_REPORT_FULLY_DRAWN: 130 Activity activity = mActivity; 131 if (activity != null) { 132 activity.reportFullyDrawn(); 133 } 134 break; 135 } 136 super.handleMessage(msg); 137 } 138 }; 139 BatteryAppListPreferenceController(Context context, String preferenceKey, Lifecycle lifecycle, SettingsActivity activity, InstrumentedPreferenceFragment fragment)140 public BatteryAppListPreferenceController(Context context, String preferenceKey, 141 Lifecycle lifecycle, SettingsActivity activity, 142 InstrumentedPreferenceFragment fragment) { 143 super(context); 144 145 if (lifecycle != null) { 146 lifecycle.addObserver(this); 147 } 148 149 mPreferenceKey = preferenceKey; 150 mBatteryUtils = BatteryUtils.getInstance(context); 151 mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE); 152 mPackageManager = context.getPackageManager(); 153 mActivity = activity; 154 mFragment = fragment; 155 } 156 157 @Override onPause()158 public void onPause() { 159 BatteryEntry.stopRequestQueue(); 160 mHandler.removeMessages(BatteryEntry.MSG_UPDATE_NAME_ICON); 161 } 162 163 @Override onDestroy()164 public void onDestroy() { 165 if (mActivity.isChangingConfigurations()) { 166 BatteryEntry.clearUidCache(); 167 } 168 } 169 170 @Override displayPreference(PreferenceScreen screen)171 public void displayPreference(PreferenceScreen screen) { 172 super.displayPreference(screen); 173 mPrefContext = screen.getContext(); 174 mAppListGroup = screen.findPreference(mPreferenceKey); 175 mAppListGroup.setTitle(mPrefContext.getString(R.string.power_usage_list_summary)); 176 } 177 178 @Override isAvailable()179 public boolean isAvailable() { 180 return true; 181 } 182 183 @Override getPreferenceKey()184 public String getPreferenceKey() { 185 return mPreferenceKey; 186 } 187 188 @Override handlePreferenceTreeClick(Preference preference)189 public boolean handlePreferenceTreeClick(Preference preference) { 190 if (preference instanceof PowerGaugePreference) { 191 PowerGaugePreference pgp = (PowerGaugePreference) preference; 192 BatteryEntry entry = pgp.getInfo(); 193 AdvancedPowerUsageDetail.startBatteryDetailPage(mActivity, 194 mFragment, entry, pgp.getPercent(), /*isValidToShowSummary=*/ true); 195 return true; 196 } 197 return false; 198 } 199 200 /** 201 * Refreshes the list of battery consumers using the supplied BatteryUsageStats. 202 */ refreshAppListGroup(BatteryUsageStats batteryUsageStats, boolean showAllApps)203 public void refreshAppListGroup(BatteryUsageStats batteryUsageStats, boolean showAllApps) { 204 if (!isAvailable()) { 205 return; 206 } 207 208 mBatteryUsageStats = USE_FAKE_DATA ? getFakeStats() : batteryUsageStats; 209 mAppListGroup.setTitle(R.string.power_usage_list_summary); 210 211 boolean addedSome = false; 212 213 cacheRemoveAllPrefs(mAppListGroup); 214 mAppListGroup.setOrderingAsAdded(false); 215 216 if (sConfig.shouldShowBatteryAttributionList(mContext)) { 217 final int dischargePercentage = getDischargePercentage(batteryUsageStats); 218 final List<BatteryEntry> usageList = 219 getCoalescedUsageList(showAllApps, /*loadDataInBackground=*/ true); 220 final double totalPower = batteryUsageStats.getConsumedPower(); 221 final int numSippers = usageList.size(); 222 for (int i = 0; i < numSippers; i++) { 223 final BatteryEntry entry = usageList.get(i); 224 225 final double percentOfTotal = mBatteryUtils.calculateBatteryPercent( 226 entry.getConsumedPower(), totalPower, dischargePercentage); 227 228 if (((int) (percentOfTotal + .5)) < 1) { 229 continue; 230 } 231 232 final UserHandle userHandle = new UserHandle(UserHandle.getUserId(entry.getUid())); 233 final Drawable badgedIcon = mUserManager.getBadgedIconForUser(entry.getIcon(), 234 userHandle); 235 final CharSequence contentDescription = mUserManager.getBadgedLabelForUser( 236 entry.getLabel(), userHandle); 237 238 final String key = entry.getKey(); 239 PowerGaugePreference pref = (PowerGaugePreference) getCachedPreference(key); 240 if (pref == null) { 241 pref = new PowerGaugePreference(mPrefContext, badgedIcon, 242 contentDescription, entry); 243 pref.setKey(key); 244 } 245 entry.percent = percentOfTotal; 246 pref.setTitle(entry.getLabel()); 247 pref.setOrder(i + 1); 248 pref.setPercent(percentOfTotal); 249 pref.shouldShowAnomalyIcon(false); 250 setUsageSummary(pref, entry); 251 addedSome = true; 252 mAppListGroup.addPreference(pref); 253 if (mAppListGroup.getPreferenceCount() - getCachedCount() 254 > (MAX_ITEMS_TO_LIST + 1)) { 255 break; 256 } 257 } 258 } 259 if (!addedSome) { 260 addNotAvailableMessage(); 261 } 262 removeCachedPrefs(mAppListGroup); 263 264 BatteryEntry.startRequestQueue(); 265 } 266 267 /** 268 * Gets the BatteryEntry list by using the supplied BatteryUsageStats. 269 */ getBatteryEntryList( BatteryUsageStats batteryUsageStats, boolean showAllApps)270 public List<BatteryEntry> getBatteryEntryList( 271 BatteryUsageStats batteryUsageStats, boolean showAllApps) { 272 mBatteryUsageStats = USE_FAKE_DATA ? getFakeStats() : batteryUsageStats; 273 if (!sConfig.shouldShowBatteryAttributionList(mContext)) { 274 return null; 275 } 276 final int dischargePercentage = getDischargePercentage(batteryUsageStats); 277 final List<BatteryEntry> usageList = 278 getCoalescedUsageList(showAllApps, /*loadDataInBackground=*/ false); 279 final double totalPower = batteryUsageStats.getConsumedPower(); 280 for (int i = 0; i < usageList.size(); i++) { 281 final BatteryEntry entry = usageList.get(i); 282 final double percentOfTotal = mBatteryUtils.calculateBatteryPercent( 283 entry.getConsumedPower(), totalPower, dischargePercentage); 284 entry.percent = percentOfTotal; 285 } 286 return usageList; 287 } 288 getDischargePercentage(BatteryUsageStats batteryUsageStats)289 private int getDischargePercentage(BatteryUsageStats batteryUsageStats) { 290 int dischargePercentage = batteryUsageStats.getDischargePercentage(); 291 if (dischargePercentage < 0) { 292 dischargePercentage = 0; 293 } 294 return dischargePercentage; 295 } 296 297 /** 298 * We want to coalesce some UIDs. For example, dex2oat runs under a shared gid that 299 * exists for all users of the same app. We detect this case and merge the power use 300 * for dex2oat to the device OWNER's use of the app. 301 * 302 * @return A sorted list of apps using power. 303 */ getCoalescedUsageList( boolean showAllApps, boolean loadDataInBackground)304 private List<BatteryEntry> getCoalescedUsageList( 305 boolean showAllApps, boolean loadDataInBackground) { 306 final SparseArray<BatteryEntry> batteryEntryList = new SparseArray<>(); 307 308 final ArrayList<BatteryEntry> results = new ArrayList<>(); 309 final List<UidBatteryConsumer> uidBatteryConsumers = 310 mBatteryUsageStats.getUidBatteryConsumers(); 311 312 // Sort to have all apps with "real" UIDs first, followed by apps that are supposed 313 // to be combined with the real ones. 314 uidBatteryConsumers.sort(Comparator.comparingInt( 315 consumer -> consumer.getUid() == getRealUid(consumer) ? 0 : 1)); 316 317 for (int i = 0, size = uidBatteryConsumers.size(); i < size; i++) { 318 final UidBatteryConsumer consumer = uidBatteryConsumers.get(i); 319 final int uid = getRealUid(consumer); 320 321 final String[] packages = mPackageManager.getPackagesForUid(uid); 322 if (mBatteryUtils.shouldHideUidBatteryConsumerUnconditionally(consumer, packages)) { 323 continue; 324 } 325 326 final boolean isHidden = mBatteryUtils.shouldHideUidBatteryConsumer(consumer, packages); 327 if (isHidden && !showAllApps) { 328 continue; 329 } 330 331 final int index = batteryEntryList.indexOfKey(uid); 332 if (index < 0) { 333 // New entry. 334 batteryEntryList.put(uid, new BatteryEntry(mContext, mHandler, mUserManager, 335 consumer, isHidden, uid, packages, null, loadDataInBackground)); 336 } else { 337 // Combine BatterySippers if we already have one with this UID. 338 final BatteryEntry existingSipper = batteryEntryList.valueAt(index); 339 existingSipper.add(consumer); 340 } 341 } 342 343 final BatteryConsumer deviceConsumer = mBatteryUsageStats.getAggregateBatteryConsumer( 344 BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_DEVICE); 345 final BatteryConsumer appsConsumer = mBatteryUsageStats.getAggregateBatteryConsumer( 346 BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_ALL_APPS); 347 348 for (int componentId = 0; componentId < BatteryConsumer.POWER_COMPONENT_COUNT; 349 componentId++) { 350 if (!showAllApps 351 && mBatteryUtils.shouldHideDevicePowerComponent(deviceConsumer, componentId)) { 352 continue; 353 } 354 355 results.add(new BatteryEntry(mContext, componentId, 356 deviceConsumer.getConsumedPower(componentId), 357 appsConsumer.getConsumedPower(componentId), 358 deviceConsumer.getUsageDurationMillis(componentId))); 359 } 360 361 for (int componentId = BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID; 362 componentId < BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID 363 + deviceConsumer.getCustomPowerComponentCount(); 364 componentId++) { 365 if (!showAllApps) { 366 continue; 367 } 368 369 results.add(new BatteryEntry(mContext, componentId, 370 deviceConsumer.getCustomPowerComponentName(componentId), 371 deviceConsumer.getConsumedPowerForCustomComponent(componentId), 372 appsConsumer.getConsumedPowerForCustomComponent(componentId))); 373 } 374 375 if (showAllApps) { 376 final List<UserBatteryConsumer> userBatteryConsumers = 377 mBatteryUsageStats.getUserBatteryConsumers(); 378 for (int i = 0, size = userBatteryConsumers.size(); i < size; i++) { 379 final UserBatteryConsumer consumer = userBatteryConsumers.get(i); 380 results.add(new BatteryEntry(mContext, mHandler, mUserManager, 381 consumer, /* isHidden */ true, Process.INVALID_UID, null, null, 382 loadDataInBackground)); 383 } 384 } 385 386 final int numUidSippers = batteryEntryList.size(); 387 388 for (int i = 0; i < numUidSippers; i++) { 389 results.add(batteryEntryList.valueAt(i)); 390 } 391 392 // The sort order must have changed, so re-sort based on total power use. 393 results.sort(BatteryEntry.COMPARATOR); 394 return results; 395 } 396 getRealUid(UidBatteryConsumer consumer)397 private int getRealUid(UidBatteryConsumer consumer) { 398 int realUid = consumer.getUid(); 399 400 // Check if this UID is a shared GID. If so, we combine it with the OWNER's 401 // actual app UID. 402 if (isSharedGid(consumer.getUid())) { 403 realUid = UserHandle.getUid(UserHandle.USER_SYSTEM, 404 UserHandle.getAppIdFromSharedAppGid(consumer.getUid())); 405 } 406 407 // Check if this UID is a system UID (mediaserver, logd, nfc, drm, etc). 408 if (isSystemUid(realUid) 409 && !MEDIASERVER_PACKAGE_NAME.equals(consumer.getPackageWithHighestDrain())) { 410 // Use the system UID for all UIDs running in their own sandbox that 411 // are not apps. We exclude mediaserver because we already are expected to 412 // report that as a separate item. 413 realUid = Process.SYSTEM_UID; 414 } 415 return realUid; 416 } 417 418 @VisibleForTesting setUsageSummary(Preference preference, BatteryEntry entry)419 void setUsageSummary(Preference preference, BatteryEntry entry) { 420 // Only show summary when usage time is longer than one minute 421 final long usageTimeMs = entry.getTimeInForegroundMs(); 422 if (shouldShowSummary(entry) && usageTimeMs >= DateUtils.MINUTE_IN_MILLIS) { 423 final CharSequence timeSequence = 424 StringUtil.formatElapsedTime(mContext, usageTimeMs, false, false); 425 preference.setSummary( 426 entry.isHidden() 427 ? timeSequence 428 : TextUtils.expandTemplate(mContext.getText(R.string.battery_used_for), 429 timeSequence)); 430 } 431 } 432 cacheRemoveAllPrefs(PreferenceGroup group)433 private void cacheRemoveAllPrefs(PreferenceGroup group) { 434 mPreferenceCache = new ArrayMap<>(); 435 final int N = group.getPreferenceCount(); 436 for (int i = 0; i < N; i++) { 437 Preference p = group.getPreference(i); 438 if (TextUtils.isEmpty(p.getKey())) { 439 continue; 440 } 441 mPreferenceCache.put(p.getKey(), p); 442 } 443 } 444 shouldShowSummary(BatteryEntry entry)445 private boolean shouldShowSummary(BatteryEntry entry) { 446 final CharSequence[] allowlistPackages = 447 FeatureFactory.getFactory(mContext) 448 .getPowerUsageFeatureProvider(mContext) 449 .getHideApplicationSummary(mContext); 450 final String target = entry.getDefaultPackageName(); 451 452 for (CharSequence packageName : allowlistPackages) { 453 if (TextUtils.equals(target, packageName)) { 454 return false; 455 } 456 } 457 return true; 458 } 459 isSharedGid(int uid)460 private static boolean isSharedGid(int uid) { 461 return UserHandle.getAppIdFromSharedAppGid(uid) > 0; 462 } 463 isSystemUid(int uid)464 private static boolean isSystemUid(int uid) { 465 final int appUid = UserHandle.getAppId(uid); 466 return appUid >= Process.SYSTEM_UID && appUid < Process.FIRST_APPLICATION_UID; 467 } 468 getFakeStats()469 private BatteryUsageStats getFakeStats() { 470 BatteryUsageStats.Builder builder = new BatteryUsageStats.Builder(new String[0]) 471 .setDischargePercentage(100); 472 473 float use = 500; 474 final AggregateBatteryConsumer.Builder appsBatteryConsumerBuilder = 475 builder.getAggregateBatteryConsumerBuilder( 476 BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_ALL_APPS); 477 final AggregateBatteryConsumer.Builder deviceBatteryConsumerBuilder = 478 builder.getAggregateBatteryConsumerBuilder( 479 BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_DEVICE); 480 for (@BatteryConsumer.PowerComponent int componentId : new int[]{ 481 BatteryConsumer.POWER_COMPONENT_AMBIENT_DISPLAY, 482 BatteryConsumer.POWER_COMPONENT_BLUETOOTH, 483 BatteryConsumer.POWER_COMPONENT_CAMERA, 484 BatteryConsumer.POWER_COMPONENT_FLASHLIGHT, 485 BatteryConsumer.POWER_COMPONENT_IDLE, 486 BatteryConsumer.POWER_COMPONENT_MEMORY, 487 BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO, 488 BatteryConsumer.POWER_COMPONENT_PHONE, 489 BatteryConsumer.POWER_COMPONENT_SCREEN, 490 BatteryConsumer.POWER_COMPONENT_WIFI, 491 }) { 492 appsBatteryConsumerBuilder.setConsumedPower(componentId, use); 493 deviceBatteryConsumerBuilder.setConsumedPower(componentId, use * 2); 494 use += 5; 495 } 496 497 use = 450; 498 for (int i = 0; i < 100; i++) { 499 builder.getOrCreateUidBatteryConsumerBuilder( 500 new FakeUid(Process.FIRST_APPLICATION_UID + i)) 501 .setTimeInStateMs(UidBatteryConsumer.STATE_FOREGROUND, 10000 + i * 1000) 502 .setTimeInStateMs(UidBatteryConsumer.STATE_BACKGROUND, 20000 + i * 2000) 503 .setConsumedPower(BatteryConsumer.POWER_COMPONENT_CPU, use); 504 use += 1; 505 } 506 507 // Simulate dex2oat process. 508 builder.getOrCreateUidBatteryConsumerBuilder(new FakeUid(Process.FIRST_APPLICATION_UID)) 509 .setUsageDurationMillis(BatteryConsumer.POWER_COMPONENT_CPU, 100000) 510 .setConsumedPower(BatteryConsumer.POWER_COMPONENT_CPU, 1000.0) 511 .setPackageWithHighestDrain("dex2oat"); 512 513 builder.getOrCreateUidBatteryConsumerBuilder(new FakeUid(Process.FIRST_APPLICATION_UID + 1)) 514 .setUsageDurationMillis(BatteryConsumer.POWER_COMPONENT_CPU, 100000) 515 .setConsumedPower(BatteryConsumer.POWER_COMPONENT_CPU, 1000.0) 516 .setPackageWithHighestDrain("dex2oat"); 517 518 builder.getOrCreateUidBatteryConsumerBuilder( 519 new FakeUid(UserHandle.getSharedAppGid(Process.LOG_UID))) 520 .setUsageDurationMillis(BatteryConsumer.POWER_COMPONENT_CPU, 100000) 521 .setConsumedPower(BatteryConsumer.POWER_COMPONENT_CPU, 900.0); 522 523 return builder.build(); 524 } 525 getCachedPreference(String key)526 private Preference getCachedPreference(String key) { 527 return mPreferenceCache != null ? mPreferenceCache.remove(key) : null; 528 } 529 removeCachedPrefs(PreferenceGroup group)530 private void removeCachedPrefs(PreferenceGroup group) { 531 for (Preference p : mPreferenceCache.values()) { 532 group.removePreference(p); 533 } 534 mPreferenceCache = null; 535 } 536 getCachedCount()537 private int getCachedCount() { 538 return mPreferenceCache != null ? mPreferenceCache.size() : 0; 539 } 540 addNotAvailableMessage()541 private void addNotAvailableMessage() { 542 final String NOT_AVAILABLE = "not_available"; 543 Preference notAvailable = getCachedPreference(NOT_AVAILABLE); 544 if (notAvailable == null) { 545 notAvailable = new Preference(mPrefContext); 546 notAvailable.setKey(NOT_AVAILABLE); 547 notAvailable.setTitle(R.string.power_usage_not_available); 548 notAvailable.setSelectable(false); 549 mAppListGroup.addPreference(notAvailable); 550 } 551 } 552 } 553