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