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 static com.android.settings.fuelgauge.BatteryBroadcastReceiver.BatteryUpdateType; 20 21 import android.app.Activity; 22 import android.app.LoaderManager; 23 import android.app.LoaderManager.LoaderCallbacks; 24 import android.content.Context; 25 import android.content.Loader; 26 import android.os.BatteryStats; 27 import android.os.Bundle; 28 import android.provider.SearchIndexableResource; 29 import android.support.annotation.VisibleForTesting; 30 import android.text.BidiFormatter; 31 import android.text.format.Formatter; 32 import android.util.SparseArray; 33 import android.view.Menu; 34 import android.view.MenuInflater; 35 import android.view.MenuItem; 36 import android.view.View; 37 import android.view.View.OnLongClickListener; 38 import android.widget.TextView; 39 40 import com.android.internal.logging.nano.MetricsProto.MetricsEvent; 41 import com.android.settings.R; 42 import com.android.settings.SettingsActivity; 43 import com.android.settings.Utils; 44 import com.android.settings.applications.LayoutPreference; 45 import com.android.settings.core.SubSettingLauncher; 46 import com.android.settings.dashboard.SummaryLoader; 47 import com.android.settings.display.BatteryPercentagePreferenceController; 48 import com.android.settings.fuelgauge.anomaly.Anomaly; 49 import com.android.settings.fuelgauge.anomaly.AnomalyDetectionPolicy; 50 import com.android.settings.fuelgauge.batterytip.BatteryTipLoader; 51 import com.android.settings.fuelgauge.batterytip.BatteryTipPreferenceController; 52 import com.android.settings.fuelgauge.batterytip.tips.BatteryTip; 53 import com.android.settings.overlay.FeatureFactory; 54 import com.android.settings.search.BaseSearchIndexProvider; 55 import com.android.settingslib.core.AbstractPreferenceController; 56 import com.android.settingslib.core.lifecycle.Lifecycle; 57 import com.android.settingslib.utils.PowerUtil; 58 import com.android.settingslib.utils.StringUtil; 59 60 import java.util.ArrayList; 61 import java.util.Collections; 62 import java.util.List; 63 64 /** 65 * Displays a list of apps and subsystems that consume power, ordered by how much power was 66 * consumed since the last time it was unplugged. 67 */ 68 public class PowerUsageSummary extends PowerUsageBase implements OnLongClickListener, 69 BatteryTipPreferenceController.BatteryTipListener { 70 71 static final String TAG = "PowerUsageSummary"; 72 73 private static final boolean DEBUG = false; 74 private static final String KEY_BATTERY_HEADER = "battery_header"; 75 private static final String KEY_BATTERY_TIP = "battery_tip"; 76 77 private static final String KEY_SCREEN_USAGE = "screen_usage"; 78 private static final String KEY_TIME_SINCE_LAST_FULL_CHARGE = "last_full_charge"; 79 private static final String KEY_BATTERY_SAVER_SUMMARY = "battery_saver_summary"; 80 81 @VisibleForTesting 82 static final int BATTERY_INFO_LOADER = 1; 83 @VisibleForTesting 84 static final int BATTERY_TIP_LOADER = 2; 85 @VisibleForTesting 86 static final int MENU_STATS_TYPE = Menu.FIRST; 87 @VisibleForTesting 88 static final int MENU_ADVANCED_BATTERY = Menu.FIRST + 1; 89 public static final int DEBUG_INFO_LOADER = 3; 90 91 @VisibleForTesting 92 PowerGaugePreference mScreenUsagePref; 93 @VisibleForTesting 94 PowerGaugePreference mLastFullChargePref; 95 @VisibleForTesting 96 PowerUsageFeatureProvider mPowerFeatureProvider; 97 @VisibleForTesting 98 BatteryUtils mBatteryUtils; 99 @VisibleForTesting 100 LayoutPreference mBatteryLayoutPref; 101 @VisibleForTesting 102 BatteryInfo mBatteryInfo; 103 104 /** 105 * SparseArray that maps uid to {@link Anomaly}, so we could find {@link Anomaly} by uid 106 */ 107 @VisibleForTesting 108 SparseArray<List<Anomaly>> mAnomalySparseArray; 109 @VisibleForTesting 110 BatteryHeaderPreferenceController mBatteryHeaderPreferenceController; 111 @VisibleForTesting 112 boolean mNeedUpdateBatteryTip; 113 @VisibleForTesting 114 BatteryTipPreferenceController mBatteryTipPreferenceController; 115 private int mStatsType = BatteryStats.STATS_SINCE_CHARGED; 116 117 @VisibleForTesting 118 LoaderManager.LoaderCallbacks<BatteryInfo> mBatteryInfoLoaderCallbacks = 119 new LoaderManager.LoaderCallbacks<BatteryInfo>() { 120 121 @Override 122 public Loader<BatteryInfo> onCreateLoader(int i, Bundle bundle) { 123 return new BatteryInfoLoader(getContext(), mStatsHelper); 124 } 125 126 @Override 127 public void onLoadFinished(Loader<BatteryInfo> loader, BatteryInfo batteryInfo) { 128 mBatteryHeaderPreferenceController.updateHeaderPreference(batteryInfo); 129 mBatteryInfo = batteryInfo; 130 updateLastFullChargePreference(); 131 } 132 133 @Override 134 public void onLoaderReset(Loader<BatteryInfo> loader) { 135 // do nothing 136 } 137 }; 138 139 LoaderManager.LoaderCallbacks<List<BatteryInfo>> mBatteryInfoDebugLoaderCallbacks = 140 new LoaderCallbacks<List<BatteryInfo>>() { 141 @Override 142 public Loader<List<BatteryInfo>> onCreateLoader(int i, Bundle bundle) { 143 return new DebugEstimatesLoader(getContext(), mStatsHelper); 144 } 145 146 @Override 147 public void onLoadFinished(Loader<List<BatteryInfo>> loader, 148 List<BatteryInfo> batteryInfos) { 149 updateViews(batteryInfos); 150 } 151 152 @Override 153 public void onLoaderReset(Loader<List<BatteryInfo>> loader) { 154 } 155 }; 156 157 protected void updateViews(List<BatteryInfo> batteryInfos) { 158 final BatteryMeterView batteryView = mBatteryLayoutPref 159 .findViewById(R.id.battery_header_icon); 160 final TextView percentRemaining = 161 mBatteryLayoutPref.findViewById(R.id.battery_percent); 162 final TextView summary1 = mBatteryLayoutPref.findViewById(R.id.summary1); 163 final TextView summary2 = mBatteryLayoutPref.findViewById(R.id.summary2); 164 BatteryInfo oldInfo = batteryInfos.get(0); 165 BatteryInfo newInfo = batteryInfos.get(1); 166 percentRemaining.setText(Utils.formatPercentage(oldInfo.batteryLevel)); 167 168 // set the text to the old estimate (copied from battery info). Note that this 169 // can sometimes say 0 time remaining because battery stats requires the phone 170 // be unplugged for a period of time before being willing ot make an estimate. 171 summary1.setText(mPowerFeatureProvider.getOldEstimateDebugString( 172 Formatter.formatShortElapsedTime(getContext(), 173 PowerUtil.convertUsToMs(oldInfo.remainingTimeUs)))); 174 175 // for this one we can just set the string directly 176 summary2.setText(mPowerFeatureProvider.getEnhancedEstimateDebugString( 177 Formatter.formatShortElapsedTime(getContext(), 178 PowerUtil.convertUsToMs(newInfo.remainingTimeUs)))); 179 180 batteryView.setBatteryLevel(oldInfo.batteryLevel); 181 batteryView.setCharging(!oldInfo.discharging); 182 } 183 184 private LoaderManager.LoaderCallbacks<List<BatteryTip>> mBatteryTipsCallbacks = 185 new LoaderManager.LoaderCallbacks<List<BatteryTip>>() { 186 187 @Override 188 public Loader<List<BatteryTip>> onCreateLoader(int id, Bundle args) { 189 return new BatteryTipLoader(getContext(), mStatsHelper); 190 } 191 192 @Override 193 public void onLoadFinished(Loader<List<BatteryTip>> loader, 194 List<BatteryTip> data) { 195 mBatteryTipPreferenceController.updateBatteryTips(data); 196 } 197 198 @Override 199 public void onLoaderReset(Loader<List<BatteryTip>> loader) { 200 201 } 202 }; 203 204 @Override 205 public void onCreate(Bundle icicle) { 206 super.onCreate(icicle); 207 setAnimationAllowed(true); 208 209 initFeatureProvider(); 210 mBatteryLayoutPref = (LayoutPreference) findPreference(KEY_BATTERY_HEADER); 211 212 mScreenUsagePref = (PowerGaugePreference) findPreference(KEY_SCREEN_USAGE); 213 mLastFullChargePref = (PowerGaugePreference) findPreference( 214 KEY_TIME_SINCE_LAST_FULL_CHARGE); 215 mFooterPreferenceMixin.createFooterPreference().setTitle(R.string.battery_footer_summary); 216 mBatteryUtils = BatteryUtils.getInstance(getContext()); 217 mAnomalySparseArray = new SparseArray<>(); 218 219 restartBatteryInfoLoader(); 220 mBatteryTipPreferenceController.restoreInstanceState(icicle); 221 updateBatteryTipFlag(icicle); 222 } 223 224 @Override 225 public int getMetricsCategory() { 226 return MetricsEvent.FUELGAUGE_POWER_USAGE_SUMMARY_V2; 227 } 228 229 @Override 230 protected String getLogTag() { 231 return TAG; 232 } 233 234 @Override 235 protected int getPreferenceScreenResId() { 236 return R.xml.power_usage_summary; 237 } 238 239 @Override 240 protected List<AbstractPreferenceController> createPreferenceControllers(Context context) { 241 final Lifecycle lifecycle = getLifecycle(); 242 final SettingsActivity activity = (SettingsActivity) getActivity(); 243 final List<AbstractPreferenceController> controllers = new ArrayList<>(); 244 mBatteryHeaderPreferenceController = new BatteryHeaderPreferenceController( 245 context, activity, this /* host */, lifecycle); 246 controllers.add(mBatteryHeaderPreferenceController); 247 mBatteryTipPreferenceController = new BatteryTipPreferenceController(context, 248 KEY_BATTERY_TIP, (SettingsActivity) getActivity(), this /* fragment */, this /* 249 BatteryTipListener */); 250 controllers.add(mBatteryTipPreferenceController); 251 controllers.add(new BatteryPercentagePreferenceController(context)); 252 return controllers; 253 } 254 255 @Override 256 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { 257 if (DEBUG) { 258 menu.add(Menu.NONE, MENU_STATS_TYPE, Menu.NONE, R.string.menu_stats_total) 259 .setIcon(com.android.internal.R.drawable.ic_menu_info_details) 260 .setAlphabeticShortcut('t'); 261 } 262 263 menu.add(Menu.NONE, MENU_ADVANCED_BATTERY, Menu.NONE, R.string.advanced_battery_title); 264 265 super.onCreateOptionsMenu(menu, inflater); 266 } 267 268 @Override 269 public int getHelpResource() { 270 return R.string.help_url_battery; 271 } 272 273 @Override 274 public boolean onOptionsItemSelected(MenuItem item) { 275 switch (item.getItemId()) { 276 case MENU_STATS_TYPE: 277 if (mStatsType == BatteryStats.STATS_SINCE_CHARGED) { 278 mStatsType = BatteryStats.STATS_SINCE_UNPLUGGED; 279 } else { 280 mStatsType = BatteryStats.STATS_SINCE_CHARGED; 281 } 282 refreshUi(BatteryUpdateType.MANUAL); 283 return true; 284 case MENU_ADVANCED_BATTERY: 285 new SubSettingLauncher(getContext()) 286 .setDestination(PowerUsageAdvanced.class.getName()) 287 .setSourceMetricsCategory(getMetricsCategory()) 288 .setTitle(R.string.advanced_battery_title) 289 .launch(); 290 return true; 291 default: 292 return super.onOptionsItemSelected(item); 293 } 294 } 295 296 protected void refreshUi(@BatteryUpdateType int refreshType) { 297 final Context context = getContext(); 298 if (context == null) { 299 return; 300 } 301 302 // Skip BatteryTipLoader if device is rotated or only battery level change 303 if (mNeedUpdateBatteryTip 304 && refreshType != BatteryUpdateType.BATTERY_LEVEL) { 305 restartBatteryTipLoader(); 306 } else { 307 mNeedUpdateBatteryTip = true; 308 } 309 310 // reload BatteryInfo and updateUI 311 restartBatteryInfoLoader(); 312 updateLastFullChargePreference(); 313 mScreenUsagePref.setSubtitle(StringUtil.formatElapsedTime(getContext(), 314 mBatteryUtils.calculateScreenUsageTime(mStatsHelper), false)); 315 } 316 317 @VisibleForTesting 318 void restartBatteryTipLoader() { 319 getLoaderManager().restartLoader(BATTERY_TIP_LOADER, Bundle.EMPTY, mBatteryTipsCallbacks); 320 } 321 322 @VisibleForTesting 323 void setBatteryLayoutPreference(LayoutPreference layoutPreference) { 324 mBatteryLayoutPref = layoutPreference; 325 } 326 327 @VisibleForTesting 328 AnomalyDetectionPolicy getAnomalyDetectionPolicy() { 329 return new AnomalyDetectionPolicy(getContext()); 330 } 331 332 @VisibleForTesting 333 void updateLastFullChargePreference() { 334 if (mBatteryInfo != null && mBatteryInfo.averageTimeToDischarge 335 != Estimate.AVERAGE_TIME_TO_DISCHARGE_UNKNOWN) { 336 mLastFullChargePref.setTitle(R.string.battery_full_charge_last); 337 mLastFullChargePref.setSubtitle( 338 StringUtil.formatElapsedTime(getContext(), mBatteryInfo.averageTimeToDischarge, 339 false /* withSeconds */)); 340 } else { 341 final long lastFullChargeTime = mBatteryUtils.calculateLastFullChargeTime(mStatsHelper, 342 System.currentTimeMillis()); 343 mLastFullChargePref.setTitle(R.string.battery_last_full_charge); 344 mLastFullChargePref.setSubtitle( 345 StringUtil.formatRelativeTime(getContext(), lastFullChargeTime, 346 false /* withSeconds */)); 347 } 348 } 349 350 @VisibleForTesting 351 void showBothEstimates() { 352 final Context context = getContext(); 353 if (context == null 354 || !mPowerFeatureProvider.isEnhancedBatteryPredictionEnabled(context)) { 355 return; 356 } 357 getLoaderManager().restartLoader(DEBUG_INFO_LOADER, Bundle.EMPTY, 358 mBatteryInfoDebugLoaderCallbacks); 359 } 360 361 @VisibleForTesting 362 void initFeatureProvider() { 363 final Context context = getContext(); 364 mPowerFeatureProvider = FeatureFactory.getFactory(context) 365 .getPowerUsageFeatureProvider(context); 366 } 367 368 @VisibleForTesting 369 void updateAnomalySparseArray(List<Anomaly> anomalies) { 370 mAnomalySparseArray.clear(); 371 for (final Anomaly anomaly : anomalies) { 372 if (mAnomalySparseArray.get(anomaly.uid) == null) { 373 mAnomalySparseArray.append(anomaly.uid, new ArrayList<>()); 374 } 375 mAnomalySparseArray.get(anomaly.uid).add(anomaly); 376 } 377 } 378 379 @VisibleForTesting 380 void restartBatteryInfoLoader() { 381 getLoaderManager().restartLoader(BATTERY_INFO_LOADER, Bundle.EMPTY, 382 mBatteryInfoLoaderCallbacks); 383 if (mPowerFeatureProvider.isEstimateDebugEnabled()) { 384 // Set long click action for summary to show debug info 385 View header = mBatteryLayoutPref.findViewById(R.id.summary1); 386 header.setOnLongClickListener(this); 387 } 388 } 389 390 @VisibleForTesting 391 void updateBatteryTipFlag(Bundle icicle) { 392 mNeedUpdateBatteryTip = icicle == null || mBatteryTipPreferenceController.needUpdate(); 393 } 394 395 @Override 396 public boolean onLongClick(View view) { 397 showBothEstimates(); 398 view.setOnLongClickListener(null); 399 return true; 400 } 401 402 @Override 403 protected void restartBatteryStatsLoader(@BatteryUpdateType int refreshType) { 404 super.restartBatteryStatsLoader(refreshType); 405 mBatteryHeaderPreferenceController.quickUpdateHeaderPreference(); 406 } 407 408 @Override 409 public void onSaveInstanceState(Bundle outState) { 410 super.onSaveInstanceState(outState); 411 mBatteryTipPreferenceController.saveInstanceState(outState); 412 } 413 414 @Override 415 public void onBatteryTipHandled(BatteryTip batteryTip) { 416 restartBatteryTipLoader(); 417 } 418 419 private static class SummaryProvider implements SummaryLoader.SummaryProvider { 420 private final Context mContext; 421 private final SummaryLoader mLoader; 422 private final BatteryBroadcastReceiver mBatteryBroadcastReceiver; 423 424 private SummaryProvider(Context context, SummaryLoader loader) { 425 mContext = context; 426 mLoader = loader; 427 mBatteryBroadcastReceiver = new BatteryBroadcastReceiver(mContext); 428 mBatteryBroadcastReceiver.setBatteryChangedListener(type -> { 429 BatteryInfo.getBatteryInfo(mContext, new BatteryInfo.Callback() { 430 @Override 431 public void onBatteryInfoLoaded(BatteryInfo info) { 432 mLoader.setSummary(SummaryProvider.this, getDashboardLabel(mContext, info)); 433 } 434 }, true /* shortString */); 435 }); 436 } 437 438 @Override 439 public void setListening(boolean listening) { 440 if (listening) { 441 mBatteryBroadcastReceiver.register(); 442 } else { 443 mBatteryBroadcastReceiver.unRegister(); 444 } 445 } 446 } 447 448 @VisibleForTesting 449 static CharSequence getDashboardLabel(Context context, BatteryInfo info) { 450 CharSequence label; 451 final BidiFormatter formatter = BidiFormatter.getInstance(); 452 if (info.remainingLabel == null) { 453 label = info.batteryPercentString; 454 } else { 455 label = context.getString(R.string.power_remaining_settings_home_page, 456 formatter.unicodeWrap(info.batteryPercentString), 457 formatter.unicodeWrap(info.remainingLabel)); 458 } 459 return label; 460 } 461 462 public static final SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = 463 new BaseSearchIndexProvider() { 464 @Override 465 public List<SearchIndexableResource> getXmlResourcesToIndex( 466 Context context, boolean enabled) { 467 final SearchIndexableResource sir = new SearchIndexableResource(context); 468 sir.xmlResId = R.xml.power_usage_summary; 469 return Collections.singletonList(sir); 470 } 471 472 @Override 473 public List<String> getNonIndexableKeys(Context context) { 474 List<String> niks = super.getNonIndexableKeys(context); 475 niks.add(KEY_BATTERY_SAVER_SUMMARY); 476 return niks; 477 } 478 }; 479 480 public static final SummaryLoader.SummaryProviderFactory SUMMARY_PROVIDER_FACTORY 481 = new SummaryLoader.SummaryProviderFactory() { 482 @Override 483 public SummaryLoader.SummaryProvider createSummaryProvider(Activity activity, 484 SummaryLoader summaryLoader) { 485 return new SummaryProvider(activity, summaryLoader); 486 } 487 }; 488 } 489