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.graphics.drawable.Drawable; 23 import android.os.BatteryStats; 24 import android.os.Handler; 25 import android.os.Looper; 26 import android.os.Message; 27 import android.os.Process; 28 import android.os.UserHandle; 29 import android.os.UserManager; 30 import android.text.TextUtils; 31 import android.text.format.DateUtils; 32 import android.util.ArrayMap; 33 import android.util.Log; 34 import android.util.SparseArray; 35 36 import androidx.annotation.VisibleForTesting; 37 import androidx.preference.Preference; 38 import androidx.preference.PreferenceGroup; 39 import androidx.preference.PreferenceScreen; 40 41 import com.android.internal.os.BatterySipper; 42 import com.android.internal.os.BatterySipper.DrainType; 43 import com.android.internal.os.BatteryStatsHelper; 44 import com.android.internal.os.PowerProfile; 45 import com.android.settings.R; 46 import com.android.settings.SettingsActivity; 47 import com.android.settings.core.InstrumentedPreferenceFragment; 48 import com.android.settings.core.PreferenceControllerMixin; 49 import com.android.settingslib.core.AbstractPreferenceController; 50 import com.android.settingslib.core.lifecycle.Lifecycle; 51 import com.android.settingslib.core.lifecycle.LifecycleObserver; 52 import com.android.settingslib.core.lifecycle.events.OnDestroy; 53 import com.android.settingslib.core.lifecycle.events.OnPause; 54 import com.android.settingslib.utils.StringUtil; 55 56 import java.util.ArrayList; 57 import java.util.List; 58 59 /** 60 * Controller that update the battery header view 61 */ 62 public class BatteryAppListPreferenceController extends AbstractPreferenceController 63 implements PreferenceControllerMixin, LifecycleObserver, OnPause, OnDestroy { 64 @VisibleForTesting 65 static final boolean USE_FAKE_DATA = false; 66 private static final int MAX_ITEMS_TO_LIST = USE_FAKE_DATA ? 30 : 20; 67 private static final int MIN_AVERAGE_POWER_THRESHOLD_MILLI_AMP = 10; 68 private static final int STATS_TYPE = BatteryStats.STATS_SINCE_CHARGED; 69 70 private final String mPreferenceKey; 71 @VisibleForTesting 72 PreferenceGroup mAppListGroup; 73 private BatteryStatsHelper mBatteryStatsHelper; 74 private ArrayMap<String, Preference> mPreferenceCache; 75 @VisibleForTesting 76 BatteryUtils mBatteryUtils; 77 private UserManager mUserManager; 78 private SettingsActivity mActivity; 79 private InstrumentedPreferenceFragment mFragment; 80 private Context mPrefContext; 81 82 private Handler mHandler = new Handler(Looper.getMainLooper()) { 83 @Override 84 public void handleMessage(Message msg) { 85 switch (msg.what) { 86 case BatteryEntry.MSG_UPDATE_NAME_ICON: 87 BatteryEntry entry = (BatteryEntry) msg.obj; 88 PowerGaugePreference pgp = 89 (PowerGaugePreference) mAppListGroup.findPreference( 90 Integer.toString(entry.sipper.uidObj.getUid())); 91 if (pgp != null) { 92 final int userId = UserHandle.getUserId(entry.sipper.getUid()); 93 final UserHandle userHandle = new UserHandle(userId); 94 pgp.setIcon(mUserManager.getBadgedIconForUser(entry.getIcon(), userHandle)); 95 pgp.setTitle(entry.name); 96 if (entry.sipper.drainType == DrainType.APP) { 97 pgp.setContentDescription(entry.name); 98 } 99 } 100 break; 101 case BatteryEntry.MSG_REPORT_FULLY_DRAWN: 102 Activity activity = mActivity; 103 if (activity != null) { 104 activity.reportFullyDrawn(); 105 } 106 break; 107 } 108 super.handleMessage(msg); 109 } 110 }; 111 BatteryAppListPreferenceController(Context context, String preferenceKey, Lifecycle lifecycle, SettingsActivity activity, InstrumentedPreferenceFragment fragment)112 public BatteryAppListPreferenceController(Context context, String preferenceKey, 113 Lifecycle lifecycle, SettingsActivity activity, 114 InstrumentedPreferenceFragment fragment) { 115 super(context); 116 117 if (lifecycle != null) { 118 lifecycle.addObserver(this); 119 } 120 121 mPreferenceKey = preferenceKey; 122 mBatteryUtils = BatteryUtils.getInstance(context); 123 mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE); 124 mActivity = activity; 125 mFragment = fragment; 126 } 127 128 @Override onPause()129 public void onPause() { 130 BatteryEntry.stopRequestQueue(); 131 mHandler.removeMessages(BatteryEntry.MSG_UPDATE_NAME_ICON); 132 } 133 134 @Override onDestroy()135 public void onDestroy() { 136 if (mActivity.isChangingConfigurations()) { 137 BatteryEntry.clearUidCache(); 138 } 139 } 140 141 @Override displayPreference(PreferenceScreen screen)142 public void displayPreference(PreferenceScreen screen) { 143 super.displayPreference(screen); 144 mPrefContext = screen.getContext(); 145 mAppListGroup = screen.findPreference(mPreferenceKey); 146 } 147 148 @Override isAvailable()149 public boolean isAvailable() { 150 return true; 151 } 152 153 @Override getPreferenceKey()154 public String getPreferenceKey() { 155 return mPreferenceKey; 156 } 157 158 @Override handlePreferenceTreeClick(Preference preference)159 public boolean handlePreferenceTreeClick(Preference preference) { 160 if (preference instanceof PowerGaugePreference) { 161 PowerGaugePreference pgp = (PowerGaugePreference) preference; 162 BatteryEntry entry = pgp.getInfo(); 163 AdvancedPowerUsageDetail.startBatteryDetailPage(mActivity, mBatteryUtils, 164 mFragment, mBatteryStatsHelper, STATS_TYPE, entry, pgp.getPercent()); 165 return true; 166 } 167 return false; 168 } 169 refreshAppListGroup(BatteryStatsHelper statsHelper, boolean showAllApps)170 public void refreshAppListGroup(BatteryStatsHelper statsHelper, boolean showAllApps) { 171 if (!isAvailable()) { 172 return; 173 } 174 175 mBatteryStatsHelper = statsHelper; 176 mAppListGroup.setTitle(R.string.power_usage_list_summary); 177 178 final PowerProfile powerProfile = statsHelper.getPowerProfile(); 179 final BatteryStats stats = statsHelper.getStats(); 180 final double averagePower = powerProfile.getAveragePower(PowerProfile.POWER_SCREEN_FULL); 181 boolean addedSome = false; 182 final int dischargeAmount = USE_FAKE_DATA ? 5000 183 : stats != null ? stats.getDischargeAmount(STATS_TYPE) : 0; 184 185 cacheRemoveAllPrefs(mAppListGroup); 186 mAppListGroup.setOrderingAsAdded(false); 187 188 if (averagePower >= MIN_AVERAGE_POWER_THRESHOLD_MILLI_AMP || USE_FAKE_DATA) { 189 final List<BatterySipper> usageList = getCoalescedUsageList( 190 USE_FAKE_DATA ? getFakeStats() : statsHelper.getUsageList()); 191 double hiddenPowerMah = showAllApps ? 0 : 192 mBatteryUtils.removeHiddenBatterySippers(usageList); 193 mBatteryUtils.sortUsageList(usageList); 194 195 final int numSippers = usageList.size(); 196 for (int i = 0; i < numSippers; i++) { 197 final BatterySipper sipper = usageList.get(i); 198 double totalPower = USE_FAKE_DATA ? 4000 : statsHelper.getTotalPower(); 199 200 final double percentOfTotal = mBatteryUtils.calculateBatteryPercent( 201 sipper.totalPowerMah, totalPower, hiddenPowerMah, dischargeAmount); 202 203 if (((int) (percentOfTotal + .5)) < 1) { 204 continue; 205 } 206 if (shouldHideSipper(sipper)) { 207 continue; 208 } 209 final UserHandle userHandle = new UserHandle(UserHandle.getUserId(sipper.getUid())); 210 final BatteryEntry entry = new BatteryEntry(mActivity, mHandler, mUserManager, 211 sipper); 212 final Drawable badgedIcon = mUserManager.getBadgedIconForUser(entry.getIcon(), 213 userHandle); 214 final CharSequence contentDescription = mUserManager.getBadgedLabelForUser( 215 entry.getLabel(), 216 userHandle); 217 218 final String key = extractKeyFromSipper(sipper); 219 PowerGaugePreference pref = (PowerGaugePreference) getCachedPreference(key); 220 if (pref == null) { 221 pref = new PowerGaugePreference(mPrefContext, badgedIcon, 222 contentDescription, entry); 223 pref.setKey(key); 224 } 225 sipper.percent = percentOfTotal; 226 pref.setTitle(entry.getLabel()); 227 pref.setOrder(i + 1); 228 pref.setPercent(percentOfTotal); 229 pref.shouldShowAnomalyIcon(false); 230 if (sipper.usageTimeMs == 0 && sipper.drainType == DrainType.APP) { 231 sipper.usageTimeMs = mBatteryUtils.getProcessTimeMs( 232 BatteryUtils.StatusType.FOREGROUND, sipper.uidObj, STATS_TYPE); 233 } 234 setUsageSummary(pref, sipper); 235 addedSome = true; 236 mAppListGroup.addPreference(pref); 237 if (mAppListGroup.getPreferenceCount() - getCachedCount() 238 > (MAX_ITEMS_TO_LIST + 1)) { 239 break; 240 } 241 } 242 } 243 if (!addedSome) { 244 addNotAvailableMessage(); 245 } 246 removeCachedPrefs(mAppListGroup); 247 248 BatteryEntry.startRequestQueue(); 249 } 250 251 /** 252 * We want to coalesce some UIDs. For example, dex2oat runs under a shared gid that 253 * exists for all users of the same app. We detect this case and merge the power use 254 * for dex2oat to the device OWNER's use of the app. 255 * 256 * @return A sorted list of apps using power. 257 */ getCoalescedUsageList(final List<BatterySipper> sippers)258 private List<BatterySipper> getCoalescedUsageList(final List<BatterySipper> sippers) { 259 final SparseArray<BatterySipper> uidList = new SparseArray<>(); 260 261 final ArrayList<BatterySipper> results = new ArrayList<>(); 262 final int numSippers = sippers.size(); 263 for (int i = 0; i < numSippers; i++) { 264 BatterySipper sipper = sippers.get(i); 265 if (sipper.getUid() > 0) { 266 int realUid = sipper.getUid(); 267 268 // Check if this UID is a shared GID. If so, we combine it with the OWNER's 269 // actual app UID. 270 if (isSharedGid(sipper.getUid())) { 271 realUid = UserHandle.getUid(UserHandle.USER_SYSTEM, 272 UserHandle.getAppIdFromSharedAppGid(sipper.getUid())); 273 } 274 275 // Check if this UID is a system UID (mediaserver, logd, nfc, drm, etc). 276 if (isSystemUid(realUid) 277 && !"mediaserver".equals(sipper.packageWithHighestDrain)) { 278 // Use the system UID for all UIDs running in their own sandbox that 279 // are not apps. We exclude mediaserver because we already are expected to 280 // report that as a separate item. 281 realUid = Process.SYSTEM_UID; 282 } 283 284 if (realUid != sipper.getUid()) { 285 // Replace the BatterySipper with a new one with the real UID set. 286 BatterySipper newSipper = new BatterySipper(sipper.drainType, 287 new FakeUid(realUid), 0.0); 288 newSipper.add(sipper); 289 newSipper.packageWithHighestDrain = sipper.packageWithHighestDrain; 290 newSipper.mPackages = sipper.mPackages; 291 sipper = newSipper; 292 } 293 294 int index = uidList.indexOfKey(realUid); 295 if (index < 0) { 296 // New entry. 297 uidList.put(realUid, sipper); 298 } else { 299 // Combine BatterySippers if we already have one with this UID. 300 final BatterySipper existingSipper = uidList.valueAt(index); 301 existingSipper.add(sipper); 302 if (existingSipper.packageWithHighestDrain == null 303 && sipper.packageWithHighestDrain != null) { 304 existingSipper.packageWithHighestDrain = sipper.packageWithHighestDrain; 305 } 306 307 final int existingPackageLen = existingSipper.mPackages != null ? 308 existingSipper.mPackages.length : 0; 309 final int newPackageLen = sipper.mPackages != null ? 310 sipper.mPackages.length : 0; 311 if (newPackageLen > 0) { 312 String[] newPackages = new String[existingPackageLen + newPackageLen]; 313 if (existingPackageLen > 0) { 314 System.arraycopy(existingSipper.mPackages, 0, newPackages, 0, 315 existingPackageLen); 316 } 317 System.arraycopy(sipper.mPackages, 0, newPackages, existingPackageLen, 318 newPackageLen); 319 existingSipper.mPackages = newPackages; 320 } 321 } 322 } else { 323 results.add(sipper); 324 } 325 } 326 327 final int numUidSippers = uidList.size(); 328 for (int i = 0; i < numUidSippers; i++) { 329 results.add(uidList.valueAt(i)); 330 } 331 332 // The sort order must have changed, so re-sort based on total power use. 333 mBatteryUtils.sortUsageList(results); 334 return results; 335 } 336 337 @VisibleForTesting setUsageSummary(Preference preference, BatterySipper sipper)338 void setUsageSummary(Preference preference, BatterySipper sipper) { 339 // Only show summary when usage time is longer than one minute 340 final long usageTimeMs = sipper.usageTimeMs; 341 if (shouldShowSummary(sipper) && usageTimeMs >= DateUtils.MINUTE_IN_MILLIS) { 342 final CharSequence timeSequence = 343 StringUtil.formatElapsedTime(mContext, usageTimeMs, false); 344 preference.setSummary( 345 (sipper.drainType != DrainType.APP || mBatteryUtils.shouldHideSipper(sipper)) 346 ? timeSequence 347 : TextUtils.expandTemplate(mContext.getText(R.string.battery_used_for), 348 timeSequence)); 349 } 350 } 351 352 @VisibleForTesting shouldHideSipper(BatterySipper sipper)353 boolean shouldHideSipper(BatterySipper sipper) { 354 // Don't show over-counted, unaccounted and hidden system module in any condition 355 return sipper.drainType == BatterySipper.DrainType.OVERCOUNTED 356 || sipper.drainType == BatterySipper.DrainType.UNACCOUNTED 357 || mBatteryUtils.isHiddenSystemModule(sipper) || sipper.getUid() < 0; 358 } 359 360 @VisibleForTesting extractKeyFromSipper(BatterySipper sipper)361 String extractKeyFromSipper(BatterySipper sipper) { 362 if (sipper.uidObj != null) { 363 return extractKeyFromUid(sipper.getUid()); 364 } else if (sipper.drainType == DrainType.USER) { 365 return sipper.drainType.toString() + sipper.userId; 366 } else if (sipper.drainType != DrainType.APP) { 367 return sipper.drainType.toString(); 368 } else if (sipper.getPackages() != null) { 369 return TextUtils.concat(sipper.getPackages()).toString(); 370 } else { 371 Log.w(TAG, "Inappropriate BatterySipper without uid and package names: " + sipper); 372 return "-1"; 373 } 374 } 375 376 @VisibleForTesting extractKeyFromUid(int uid)377 String extractKeyFromUid(int uid) { 378 return Integer.toString(uid); 379 } 380 cacheRemoveAllPrefs(PreferenceGroup group)381 private void cacheRemoveAllPrefs(PreferenceGroup group) { 382 mPreferenceCache = new ArrayMap<>(); 383 final int N = group.getPreferenceCount(); 384 for (int i = 0; i < N; i++) { 385 Preference p = group.getPreference(i); 386 if (TextUtils.isEmpty(p.getKey())) { 387 continue; 388 } 389 mPreferenceCache.put(p.getKey(), p); 390 } 391 } 392 shouldShowSummary(BatterySipper sipper)393 private boolean shouldShowSummary(BatterySipper sipper) { 394 final CharSequence[] whitelistPackages = mContext.getResources() 395 .getTextArray(R.array.whitelist_hide_summary_in_battery_usage); 396 final String target = sipper.packageWithHighestDrain; 397 398 for (CharSequence packageName: whitelistPackages) { 399 if (TextUtils.equals(target, packageName)) { 400 return false; 401 } 402 } 403 return true; 404 } 405 isSharedGid(int uid)406 private static boolean isSharedGid(int uid) { 407 return UserHandle.getAppIdFromSharedAppGid(uid) > 0; 408 } 409 isSystemUid(int uid)410 private static boolean isSystemUid(int uid) { 411 final int appUid = UserHandle.getAppId(uid); 412 return appUid >= Process.SYSTEM_UID && appUid < Process.FIRST_APPLICATION_UID; 413 } 414 getFakeStats()415 private static List<BatterySipper> getFakeStats() { 416 ArrayList<BatterySipper> stats = new ArrayList<>(); 417 float use = 5; 418 for (DrainType type : DrainType.values()) { 419 if (type == DrainType.APP) { 420 continue; 421 } 422 stats.add(new BatterySipper(type, null, use)); 423 use += 5; 424 } 425 for (int i = 0; i < 100; i++) { 426 stats.add(new BatterySipper(DrainType.APP, 427 new FakeUid(Process.FIRST_APPLICATION_UID + i), use)); 428 } 429 stats.add(new BatterySipper(DrainType.APP, 430 new FakeUid(0), use)); 431 432 // Simulate dex2oat process. 433 BatterySipper sipper = new BatterySipper(DrainType.APP, 434 new FakeUid(UserHandle.getSharedAppGid(Process.FIRST_APPLICATION_UID)), 10.0f); 435 sipper.packageWithHighestDrain = "dex2oat"; 436 stats.add(sipper); 437 438 sipper = new BatterySipper(DrainType.APP, 439 new FakeUid(UserHandle.getSharedAppGid(Process.FIRST_APPLICATION_UID + 1)), 10.0f); 440 sipper.packageWithHighestDrain = "dex2oat"; 441 stats.add(sipper); 442 443 sipper = new BatterySipper(DrainType.APP, 444 new FakeUid(UserHandle.getSharedAppGid(Process.LOG_UID)), 9.0f); 445 stats.add(sipper); 446 447 return stats; 448 } 449 getCachedPreference(String key)450 private Preference getCachedPreference(String key) { 451 return mPreferenceCache != null ? mPreferenceCache.remove(key) : null; 452 } 453 removeCachedPrefs(PreferenceGroup group)454 private void removeCachedPrefs(PreferenceGroup group) { 455 for (Preference p : mPreferenceCache.values()) { 456 group.removePreference(p); 457 } 458 mPreferenceCache = null; 459 } 460 getCachedCount()461 private int getCachedCount() { 462 return mPreferenceCache != null ? mPreferenceCache.size() : 0; 463 } 464 addNotAvailableMessage()465 private void addNotAvailableMessage() { 466 final String NOT_AVAILABLE = "not_available"; 467 Preference notAvailable = getCachedPreference(NOT_AVAILABLE); 468 if (notAvailable == null) { 469 notAvailable = new Preference(mPrefContext); 470 notAvailable.setKey(NOT_AVAILABLE); 471 notAvailable.setTitle(R.string.power_usage_not_available); 472 notAvailable.setSelectable(false); 473 mAppListGroup.addPreference(notAvailable); 474 } 475 } 476 } 477