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