• 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 
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