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