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