1 /* 2 * Copyright (C) 2009 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 17 package com.android.settings.fuelgauge; 18 19 import android.app.Activity; 20 import android.content.Context; 21 import android.graphics.drawable.Drawable; 22 import android.os.BatteryStats; 23 import android.os.Build; 24 import android.os.Bundle; 25 import android.os.Handler; 26 import android.os.Message; 27 import android.os.Process; 28 import android.os.UserHandle; 29 import android.support.v7.preference.Preference; 30 import android.support.v7.preference.PreferenceGroup; 31 import android.text.TextUtils; 32 import android.util.SparseArray; 33 import android.util.TypedValue; 34 import android.view.Menu; 35 import android.view.MenuInflater; 36 import android.view.MenuItem; 37 import com.android.internal.logging.MetricsProto.MetricsEvent; 38 import com.android.internal.os.BatterySipper; 39 import com.android.internal.os.BatterySipper.DrainType; 40 import com.android.internal.os.PowerProfile; 41 import com.android.settings.R; 42 import com.android.settings.Settings.HighPowerApplicationsActivity; 43 import com.android.settings.SettingsActivity; 44 import com.android.settings.applications.ManageApplications; 45 import com.android.settings.dashboard.SummaryLoader; 46 import com.android.settingslib.BatteryInfo; 47 48 import java.util.ArrayList; 49 import java.util.Collections; 50 import java.util.Comparator; 51 import java.util.List; 52 53 /** 54 * Displays a list of apps and subsystems that consume power, ordered by how much power was 55 * consumed since the last time it was unplugged. 56 */ 57 public class PowerUsageSummary extends PowerUsageBase { 58 59 private static final boolean DEBUG = false; 60 61 private static final boolean USE_FAKE_DATA = false; 62 63 static final String TAG = "PowerUsageSummary"; 64 65 private static final String KEY_APP_LIST = "app_list"; 66 private static final String KEY_BATTERY_HISTORY = "battery_history"; 67 68 private static final int MENU_STATS_TYPE = Menu.FIRST; 69 private static final int MENU_HIGH_POWER_APPS = Menu.FIRST + 3; 70 private static final int MENU_HELP = Menu.FIRST + 4; 71 72 private BatteryHistoryPreference mHistPref; 73 private PreferenceGroup mAppListGroup; 74 75 private int mStatsType = BatteryStats.STATS_SINCE_CHARGED; 76 77 private static final int MIN_POWER_THRESHOLD_MILLI_AMP = 5; 78 private static final int MAX_ITEMS_TO_LIST = USE_FAKE_DATA ? 30 : 10; 79 private static final int MIN_AVERAGE_POWER_THRESHOLD_MILLI_AMP = 10; 80 private static final int SECONDS_IN_HOUR = 60 * 60; 81 82 @Override onCreate(Bundle icicle)83 public void onCreate(Bundle icicle) { 84 super.onCreate(icicle); 85 setAnimationAllowed(true); 86 87 addPreferencesFromResource(R.xml.power_usage_summary); 88 mHistPref = (BatteryHistoryPreference) findPreference(KEY_BATTERY_HISTORY); 89 mAppListGroup = (PreferenceGroup) findPreference(KEY_APP_LIST); 90 } 91 92 @Override getMetricsCategory()93 protected int getMetricsCategory() { 94 return MetricsEvent.FUELGAUGE_POWER_USAGE_SUMMARY; 95 } 96 97 @Override onResume()98 public void onResume() { 99 super.onResume(); 100 refreshStats(); 101 } 102 103 @Override onPause()104 public void onPause() { 105 BatteryEntry.stopRequestQueue(); 106 mHandler.removeMessages(BatteryEntry.MSG_UPDATE_NAME_ICON); 107 super.onPause(); 108 } 109 110 @Override onDestroy()111 public void onDestroy() { 112 super.onDestroy(); 113 if (getActivity().isChangingConfigurations()) { 114 BatteryEntry.clearUidCache(); 115 } 116 } 117 118 @Override onPreferenceTreeClick(Preference preference)119 public boolean onPreferenceTreeClick(Preference preference) { 120 if (!(preference instanceof PowerGaugePreference)) { 121 return super.onPreferenceTreeClick(preference); 122 } 123 PowerGaugePreference pgp = (PowerGaugePreference) preference; 124 BatteryEntry entry = pgp.getInfo(); 125 PowerUsageDetail.startBatteryDetailPage((SettingsActivity) getActivity(), mStatsHelper, 126 mStatsType, entry, true, true); 127 return super.onPreferenceTreeClick(preference); 128 } 129 130 @Override onCreateOptionsMenu(Menu menu, MenuInflater inflater)131 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { 132 if (DEBUG) { 133 menu.add(0, MENU_STATS_TYPE, 0, R.string.menu_stats_total) 134 .setIcon(com.android.internal.R.drawable.ic_menu_info_details) 135 .setAlphabeticShortcut('t'); 136 } 137 138 menu.add(0, MENU_HIGH_POWER_APPS, 0, R.string.high_power_apps); 139 super.onCreateOptionsMenu(menu, inflater); 140 } 141 142 @Override getHelpResource()143 protected int getHelpResource() { 144 return R.string.help_url_battery; 145 } 146 147 @Override onOptionsItemSelected(MenuItem item)148 public boolean onOptionsItemSelected(MenuItem item) { 149 final SettingsActivity sa = (SettingsActivity) getActivity(); 150 switch (item.getItemId()) { 151 case MENU_STATS_TYPE: 152 if (mStatsType == BatteryStats.STATS_SINCE_CHARGED) { 153 mStatsType = BatteryStats.STATS_SINCE_UNPLUGGED; 154 } else { 155 mStatsType = BatteryStats.STATS_SINCE_CHARGED; 156 } 157 refreshStats(); 158 return true; 159 case MENU_HIGH_POWER_APPS: 160 Bundle args = new Bundle(); 161 args.putString(ManageApplications.EXTRA_CLASSNAME, 162 HighPowerApplicationsActivity.class.getName()); 163 sa.startPreferencePanel(ManageApplications.class.getName(), args, 164 R.string.high_power_apps, null, null, 0); 165 return true; 166 default: 167 return super.onOptionsItemSelected(item); 168 } 169 } 170 addNotAvailableMessage()171 private void addNotAvailableMessage() { 172 final String NOT_AVAILABLE = "not_available"; 173 Preference notAvailable = getCachedPreference(NOT_AVAILABLE); 174 if (notAvailable == null) { 175 notAvailable = new Preference(getPrefContext()); 176 notAvailable.setKey(NOT_AVAILABLE); 177 notAvailable.setTitle(R.string.power_usage_not_available); 178 mAppListGroup.addPreference(notAvailable); 179 } 180 } 181 isSharedGid(int uid)182 private static boolean isSharedGid(int uid) { 183 return UserHandle.getAppIdFromSharedAppGid(uid) > 0; 184 } 185 isSystemUid(int uid)186 private static boolean isSystemUid(int uid) { 187 return uid >= Process.SYSTEM_UID && uid < Process.FIRST_APPLICATION_UID; 188 } 189 190 /** 191 * We want to coalesce some UIDs. For example, dex2oat runs under a shared gid that 192 * exists for all users of the same app. We detect this case and merge the power use 193 * for dex2oat to the device OWNER's use of the app. 194 * @return A sorted list of apps using power. 195 */ getCoalescedUsageList(final List<BatterySipper> sippers)196 private static List<BatterySipper> getCoalescedUsageList(final List<BatterySipper> sippers) { 197 final SparseArray<BatterySipper> uidList = new SparseArray<>(); 198 199 final ArrayList<BatterySipper> results = new ArrayList<>(); 200 final int numSippers = sippers.size(); 201 for (int i = 0; i < numSippers; i++) { 202 BatterySipper sipper = sippers.get(i); 203 if (sipper.getUid() > 0) { 204 int realUid = sipper.getUid(); 205 206 // Check if this UID is a shared GID. If so, we combine it with the OWNER's 207 // actual app UID. 208 if (isSharedGid(sipper.getUid())) { 209 realUid = UserHandle.getUid(UserHandle.USER_SYSTEM, 210 UserHandle.getAppIdFromSharedAppGid(sipper.getUid())); 211 } 212 213 // Check if this UID is a system UID (mediaserver, logd, nfc, drm, etc). 214 if (isSystemUid(realUid) 215 && !"mediaserver".equals(sipper.packageWithHighestDrain)) { 216 // Use the system UID for all UIDs running in their own sandbox that 217 // are not apps. We exclude mediaserver because we already are expected to 218 // report that as a separate item. 219 realUid = Process.SYSTEM_UID; 220 } 221 222 if (realUid != sipper.getUid()) { 223 // Replace the BatterySipper with a new one with the real UID set. 224 BatterySipper newSipper = new BatterySipper(sipper.drainType, 225 new FakeUid(realUid), 0.0); 226 newSipper.add(sipper); 227 newSipper.packageWithHighestDrain = sipper.packageWithHighestDrain; 228 newSipper.mPackages = sipper.mPackages; 229 sipper = newSipper; 230 } 231 232 int index = uidList.indexOfKey(realUid); 233 if (index < 0) { 234 // New entry. 235 uidList.put(realUid, sipper); 236 } else { 237 // Combine BatterySippers if we already have one with this UID. 238 final BatterySipper existingSipper = uidList.valueAt(index); 239 existingSipper.add(sipper); 240 if (existingSipper.packageWithHighestDrain == null 241 && sipper.packageWithHighestDrain != null) { 242 existingSipper.packageWithHighestDrain = sipper.packageWithHighestDrain; 243 } 244 245 final int existingPackageLen = existingSipper.mPackages != null ? 246 existingSipper.mPackages.length : 0; 247 final int newPackageLen = sipper.mPackages != null ? 248 sipper.mPackages.length : 0; 249 if (newPackageLen > 0) { 250 String[] newPackages = new String[existingPackageLen + newPackageLen]; 251 if (existingPackageLen > 0) { 252 System.arraycopy(existingSipper.mPackages, 0, newPackages, 0, 253 existingPackageLen); 254 } 255 System.arraycopy(sipper.mPackages, 0, newPackages, existingPackageLen, 256 newPackageLen); 257 existingSipper.mPackages = newPackages; 258 } 259 } 260 } else { 261 results.add(sipper); 262 } 263 } 264 265 final int numUidSippers = uidList.size(); 266 for (int i = 0; i < numUidSippers; i++) { 267 results.add(uidList.valueAt(i)); 268 } 269 270 // The sort order must have changed, so re-sort based on total power use. 271 Collections.sort(results, new Comparator<BatterySipper>() { 272 @Override 273 public int compare(BatterySipper a, BatterySipper b) { 274 return Double.compare(b.totalPowerMah, a.totalPowerMah); 275 } 276 }); 277 return results; 278 } 279 refreshStats()280 protected void refreshStats() { 281 super.refreshStats(); 282 updatePreference(mHistPref); 283 cacheRemoveAllPrefs(mAppListGroup); 284 mAppListGroup.setOrderingAsAdded(false); 285 boolean addedSome = false; 286 287 final PowerProfile powerProfile = mStatsHelper.getPowerProfile(); 288 final BatteryStats stats = mStatsHelper.getStats(); 289 final double averagePower = powerProfile.getAveragePower(PowerProfile.POWER_SCREEN_FULL); 290 291 TypedValue value = new TypedValue(); 292 getContext().getTheme().resolveAttribute(android.R.attr.colorControlNormal, value, true); 293 int colorControl = getContext().getColor(value.resourceId); 294 295 if (averagePower >= MIN_AVERAGE_POWER_THRESHOLD_MILLI_AMP || USE_FAKE_DATA) { 296 final List<BatterySipper> usageList = getCoalescedUsageList( 297 USE_FAKE_DATA ? getFakeStats() : mStatsHelper.getUsageList()); 298 299 final int dischargeAmount = USE_FAKE_DATA ? 5000 300 : stats != null ? stats.getDischargeAmount(mStatsType) : 0; 301 final int numSippers = usageList.size(); 302 for (int i = 0; i < numSippers; i++) { 303 final BatterySipper sipper = usageList.get(i); 304 if ((sipper.totalPowerMah * SECONDS_IN_HOUR) < MIN_POWER_THRESHOLD_MILLI_AMP) { 305 continue; 306 } 307 double totalPower = USE_FAKE_DATA ? 4000 : mStatsHelper.getTotalPower(); 308 final double percentOfTotal = 309 ((sipper.totalPowerMah / totalPower) * dischargeAmount); 310 if (((int) (percentOfTotal + .5)) < 1) { 311 continue; 312 } 313 if (sipper.drainType == BatterySipper.DrainType.OVERCOUNTED) { 314 // Don't show over-counted unless it is at least 2/3 the size of 315 // the largest real entry, and its percent of total is more significant 316 if (sipper.totalPowerMah < ((mStatsHelper.getMaxRealPower()*2)/3)) { 317 continue; 318 } 319 if (percentOfTotal < 10) { 320 continue; 321 } 322 if ("user".equals(Build.TYPE)) { 323 continue; 324 } 325 } 326 if (sipper.drainType == BatterySipper.DrainType.UNACCOUNTED) { 327 // Don't show over-counted unless it is at least 1/2 the size of 328 // the largest real entry, and its percent of total is more significant 329 if (sipper.totalPowerMah < (mStatsHelper.getMaxRealPower()/2)) { 330 continue; 331 } 332 if (percentOfTotal < 5) { 333 continue; 334 } 335 if ("user".equals(Build.TYPE)) { 336 continue; 337 } 338 } 339 final UserHandle userHandle = new UserHandle(UserHandle.getUserId(sipper.getUid())); 340 final BatteryEntry entry = new BatteryEntry(getActivity(), mHandler, mUm, sipper); 341 final Drawable badgedIcon = mUm.getBadgedIconForUser(entry.getIcon(), 342 userHandle); 343 final CharSequence contentDescription = mUm.getBadgedLabelForUser(entry.getLabel(), 344 userHandle); 345 final String key = sipper.drainType == DrainType.APP ? sipper.getPackages() != null 346 ? TextUtils.concat(sipper.getPackages()).toString() 347 : String.valueOf(sipper.getUid()) 348 : sipper.drainType.toString(); 349 PowerGaugePreference pref = (PowerGaugePreference) getCachedPreference(key); 350 if (pref == null) { 351 pref = new PowerGaugePreference(getPrefContext(), badgedIcon, 352 contentDescription, entry); 353 pref.setKey(key); 354 } 355 356 final double percentOfMax = (sipper.totalPowerMah * 100) 357 / mStatsHelper.getMaxPower(); 358 sipper.percent = percentOfTotal; 359 pref.setTitle(entry.getLabel()); 360 pref.setOrder(i + 1); 361 pref.setPercent(percentOfMax, percentOfTotal); 362 if (sipper.uidObj != null) { 363 pref.setKey(Integer.toString(sipper.uidObj.getUid())); 364 } 365 if ((sipper.drainType != DrainType.APP || sipper.uidObj.getUid() == 0) 366 && sipper.drainType != DrainType.USER) { 367 pref.setTint(colorControl); 368 } 369 addedSome = true; 370 mAppListGroup.addPreference(pref); 371 if (mAppListGroup.getPreferenceCount() - getCachedCount() 372 > (MAX_ITEMS_TO_LIST + 1)) { 373 break; 374 } 375 } 376 } 377 if (!addedSome) { 378 addNotAvailableMessage(); 379 } 380 removeCachedPrefs(mAppListGroup); 381 382 BatteryEntry.startRequestQueue(); 383 } 384 getFakeStats()385 private static List<BatterySipper> getFakeStats() { 386 ArrayList<BatterySipper> stats = new ArrayList<>(); 387 float use = 5; 388 for (DrainType type : DrainType.values()) { 389 if (type == DrainType.APP) { 390 continue; 391 } 392 stats.add(new BatterySipper(type, null, use)); 393 use += 5; 394 } 395 for (int i = 0; i < 100; i++) { 396 stats.add(new BatterySipper(DrainType.APP, 397 new FakeUid(Process.FIRST_APPLICATION_UID + i), use)); 398 } 399 stats.add(new BatterySipper(DrainType.APP, 400 new FakeUid(0), use)); 401 402 // Simulate dex2oat process. 403 BatterySipper sipper = new BatterySipper(DrainType.APP, 404 new FakeUid(UserHandle.getSharedAppGid(Process.FIRST_APPLICATION_UID)), 10.0f); 405 sipper.packageWithHighestDrain = "dex2oat"; 406 stats.add(sipper); 407 408 sipper = new BatterySipper(DrainType.APP, 409 new FakeUid(UserHandle.getSharedAppGid(Process.FIRST_APPLICATION_UID + 1)), 10.0f); 410 sipper.packageWithHighestDrain = "dex2oat"; 411 stats.add(sipper); 412 413 sipper = new BatterySipper(DrainType.APP, 414 new FakeUid(UserHandle.getSharedAppGid(Process.LOG_UID)), 9.0f); 415 stats.add(sipper); 416 417 return stats; 418 } 419 420 Handler mHandler = new Handler() { 421 422 @Override 423 public void handleMessage(Message msg) { 424 switch (msg.what) { 425 case BatteryEntry.MSG_UPDATE_NAME_ICON: 426 BatteryEntry entry = (BatteryEntry) msg.obj; 427 PowerGaugePreference pgp = 428 (PowerGaugePreference) findPreference( 429 Integer.toString(entry.sipper.uidObj.getUid())); 430 if (pgp != null) { 431 final int userId = UserHandle.getUserId(entry.sipper.getUid()); 432 final UserHandle userHandle = new UserHandle(userId); 433 pgp.setIcon(mUm.getBadgedIconForUser(entry.getIcon(), userHandle)); 434 pgp.setTitle(entry.name); 435 if (entry.sipper.drainType == DrainType.APP) { 436 pgp.setContentDescription(entry.name); 437 } 438 } 439 break; 440 case BatteryEntry.MSG_REPORT_FULLY_DRAWN: 441 Activity activity = getActivity(); 442 if (activity != null) { 443 activity.reportFullyDrawn(); 444 } 445 break; 446 } 447 super.handleMessage(msg); 448 } 449 }; 450 451 private static class SummaryProvider implements SummaryLoader.SummaryProvider { 452 private final Context mContext; 453 private final SummaryLoader mLoader; 454 SummaryProvider(Context context, SummaryLoader loader)455 private SummaryProvider(Context context, SummaryLoader loader) { 456 mContext = context; 457 mLoader = loader; 458 } 459 460 @Override setListening(boolean listening)461 public void setListening(boolean listening) { 462 if (listening) { 463 // TODO: Listen. 464 BatteryInfo.getBatteryInfo(mContext, new BatteryInfo.Callback() { 465 @Override 466 public void onBatteryInfoLoaded(BatteryInfo info) { 467 mLoader.setSummary(SummaryProvider.this, info.mChargeLabelString); 468 } 469 }); 470 } 471 } 472 } 473 474 public static final SummaryLoader.SummaryProviderFactory SUMMARY_PROVIDER_FACTORY 475 = new SummaryLoader.SummaryProviderFactory() { 476 @Override 477 public SummaryLoader.SummaryProvider createSummaryProvider(Activity activity, 478 SummaryLoader summaryLoader) { 479 return new SummaryProvider(activity, summaryLoader); 480 } 481 }; 482 } 483