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