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