• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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