1 /* 2 * Copyright (C) 2022 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 package com.android.settings.fuelgauge.batteryusage; 17 18 import static com.android.settings.fuelgauge.BatteryBroadcastReceiver.BatteryUpdateType; 19 20 import android.app.settings.SettingsEnums; 21 import android.content.Context; 22 import android.database.ContentObserver; 23 import android.net.Uri; 24 import android.os.AsyncTask; 25 import android.os.Bundle; 26 import android.os.Handler; 27 import android.os.Looper; 28 import android.provider.SearchIndexableResource; 29 import android.util.Log; 30 import android.util.Pair; 31 32 import androidx.annotation.VisibleForTesting; 33 import androidx.loader.app.LoaderManager; 34 import androidx.loader.content.Loader; 35 import androidx.preference.PreferenceScreen; 36 import androidx.recyclerview.widget.RecyclerView; 37 38 import com.android.settings.R; 39 import com.android.settings.SettingsActivity; 40 import com.android.settings.fuelgauge.BatteryBroadcastReceiver; 41 import com.android.settings.fuelgauge.PowerUsageFeatureProvider; 42 import com.android.settings.overlay.FeatureFactory; 43 import com.android.settings.search.BaseSearchIndexProvider; 44 import com.android.settingslib.core.AbstractPreferenceController; 45 import com.android.settingslib.search.SearchIndexable; 46 import com.android.settingslib.utils.AsyncLoaderCompat; 47 48 import java.util.ArrayList; 49 import java.util.Arrays; 50 import java.util.Comparator; 51 import java.util.List; 52 import java.util.Map; 53 import java.util.Optional; 54 import java.util.Set; 55 import java.util.concurrent.ExecutorService; 56 import java.util.concurrent.Executors; 57 import java.util.function.Predicate; 58 59 /** Advanced power usage. */ 60 @SearchIndexable(forTarget = SearchIndexable.ALL & ~SearchIndexable.ARC) 61 public class PowerUsageAdvanced extends PowerUsageBase { 62 private static final String TAG = "AdvancedBatteryUsage"; 63 private static final String KEY_REFRESH_TYPE = "refresh_type"; 64 private static final String KEY_BATTERY_CHART = "battery_chart"; 65 66 @VisibleForTesting BatteryHistoryPreference mHistPref; 67 68 @VisibleForTesting 69 final BatteryLevelDataLoaderCallbacks mBatteryLevelDataLoaderCallbacks = 70 new BatteryLevelDataLoaderCallbacks(); 71 72 private boolean mIsChartDataLoaded = false; 73 private long mResumeTimestamp; 74 private Map<Integer, Map<Integer, BatteryDiffData>> mBatteryUsageMap; 75 76 private final ExecutorService mExecutor = Executors.newSingleThreadExecutor(); 77 private final Handler mHandler = new Handler(Looper.getMainLooper()); 78 private final ContentObserver mBatteryObserver = 79 new ContentObserver(mHandler) { 80 @Override 81 public void onChange(boolean selfChange) { 82 Log.d(TAG, "onBatteryContentChange: " + selfChange); 83 mIsChartDataLoaded = false; 84 restartBatteryStatsLoader(BatteryBroadcastReceiver.BatteryUpdateType.MANUAL); 85 } 86 }; 87 88 @VisibleForTesting BatteryTipsController mBatteryTipsController; 89 @VisibleForTesting BatteryChartPreferenceController mBatteryChartPreferenceController; 90 @VisibleForTesting ScreenOnTimeController mScreenOnTimeController; 91 @VisibleForTesting BatteryUsageBreakdownController mBatteryUsageBreakdownController; 92 @VisibleForTesting Optional<BatteryLevelData> mBatteryLevelData; 93 @VisibleForTesting Optional<AnomalyEventWrapper> mHighlightEventWrapper; 94 95 @Override onCreate(Bundle icicle)96 public void onCreate(Bundle icicle) { 97 super.onCreate(icicle); 98 mHistPref = findPreference(KEY_BATTERY_CHART); 99 setBatteryChartPreferenceController(); 100 AsyncTask.execute(() -> { 101 if (getContext() != null) { 102 BootBroadcastReceiver.invokeJobRecheck(getContext()); 103 } 104 }); 105 } 106 107 @Override onDestroy()108 public void onDestroy() { 109 super.onDestroy(); 110 if (getActivity().isChangingConfigurations()) { 111 BatteryEntry.clearUidCache(); 112 } 113 mExecutor.shutdown(); 114 } 115 116 @Override getMetricsCategory()117 public int getMetricsCategory() { 118 return SettingsEnums.FUELGAUGE_BATTERY_HISTORY_DETAIL; 119 } 120 121 @Override getLogTag()122 protected String getLogTag() { 123 return TAG; 124 } 125 126 @Override getPreferenceScreenResId()127 protected int getPreferenceScreenResId() { 128 return R.xml.power_usage_advanced; 129 } 130 131 @Override onPause()132 public void onPause() { 133 super.onPause(); 134 // Resets the flag to reload usage data in onResume() callback. 135 mIsChartDataLoaded = false; 136 final Uri uri = DatabaseUtils.BATTERY_CONTENT_URI; 137 if (uri != null) { 138 getContext().getContentResolver().unregisterContentObserver(mBatteryObserver); 139 } 140 } 141 142 @Override onResume()143 public void onResume() { 144 super.onResume(); 145 mResumeTimestamp = System.currentTimeMillis(); 146 final Uri uri = DatabaseUtils.BATTERY_CONTENT_URI; 147 if (uri != null) { 148 getContext() 149 .getContentResolver() 150 .registerContentObserver(uri, /*notifyForDescendants*/ true, mBatteryObserver); 151 } 152 } 153 154 @Override onCreateAdapter(PreferenceScreen preferenceScreen)155 protected RecyclerView.Adapter onCreateAdapter(PreferenceScreen preferenceScreen) { 156 final RecyclerView.Adapter adapter = super.onCreateAdapter(preferenceScreen); 157 adapter.setHasStableIds(true); 158 return adapter; 159 } 160 161 @Override createPreferenceControllers(Context context)162 protected List<AbstractPreferenceController> createPreferenceControllers(Context context) { 163 final List<AbstractPreferenceController> controllers = new ArrayList<>(); 164 mBatteryTipsController = new BatteryTipsController(context); 165 mBatteryChartPreferenceController = 166 new BatteryChartPreferenceController( 167 context, getSettingsLifecycle(), (SettingsActivity) getActivity()); 168 mScreenOnTimeController = new ScreenOnTimeController(context); 169 mBatteryUsageBreakdownController = 170 new BatteryUsageBreakdownController( 171 context, getSettingsLifecycle(), (SettingsActivity) getActivity(), this); 172 173 controllers.add(mBatteryTipsController); 174 controllers.add(mBatteryChartPreferenceController); 175 controllers.add(mScreenOnTimeController); 176 controllers.add(mBatteryUsageBreakdownController); 177 setBatteryChartPreferenceController(); 178 mBatteryChartPreferenceController.setOnSelectedIndexUpdatedListener( 179 this::onSelectedSlotDataUpdated); 180 181 // Force UI refresh if battery usage data was loaded before UI initialization. 182 onSelectedSlotDataUpdated(); 183 return controllers; 184 } 185 186 @Override refreshUi(@atteryUpdateType int refreshType)187 protected void refreshUi(@BatteryUpdateType int refreshType) { 188 // Do nothing 189 } 190 191 @Override restartBatteryStatsLoader(int refreshType)192 protected void restartBatteryStatsLoader(int refreshType) { 193 final Bundle bundle = new Bundle(); 194 bundle.putInt(KEY_REFRESH_TYPE, refreshType); 195 if (!mIsChartDataLoaded) { 196 mIsChartDataLoaded = true; 197 mBatteryLevelData = null; 198 mBatteryUsageMap = null; 199 mHighlightEventWrapper = null; 200 restartLoader( 201 LoaderIndex.BATTERY_LEVEL_DATA_LOADER, 202 bundle, 203 mBatteryLevelDataLoaderCallbacks); 204 } 205 } 206 onBatteryLevelDataUpdate(BatteryLevelData batteryLevelData)207 private void onBatteryLevelDataUpdate(BatteryLevelData batteryLevelData) { 208 if (!isResumed()) { 209 return; 210 } 211 mBatteryLevelData = Optional.ofNullable(batteryLevelData); 212 if (mBatteryChartPreferenceController != null) { 213 mBatteryChartPreferenceController.onBatteryLevelDataUpdate(batteryLevelData); 214 Log.d( 215 TAG, 216 String.format( 217 "Battery chart shows in %d millis", 218 System.currentTimeMillis() - mResumeTimestamp)); 219 } 220 } 221 onBatteryDiffDataMapUpdate(Map<Long, BatteryDiffData> batteryDiffDataMap)222 private void onBatteryDiffDataMapUpdate(Map<Long, BatteryDiffData> batteryDiffDataMap) { 223 if (!isResumed() || mBatteryLevelData == null) { 224 return; 225 } 226 mHandler.post(() -> { 227 mBatteryUsageMap = 228 DataProcessor.generateBatteryUsageMap( 229 getContext(), batteryDiffDataMap, mBatteryLevelData.orElse(null)); 230 Log.d(TAG, "onBatteryDiffDataMapUpdate: " + mBatteryUsageMap); 231 DataProcessor.loadLabelAndIcon(mBatteryUsageMap); 232 onSelectedSlotDataUpdated(); 233 detectAnomaly(); 234 logScreenUsageTime(); 235 if (mBatteryChartPreferenceController != null 236 && mBatteryLevelData.isEmpty() 237 && isBatteryUsageMapNullOrEmpty()) { 238 // No available battery usage and battery level data. 239 mBatteryChartPreferenceController.showEmptyChart(); 240 } 241 }); 242 } 243 onSelectedSlotDataUpdated()244 private void onSelectedSlotDataUpdated() { 245 if (mBatteryChartPreferenceController == null 246 || mScreenOnTimeController == null 247 || mBatteryUsageBreakdownController == null 248 || mBatteryUsageMap == null) { 249 return; 250 } 251 final int dailyIndex = mBatteryChartPreferenceController.getDailyChartIndex(); 252 final int hourlyIndex = mBatteryChartPreferenceController.getHourlyChartIndex(); 253 final String slotInformation = 254 mBatteryChartPreferenceController.getSlotInformation( 255 /* isAccessibilityText= */ false); 256 final String accessibilitySlotInformation = 257 mBatteryChartPreferenceController.getSlotInformation( 258 /* isAccessibilityText= */ true); 259 final BatteryDiffData slotUsageData = mBatteryUsageMap.get(dailyIndex).get(hourlyIndex); 260 mScreenOnTimeController.handleScreenOnTimeUpdated( 261 slotUsageData != null ? slotUsageData.getScreenOnTime() : 0L, 262 slotInformation, 263 accessibilitySlotInformation); 264 // Hide card tips if the related highlight slot was clicked. 265 if (isAppsAnomalyEventFocused()) { 266 mBatteryTipsController.acceptTipsCard(); 267 } 268 mBatteryUsageBreakdownController.handleBatteryUsageUpdated( 269 slotUsageData, 270 slotInformation, 271 accessibilitySlotInformation, 272 isBatteryUsageMapNullOrEmpty(), 273 isAppsAnomalyEventFocused(), 274 mHighlightEventWrapper); 275 Log.d( 276 TAG, 277 String.format( 278 "Battery usage list shows in %d millis", 279 System.currentTimeMillis() - mResumeTimestamp)); 280 } 281 detectAnomaly()282 private void detectAnomaly() { 283 mExecutor.execute( 284 () -> { 285 final PowerUsageFeatureProvider powerUsageFeatureProvider = 286 FeatureFactory.getFeatureFactory().getPowerUsageFeatureProvider(); 287 final PowerAnomalyEventList anomalyEventList = 288 powerUsageFeatureProvider.detectPowerAnomaly( 289 getContext(), 290 /* displayDrain= */ 0, 291 DetectRequestSourceType.TYPE_USAGE_UI); 292 mHandler.post(() -> onAnomalyDetected(anomalyEventList)); 293 }); 294 } 295 onAnomalyDetected(PowerAnomalyEventList anomalyEventList)296 private void onAnomalyDetected(PowerAnomalyEventList anomalyEventList) { 297 if (!isResumed() || anomalyEventList == null) { 298 return; 299 } 300 logPowerAnomalyEventList(anomalyEventList); 301 final Set<String> dismissedPowerAnomalyKeys = 302 DatabaseUtils.getDismissedPowerAnomalyKeys(getContext()); 303 Log.d(TAG, "dismissedPowerAnomalyKeys = " + dismissedPowerAnomalyKeys); 304 305 // Choose an app anomaly event with highest score to show highlight slot 306 final PowerAnomalyEvent highlightEvent = 307 getAnomalyEvent(anomalyEventList, PowerAnomalyEvent::hasWarningItemInfo); 308 // Choose an event never dismissed to show as card. 309 // If the slot is already highlighted, the tips card should be the corresponding app 310 // or settings anomaly event. 311 final PowerAnomalyEvent tipsCardEvent = 312 getAnomalyEvent( 313 anomalyEventList, 314 event -> 315 !dismissedPowerAnomalyKeys.contains(event.getDismissRecordKey()) 316 && (event.equals(highlightEvent) 317 || !event.hasWarningItemInfo())); 318 onDisplayAnomalyEventUpdated(tipsCardEvent, highlightEvent); 319 } 320 321 @VisibleForTesting onDisplayAnomalyEventUpdated( PowerAnomalyEvent tipsCardEvent, PowerAnomalyEvent highlightEvent)322 void onDisplayAnomalyEventUpdated( 323 PowerAnomalyEvent tipsCardEvent, PowerAnomalyEvent highlightEvent) { 324 if (mBatteryTipsController == null 325 || mBatteryChartPreferenceController == null 326 || mBatteryUsageBreakdownController == null) { 327 return; 328 } 329 330 final boolean isSameAnomalyEvent = (tipsCardEvent == highlightEvent); 331 // Update battery tips card preference & behaviour 332 mBatteryTipsController.setOnAnomalyConfirmListener(null); 333 mBatteryTipsController.setOnAnomalyRejectListener(null); 334 final AnomalyEventWrapper tipsCardEventWrapper = 335 (tipsCardEvent == null) 336 ? null 337 : new AnomalyEventWrapper(getContext(), tipsCardEvent); 338 if (tipsCardEventWrapper != null) { 339 tipsCardEventWrapper.setRelatedBatteryDiffEntry( 340 findRelatedBatteryDiffEntry(tipsCardEventWrapper)); 341 } 342 mBatteryTipsController.handleBatteryTipsCardUpdated( 343 tipsCardEventWrapper, isSameAnomalyEvent); 344 345 // Update highlight slot effect in battery chart view 346 Pair<Integer, Integer> highlightSlotIndexPair = 347 Pair.create( 348 BatteryChartViewModel.SELECTED_INDEX_INVALID, 349 BatteryChartViewModel.SELECTED_INDEX_INVALID); 350 mHighlightEventWrapper = 351 Optional.ofNullable( 352 isSameAnomalyEvent 353 ? tipsCardEventWrapper 354 : ((highlightEvent != null) 355 ? new AnomalyEventWrapper(getContext(), highlightEvent) 356 : null)); 357 if (mBatteryLevelData != null 358 && mBatteryLevelData.isPresent() 359 && mHighlightEventWrapper.isPresent() 360 && mHighlightEventWrapper.get().hasHighlightSlotPair(mBatteryLevelData.get())) { 361 highlightSlotIndexPair = 362 mHighlightEventWrapper.get().getHighlightSlotPair(mBatteryLevelData.get()); 363 if (isSameAnomalyEvent) { 364 // For main button, focus on highlight slot when clicked 365 mBatteryTipsController.setOnAnomalyConfirmListener( 366 () -> { 367 mBatteryChartPreferenceController.selectHighlightSlotIndex(); 368 mBatteryTipsController.acceptTipsCard(); 369 }); 370 } 371 } 372 mBatteryChartPreferenceController.onHighlightSlotIndexUpdate( 373 highlightSlotIndexPair.first, highlightSlotIndexPair.second); 374 } 375 376 @VisibleForTesting findRelatedBatteryDiffEntry(AnomalyEventWrapper eventWrapper)377 BatteryDiffEntry findRelatedBatteryDiffEntry(AnomalyEventWrapper eventWrapper) { 378 if (eventWrapper == null 379 || mBatteryLevelData == null 380 || mBatteryLevelData.isEmpty() 381 || !eventWrapper.hasHighlightSlotPair(mBatteryLevelData.get()) 382 || !eventWrapper.hasAnomalyEntryKey() 383 || mBatteryUsageMap == null) { 384 return null; 385 } 386 final Pair<Integer, Integer> highlightSlotIndexPair = 387 eventWrapper.getHighlightSlotPair(mBatteryLevelData.get()); 388 final BatteryDiffData relatedDiffData = 389 mBatteryUsageMap 390 .get(highlightSlotIndexPair.first) 391 .get(highlightSlotIndexPair.second); 392 final String anomalyEntryKey = eventWrapper.getAnomalyEntryKey(); 393 if (relatedDiffData == null || anomalyEntryKey == null) { 394 return null; 395 } 396 for (BatteryDiffEntry entry : relatedDiffData.getAppDiffEntryList()) { 397 if (anomalyEntryKey.equals(entry.getKey())) { 398 return entry; 399 } 400 } 401 return null; 402 } 403 setBatteryChartPreferenceController()404 private void setBatteryChartPreferenceController() { 405 if (mHistPref != null && mBatteryChartPreferenceController != null) { 406 mHistPref.setChartPreferenceController(mBatteryChartPreferenceController); 407 } 408 } 409 isBatteryUsageMapNullOrEmpty()410 private boolean isBatteryUsageMapNullOrEmpty() { 411 final BatteryDiffData allBatteryDiffData = getAllBatteryDiffData(mBatteryUsageMap); 412 // If all data is null or empty, each slot must be null or empty. 413 return allBatteryDiffData == null 414 || (allBatteryDiffData.getAppDiffEntryList().isEmpty() 415 && allBatteryDiffData.getSystemDiffEntryList().isEmpty()); 416 } 417 isAppsAnomalyEventFocused()418 private boolean isAppsAnomalyEventFocused() { 419 return mBatteryChartPreferenceController != null 420 && mBatteryChartPreferenceController.isHighlightSlotFocused(); 421 } 422 logScreenUsageTime()423 private void logScreenUsageTime() { 424 final BatteryDiffData allBatteryDiffData = getAllBatteryDiffData(mBatteryUsageMap); 425 if (allBatteryDiffData == null) { 426 return; 427 } 428 long totalForegroundUsageTime = 0; 429 for (final BatteryDiffEntry entry : allBatteryDiffData.getAppDiffEntryList()) { 430 totalForegroundUsageTime += entry.mForegroundUsageTimeInMs; 431 } 432 mMetricsFeatureProvider.action( 433 getContext(), 434 SettingsEnums.ACTION_BATTERY_USAGE_SCREEN_ON_TIME, 435 (int) allBatteryDiffData.getScreenOnTime()); 436 mMetricsFeatureProvider.action( 437 getContext(), 438 SettingsEnums.ACTION_BATTERY_USAGE_FOREGROUND_USAGE_TIME, 439 (int) totalForegroundUsageTime); 440 } 441 442 @VisibleForTesting getAnomalyEvent( PowerAnomalyEventList anomalyEventList, Predicate<PowerAnomalyEvent> predicate)443 static PowerAnomalyEvent getAnomalyEvent( 444 PowerAnomalyEventList anomalyEventList, Predicate<PowerAnomalyEvent> predicate) { 445 if (anomalyEventList == null || anomalyEventList.getPowerAnomalyEventsCount() == 0) { 446 return null; 447 } 448 449 final PowerAnomalyEvent filterAnomalyEvent = 450 anomalyEventList.getPowerAnomalyEventsList().stream() 451 .filter(predicate) 452 .max(Comparator.comparing(PowerAnomalyEvent::getScore)) 453 .orElse(null); 454 Log.d(TAG, "filterAnomalyEvent = " 455 + (filterAnomalyEvent == null ? null : filterAnomalyEvent.getEventId())); 456 return filterAnomalyEvent; 457 } 458 logPowerAnomalyEventList(PowerAnomalyEventList anomalyEventList)459 private static void logPowerAnomalyEventList(PowerAnomalyEventList anomalyEventList) { 460 final StringBuilder stringBuilder = new StringBuilder(); 461 for (PowerAnomalyEvent anomalyEvent : anomalyEventList.getPowerAnomalyEventsList()) { 462 stringBuilder.append(anomalyEvent.getEventId()).append(", "); 463 } 464 Log.d(TAG, "anomalyEventList = [" + stringBuilder + "]"); 465 } 466 getAllBatteryDiffData( Map<Integer, Map<Integer, BatteryDiffData>> batteryUsageMap)467 private static BatteryDiffData getAllBatteryDiffData( 468 Map<Integer, Map<Integer, BatteryDiffData>> batteryUsageMap) { 469 return batteryUsageMap == null 470 ? null 471 : batteryUsageMap 472 .get(BatteryChartViewModel.SELECTED_INDEX_ALL) 473 .get(BatteryChartViewModel.SELECTED_INDEX_ALL); 474 } 475 476 public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = 477 new BaseSearchIndexProvider() { 478 @Override 479 public List<SearchIndexableResource> getXmlResourcesToIndex( 480 Context context, boolean enabled) { 481 final SearchIndexableResource sir = new SearchIndexableResource(context); 482 sir.xmlResId = R.xml.power_usage_advanced; 483 return Arrays.asList(sir); 484 } 485 486 @Override 487 public List<AbstractPreferenceController> createPreferenceControllers( 488 Context context) { 489 final List<AbstractPreferenceController> controllers = new ArrayList<>(); 490 controllers.add( 491 new BatteryChartPreferenceController( 492 context, null /* lifecycle */, null /* activity */)); 493 controllers.add((new ScreenOnTimeController(context))); 494 controllers.add( 495 new BatteryUsageBreakdownController( 496 context, 497 null /* lifecycle */, 498 null /* activity */, 499 null /* fragment */)); 500 controllers.add(new BatteryTipsController(context)); 501 return controllers; 502 } 503 }; 504 505 private class BatteryLevelDataLoaderCallbacks 506 implements LoaderManager.LoaderCallbacks<BatteryLevelData> { 507 @Override onCreateLoader(int id, Bundle bundle)508 public Loader<BatteryLevelData> onCreateLoader(int id, Bundle bundle) { 509 return new AsyncLoaderCompat<BatteryLevelData>(getContext().getApplicationContext()) { 510 @Override 511 protected void onDiscardResult(BatteryLevelData result) {} 512 513 @Override 514 public BatteryLevelData loadInBackground() { 515 Context context = getContext(); 516 return DataProcessManager.getBatteryLevelData( 517 context, 518 getLifecycle(), 519 new UserIdsSeries(context, /* isNonUIRequest= */ false), 520 /* isFromPeriodJob= */ false, 521 PowerUsageAdvanced.this::onBatteryDiffDataMapUpdate); 522 } 523 }; 524 } 525 526 @Override onLoadFinished( Loader<BatteryLevelData> loader, BatteryLevelData batteryLevelData)527 public void onLoadFinished( 528 Loader<BatteryLevelData> loader, BatteryLevelData batteryLevelData) { 529 PowerUsageAdvanced.this.onBatteryLevelDataUpdate(batteryLevelData); 530 } 531 532 @Override onLoaderReset(Loader<BatteryLevelData> loader)533 public void onLoaderReset(Loader<BatteryLevelData> loader) {} 534 } 535 } 536