• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  * Licensed under the Apache License, Version 2.0 (the "License");
4  * you may not use this file except in compliance with the License.
5  * You may obtain a copy of the License at
6  *
7  *      http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software
10  * distributed under the License is distributed on an "AS IS" BASIS,
11  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12  * See the License for the specific language governing permissions and
13  * limitations under the License.
14  *
15  */
16 
17 package com.android.settings.fuelgauge;
18 
19 import android.app.settings.SettingsEnums;
20 import android.content.Context;
21 import android.content.res.Configuration;
22 import android.graphics.drawable.Drawable;
23 import android.os.AsyncTask;
24 import android.os.Bundle;
25 import android.os.Handler;
26 import android.os.Looper;
27 import android.text.TextUtils;
28 import android.text.format.DateFormat;
29 import android.text.format.DateUtils;
30 import android.util.Log;
31 import android.util.Pair;
32 
33 import androidx.annotation.VisibleForTesting;
34 import androidx.preference.Preference;
35 import androidx.preference.PreferenceGroup;
36 import androidx.preference.PreferenceScreen;
37 
38 import com.android.settings.R;
39 import com.android.settings.SettingsActivity;
40 import com.android.settings.core.InstrumentedPreferenceFragment;
41 import com.android.settings.core.PreferenceControllerMixin;
42 import com.android.settings.overlay.FeatureFactory;
43 import com.android.settingslib.core.AbstractPreferenceController;
44 import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
45 import com.android.settingslib.core.lifecycle.Lifecycle;
46 import com.android.settingslib.core.lifecycle.LifecycleObserver;
47 import com.android.settingslib.core.lifecycle.events.OnCreate;
48 import com.android.settingslib.core.lifecycle.events.OnDestroy;
49 import com.android.settingslib.core.lifecycle.events.OnResume;
50 import com.android.settingslib.core.lifecycle.events.OnSaveInstanceState;
51 import com.android.settingslib.utils.StringUtil;
52 import com.android.settingslib.widget.FooterPreference;
53 
54 import java.util.ArrayList;
55 import java.util.Arrays;
56 import java.util.Collections;
57 import java.util.HashMap;
58 import java.util.List;
59 import java.util.Map;
60 
61 /** Controls the update for chart graph and the list items. */
62 public class BatteryChartPreferenceController extends AbstractPreferenceController
63         implements PreferenceControllerMixin, LifecycleObserver, OnCreate, OnDestroy,
64                 OnSaveInstanceState, BatteryChartView.OnSelectListener, OnResume,
65                 ExpandDividerPreference.OnExpandListener {
66     private static final String TAG = "BatteryChartPreferenceController";
67     private static final String KEY_FOOTER_PREF = "battery_graph_footer";
68 
69     /** Desired battery history size for timestamp slots. */
70     public static final int DESIRED_HISTORY_SIZE = 25;
71     private static final int CHART_LEVEL_ARRAY_SIZE = 13;
72     private static final int CHART_KEY_ARRAY_SIZE = DESIRED_HISTORY_SIZE;
73     private static final long VALID_USAGE_TIME_DURATION = DateUtils.HOUR_IN_MILLIS * 2;
74     private static final long VALID_DIFF_DURATION = DateUtils.MINUTE_IN_MILLIS * 3;
75 
76     // Keys for bundle instance to restore configurations.
77     private static final String KEY_EXPAND_SYSTEM_INFO = "expand_system_info";
78     private static final String KEY_CURRENT_TIME_SLOT = "current_time_slot";
79 
80     private static int sUiMode = Configuration.UI_MODE_NIGHT_UNDEFINED;
81 
82     @VisibleForTesting
83     Map<Integer, List<BatteryDiffEntry>> mBatteryIndexedMap;
84 
85     @VisibleForTesting Context mPrefContext;
86     @VisibleForTesting BatteryUtils mBatteryUtils;
87     @VisibleForTesting PreferenceGroup mAppListPrefGroup;
88     @VisibleForTesting BatteryChartView mBatteryChartView;
89     @VisibleForTesting ExpandDividerPreference mExpandDividerPreference;
90 
91     @VisibleForTesting boolean mIsExpanded = false;
92     @VisibleForTesting int[] mBatteryHistoryLevels;
93     @VisibleForTesting long[] mBatteryHistoryKeys;
94     @VisibleForTesting int mTrapezoidIndex = BatteryChartView.SELECTED_INDEX_INVALID;
95 
96     private boolean mIs24HourFormat = false;
97     private boolean mIsFooterPrefAdded = false;
98     private PreferenceScreen mPreferenceScreen;
99     private FooterPreference mFooterPreference;
100 
101     private final String mPreferenceKey;
102     private final SettingsActivity mActivity;
103     private final InstrumentedPreferenceFragment mFragment;
104     private final CharSequence[] mNotAllowShowEntryPackages;
105     private final CharSequence[] mNotAllowShowSummaryPackages;
106     private final MetricsFeatureProvider mMetricsFeatureProvider;
107     private final Handler mHandler = new Handler(Looper.getMainLooper());
108 
109     // Preference cache to avoid create new instance each time.
110     @VisibleForTesting
111     final Map<String, Preference> mPreferenceCache = new HashMap<>();
112     @VisibleForTesting
113     final List<BatteryDiffEntry> mSystemEntries = new ArrayList<>();
114 
BatteryChartPreferenceController( Context context, String preferenceKey, Lifecycle lifecycle, SettingsActivity activity, InstrumentedPreferenceFragment fragment)115     public BatteryChartPreferenceController(
116             Context context, String preferenceKey,
117             Lifecycle lifecycle, SettingsActivity activity,
118             InstrumentedPreferenceFragment fragment) {
119         super(context);
120         mActivity = activity;
121         mFragment = fragment;
122         mPreferenceKey = preferenceKey;
123         mIs24HourFormat = DateFormat.is24HourFormat(context);
124         mNotAllowShowSummaryPackages = context.getResources()
125             .getTextArray(R.array.allowlist_hide_summary_in_battery_usage);
126         mNotAllowShowEntryPackages = context.getResources()
127             .getTextArray(R.array.allowlist_hide_entry_in_battery_usage);
128         mMetricsFeatureProvider =
129             FeatureFactory.getFactory(mContext).getMetricsFeatureProvider();
130         if (lifecycle != null) {
131             lifecycle.addObserver(this);
132         }
133     }
134 
135     @Override
onCreate(Bundle savedInstanceState)136     public void onCreate(Bundle savedInstanceState) {
137         if (savedInstanceState == null) {
138             return;
139         }
140         mTrapezoidIndex =
141             savedInstanceState.getInt(KEY_CURRENT_TIME_SLOT, mTrapezoidIndex);
142         mIsExpanded =
143             savedInstanceState.getBoolean(KEY_EXPAND_SYSTEM_INFO, mIsExpanded);
144         Log.d(TAG, String.format("onCreate() slotIndex=%d isExpanded=%b",
145             mTrapezoidIndex, mIsExpanded));
146     }
147 
148     @Override
onResume()149     public void onResume() {
150         final int currentUiMode =
151             mContext.getResources().getConfiguration().uiMode
152                 & Configuration.UI_MODE_NIGHT_MASK;
153         if (sUiMode != currentUiMode) {
154             sUiMode = currentUiMode;
155             BatteryDiffEntry.clearCache();
156             Log.d(TAG, "clear icon and label cache since uiMode is changed");
157         }
158         mIs24HourFormat = DateFormat.is24HourFormat(mContext);
159         mMetricsFeatureProvider.action(mPrefContext, SettingsEnums.OPEN_BATTERY_USAGE);
160     }
161 
162     @Override
onSaveInstanceState(Bundle savedInstance)163     public void onSaveInstanceState(Bundle savedInstance) {
164         if (savedInstance == null) {
165             return;
166         }
167         savedInstance.putInt(KEY_CURRENT_TIME_SLOT, mTrapezoidIndex);
168         savedInstance.putBoolean(KEY_EXPAND_SYSTEM_INFO, mIsExpanded);
169         Log.d(TAG, String.format("onSaveInstanceState() slotIndex=%d isExpanded=%b",
170             mTrapezoidIndex, mIsExpanded));
171     }
172 
173     @Override
onDestroy()174     public void onDestroy() {
175         if (mActivity.isChangingConfigurations()) {
176             BatteryDiffEntry.clearCache();
177         }
178         mHandler.removeCallbacksAndMessages(/*token=*/ null);
179         mPreferenceCache.clear();
180         if (mAppListPrefGroup != null) {
181             mAppListPrefGroup.removeAll();
182         }
183     }
184 
185     @Override
displayPreference(PreferenceScreen screen)186     public void displayPreference(PreferenceScreen screen) {
187         super.displayPreference(screen);
188         mPreferenceScreen = screen;
189         mPrefContext = screen.getContext();
190         mAppListPrefGroup = screen.findPreference(mPreferenceKey);
191         mAppListPrefGroup.setOrderingAsAdded(false);
192         mAppListPrefGroup.setTitle(
193             mPrefContext.getString(R.string.battery_app_usage_for_past_24));
194         mFooterPreference = screen.findPreference(KEY_FOOTER_PREF);
195         // Removes footer first until usage data is loaded to avoid flashing.
196         if (mFooterPreference != null) {
197             screen.removePreference(mFooterPreference);
198         }
199     }
200 
201     @Override
isAvailable()202     public boolean isAvailable() {
203         return true;
204     }
205 
206     @Override
getPreferenceKey()207     public String getPreferenceKey() {
208         return mPreferenceKey;
209     }
210 
211     @Override
handlePreferenceTreeClick(Preference preference)212     public boolean handlePreferenceTreeClick(Preference preference) {
213         if (!(preference instanceof PowerGaugePreference)) {
214             return false;
215         }
216         final PowerGaugePreference powerPref = (PowerGaugePreference) preference;
217         final BatteryDiffEntry diffEntry = powerPref.getBatteryDiffEntry();
218         final BatteryHistEntry histEntry = diffEntry.mBatteryHistEntry;
219         final String packageName = histEntry.mPackageName;
220         final boolean isAppEntry = histEntry.isAppEntry();
221         mMetricsFeatureProvider.action(
222             mPrefContext,
223             isAppEntry
224                 ? SettingsEnums.ACTION_BATTERY_USAGE_APP_ITEM
225                 : SettingsEnums.ACTION_BATTERY_USAGE_SYSTEM_ITEM,
226             new Pair(ConvertUtils.METRIC_KEY_PACKAGE, packageName),
227             new Pair(ConvertUtils.METRIC_KEY_BATTERY_LEVEL, histEntry.mBatteryLevel),
228             new Pair(ConvertUtils.METRIC_KEY_BATTERY_USAGE, powerPref.getPercent()));
229         Log.d(TAG, String.format("handleClick() label=%s key=%s package=%s",
230                 diffEntry.getAppLabel(), histEntry.getKey(), histEntry.mPackageName));
231         AdvancedPowerUsageDetail.startBatteryDetailPage(
232                 mActivity, mFragment, diffEntry, powerPref.getPercent(),
233                 isValidToShowSummary(packageName), getSlotInformation());
234         return true;
235     }
236 
237     @Override
onSelect(int trapezoidIndex)238     public void onSelect(int trapezoidIndex) {
239         Log.d(TAG, "onChartSelect:" + trapezoidIndex);
240         refreshUi(trapezoidIndex, /*isForce=*/ false);
241         mMetricsFeatureProvider.action(
242             mPrefContext,
243             trapezoidIndex == BatteryChartView.SELECTED_INDEX_ALL
244                 ? SettingsEnums.ACTION_BATTERY_USAGE_SHOW_ALL
245                 : SettingsEnums.ACTION_BATTERY_USAGE_TIME_SLOT);
246     }
247 
248     @Override
onExpand(boolean isExpanded)249     public void onExpand(boolean isExpanded) {
250         mIsExpanded = isExpanded;
251         mMetricsFeatureProvider.action(
252             mPrefContext,
253             SettingsEnums.ACTION_BATTERY_USAGE_EXPAND_ITEM,
254             isExpanded);
255         refreshExpandUi();
256     }
257 
setBatteryHistoryMap( final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap)258     void setBatteryHistoryMap(
259             final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap) {
260         // Resets all battery history data relative variables.
261         if (batteryHistoryMap == null || batteryHistoryMap.isEmpty()) {
262             mBatteryIndexedMap = null;
263             mBatteryHistoryKeys = null;
264             mBatteryHistoryLevels = null;
265             addFooterPreferenceIfNeeded(false);
266             return;
267         }
268         mBatteryHistoryKeys = getBatteryHistoryKeys(batteryHistoryMap);
269         mBatteryHistoryLevels = new int[CHART_LEVEL_ARRAY_SIZE];
270         for (int index = 0; index < CHART_LEVEL_ARRAY_SIZE; index++) {
271             final long timestamp = mBatteryHistoryKeys[index * 2];
272             final Map<String, BatteryHistEntry> entryMap = batteryHistoryMap.get(timestamp);
273             if (entryMap == null || entryMap.isEmpty()) {
274                 Log.e(TAG, "abnormal entry list in the timestamp:"
275                     + ConvertUtils.utcToLocalTime(mPrefContext, timestamp));
276                 continue;
277             }
278             // Averages the battery level in each time slot to avoid corner conditions.
279             float batteryLevelCounter = 0;
280             for (BatteryHistEntry entry : entryMap.values()) {
281                 batteryLevelCounter += entry.mBatteryLevel;
282             }
283             mBatteryHistoryLevels[index] =
284                 Math.round(batteryLevelCounter / entryMap.size());
285         }
286         forceRefreshUi();
287         Log.d(TAG, String.format(
288             "setBatteryHistoryMap() size=%d key=%s\nlevels=%s",
289             batteryHistoryMap.size(),
290             ConvertUtils.utcToLocalTime(mPrefContext,
291                 mBatteryHistoryKeys[mBatteryHistoryKeys.length - 1]),
292             Arrays.toString(mBatteryHistoryLevels)));
293 
294         // Loads item icon and label in the background.
295         new LoadAllItemsInfoTask(batteryHistoryMap).execute();
296     }
297 
setBatteryChartView(final BatteryChartView batteryChartView)298     void setBatteryChartView(final BatteryChartView batteryChartView) {
299         if (mBatteryChartView != batteryChartView) {
300             mHandler.post(() -> setBatteryChartViewInner(batteryChartView));
301         }
302     }
303 
setBatteryChartViewInner(final BatteryChartView batteryChartView)304     private void setBatteryChartViewInner(final BatteryChartView batteryChartView) {
305         mBatteryChartView = batteryChartView;
306         mBatteryChartView.setOnSelectListener(this);
307         forceRefreshUi();
308     }
309 
forceRefreshUi()310     private void forceRefreshUi() {
311         final int refreshIndex =
312             mTrapezoidIndex == BatteryChartView.SELECTED_INDEX_INVALID
313                 ? BatteryChartView.SELECTED_INDEX_ALL
314                 : mTrapezoidIndex;
315         if (mBatteryChartView != null) {
316             mBatteryChartView.setLevels(mBatteryHistoryLevels);
317             mBatteryChartView.setSelectedIndex(refreshIndex);
318             setTimestampLabel();
319         }
320         refreshUi(refreshIndex, /*isForce=*/ true);
321     }
322 
323     @VisibleForTesting
refreshUi(int trapezoidIndex, boolean isForce)324     boolean refreshUi(int trapezoidIndex, boolean isForce) {
325         // Invalid refresh condition.
326         if (mBatteryIndexedMap == null
327                 || mBatteryChartView == null
328                 || (mTrapezoidIndex == trapezoidIndex && !isForce)) {
329             return false;
330         }
331         Log.d(TAG, String.format("refreshUi: index=%d size=%d isForce:%b",
332             trapezoidIndex, mBatteryIndexedMap.size(), isForce));
333 
334         mTrapezoidIndex = trapezoidIndex;
335         mHandler.post(() -> {
336             final long start = System.currentTimeMillis();
337             removeAndCacheAllPrefs();
338             addAllPreferences();
339             refreshCategoryTitle();
340             Log.d(TAG, String.format("refreshUi is finished in %d/ms",
341                     (System.currentTimeMillis() - start)));
342         });
343         return true;
344     }
345 
addAllPreferences()346     private void addAllPreferences() {
347         final List<BatteryDiffEntry> entries =
348             mBatteryIndexedMap.get(Integer.valueOf(mTrapezoidIndex));
349         addFooterPreferenceIfNeeded(!entries.isEmpty());
350         if (entries == null) {
351             Log.w(TAG, "cannot find BatteryDiffEntry for:" + mTrapezoidIndex);
352             return;
353         }
354         // Separates data into two groups and sort them individually.
355         final List<BatteryDiffEntry> appEntries = new ArrayList<>();
356         mSystemEntries.clear();
357         entries.forEach(entry -> {
358             final String packageName = entry.getPackageName();
359             if (!isValidToShowEntry(packageName)) {
360                 Log.w(TAG, "ignore showing item:" + packageName);
361                 return;
362             }
363             if (entry.isSystemEntry()) {
364                 mSystemEntries.add(entry);
365             } else {
366                 appEntries.add(entry);
367             }
368             // Validates the usage time if users click a specific slot.
369             if (mTrapezoidIndex >= 0) {
370                 validateUsageTime(entry);
371             }
372         });
373         Collections.sort(appEntries, BatteryDiffEntry.COMPARATOR);
374         Collections.sort(mSystemEntries, BatteryDiffEntry.COMPARATOR);
375         Log.d(TAG, String.format("addAllPreferences() app=%d system=%d",
376             appEntries.size(), mSystemEntries.size()));
377 
378         // Adds app entries to the list if it is not empty.
379         if (!appEntries.isEmpty()) {
380             addPreferenceToScreen(appEntries);
381         }
382         // Adds the expabable divider if we have system entries data.
383         if (!mSystemEntries.isEmpty()) {
384             if (mExpandDividerPreference == null) {
385                 mExpandDividerPreference = new ExpandDividerPreference(mPrefContext);
386                 mExpandDividerPreference.setOnExpandListener(this);
387                 mExpandDividerPreference.setIsExpanded(mIsExpanded);
388             }
389             mExpandDividerPreference.setOrder(
390                 mAppListPrefGroup.getPreferenceCount());
391             mAppListPrefGroup.addPreference(mExpandDividerPreference);
392         }
393         refreshExpandUi();
394     }
395 
396     @VisibleForTesting
addPreferenceToScreen(List<BatteryDiffEntry> entries)397     void addPreferenceToScreen(List<BatteryDiffEntry> entries) {
398         if (mAppListPrefGroup == null || entries.isEmpty()) {
399             return;
400         }
401         int prefIndex = mAppListPrefGroup.getPreferenceCount();
402         for (BatteryDiffEntry entry : entries) {
403             boolean isAdded = false;
404             final String appLabel = entry.getAppLabel();
405             final Drawable appIcon = entry.getAppIcon();
406             if (TextUtils.isEmpty(appLabel) || appIcon == null) {
407                 Log.w(TAG, "cannot find app resource for:" + entry.getPackageName());
408                 continue;
409             }
410             final String prefKey = entry.mBatteryHistEntry.getKey();
411             PowerGaugePreference pref = mAppListPrefGroup.findPreference(prefKey);
412             if (pref != null) {
413                 isAdded = true;
414                 Log.w(TAG, "preference should be removed for:" + entry.getPackageName());
415             } else {
416                 pref = (PowerGaugePreference) mPreferenceCache.get(prefKey);
417             }
418             // Creates new innstance if cached preference is not found.
419             if (pref == null) {
420                 pref = new PowerGaugePreference(mPrefContext);
421                 pref.setKey(prefKey);
422                 mPreferenceCache.put(prefKey, pref);
423             }
424             pref.setIcon(appIcon);
425             pref.setTitle(appLabel);
426             pref.setOrder(prefIndex);
427             pref.setPercent(entry.getPercentOfTotal());
428             pref.setSingleLineTitle(true);
429             // Sets the BatteryDiffEntry to preference for launching detailed page.
430             pref.setBatteryDiffEntry(entry);
431             pref.setEnabled(entry.validForRestriction());
432             setPreferenceSummary(pref, entry);
433             if (!isAdded) {
434                 mAppListPrefGroup.addPreference(pref);
435             }
436             prefIndex++;
437         }
438     }
439 
removeAndCacheAllPrefs()440     private void removeAndCacheAllPrefs() {
441         if (mAppListPrefGroup == null
442                 || mAppListPrefGroup.getPreferenceCount() == 0) {
443             return;
444         }
445         final int prefsCount = mAppListPrefGroup.getPreferenceCount();
446         for (int index = 0; index < prefsCount; index++) {
447             final Preference pref = mAppListPrefGroup.getPreference(index);
448             if (TextUtils.isEmpty(pref.getKey())) {
449                 continue;
450             }
451             mPreferenceCache.put(pref.getKey(), pref);
452         }
453         mAppListPrefGroup.removeAll();
454     }
455 
refreshExpandUi()456     private void refreshExpandUi() {
457         if (mIsExpanded) {
458             addPreferenceToScreen(mSystemEntries);
459         } else {
460             // Removes and recycles all system entries to hide all of them.
461             for (BatteryDiffEntry entry : mSystemEntries) {
462                 final String prefKey = entry.mBatteryHistEntry.getKey();
463                 final Preference pref = mAppListPrefGroup.findPreference(prefKey);
464                 if (pref != null) {
465                     mAppListPrefGroup.removePreference(pref);
466                     mPreferenceCache.put(pref.getKey(), pref);
467                 }
468             }
469         }
470     }
471 
472     @VisibleForTesting
refreshCategoryTitle()473     void refreshCategoryTitle() {
474         final String slotInformation = getSlotInformation();
475         Log.d(TAG, String.format("refreshCategoryTitle:%s", slotInformation));
476         if (mAppListPrefGroup != null) {
477             mAppListPrefGroup.setTitle(
478                 getSlotInformation(/*isApp=*/ true, slotInformation));
479         }
480         if (mExpandDividerPreference != null) {
481             mExpandDividerPreference.setTitle(
482                 getSlotInformation(/*isApp=*/ false, slotInformation));
483         }
484     }
485 
getSlotInformation(boolean isApp, String slotInformation)486     private String getSlotInformation(boolean isApp, String slotInformation) {
487         // Null means we show all information without a specific time slot.
488         if (slotInformation == null) {
489             return isApp
490                 ? mPrefContext.getString(R.string.battery_app_usage_for_past_24)
491                 : mPrefContext.getString(R.string.battery_system_usage_for_past_24);
492         } else {
493             return isApp
494                 ? mPrefContext.getString(R.string.battery_app_usage_for, slotInformation)
495                 : mPrefContext.getString(R.string.battery_system_usage_for ,slotInformation);
496         }
497     }
498 
getSlotInformation()499     private String getSlotInformation() {
500         if (mTrapezoidIndex < 0) {
501             return null;
502         }
503         final String fromHour = ConvertUtils.utcToLocalTimeHour(mPrefContext,
504             mBatteryHistoryKeys[mTrapezoidIndex * 2], mIs24HourFormat);
505         final String toHour = ConvertUtils.utcToLocalTimeHour(mPrefContext,
506             mBatteryHistoryKeys[(mTrapezoidIndex + 1) * 2], mIs24HourFormat);
507         return String.format("%s - %s", fromHour, toHour);
508     }
509 
510     @VisibleForTesting
setPreferenceSummary( PowerGaugePreference preference, BatteryDiffEntry entry)511     void setPreferenceSummary(
512             PowerGaugePreference preference, BatteryDiffEntry entry) {
513         final long foregroundUsageTimeInMs = entry.mForegroundUsageTimeInMs;
514         final long backgroundUsageTimeInMs = entry.mBackgroundUsageTimeInMs;
515         final long totalUsageTimeInMs = foregroundUsageTimeInMs + backgroundUsageTimeInMs;
516         // Checks whether the package is allowed to show summary or not.
517         if (!isValidToShowSummary(entry.getPackageName())) {
518             preference.setSummary(null);
519             return;
520         }
521         String usageTimeSummary = null;
522         // Not shows summary for some system components without usage time.
523         if (totalUsageTimeInMs == 0) {
524             preference.setSummary(null);
525         // Shows background summary only if we don't have foreground usage time.
526         } else if (foregroundUsageTimeInMs == 0 && backgroundUsageTimeInMs != 0) {
527             usageTimeSummary = buildUsageTimeInfo(backgroundUsageTimeInMs, true);
528         // Shows total usage summary only if total usage time is small.
529         } else if (totalUsageTimeInMs < DateUtils.MINUTE_IN_MILLIS) {
530             usageTimeSummary = buildUsageTimeInfo(totalUsageTimeInMs, false);
531         } else {
532             usageTimeSummary = buildUsageTimeInfo(totalUsageTimeInMs, false);
533             // Shows background usage time if it is larger than a minute.
534             if (backgroundUsageTimeInMs > 0) {
535                 usageTimeSummary +=
536                     "\n" + buildUsageTimeInfo(backgroundUsageTimeInMs, true);
537             }
538         }
539         preference.setSummary(usageTimeSummary);
540     }
541 
buildUsageTimeInfo(long usageTimeInMs, boolean isBackground)542     private String buildUsageTimeInfo(long usageTimeInMs, boolean isBackground) {
543         if (usageTimeInMs < DateUtils.MINUTE_IN_MILLIS) {
544             return mPrefContext.getString(
545                 isBackground
546                     ? R.string.battery_usage_background_less_than_one_minute
547                     : R.string.battery_usage_total_less_than_one_minute);
548         }
549         final CharSequence timeSequence =
550             StringUtil.formatElapsedTime(mPrefContext, usageTimeInMs,
551                 /*withSeconds=*/ false, /*collapseTimeUnit=*/ false);
552         final int resourceId =
553             isBackground
554                 ? R.string.battery_usage_for_background_time
555                 : R.string.battery_usage_for_total_time;
556         return mPrefContext.getString(resourceId, timeSequence);
557     }
558 
559     @VisibleForTesting
isValidToShowSummary(String packageName)560     boolean isValidToShowSummary(String packageName) {
561         return !contains(packageName, mNotAllowShowSummaryPackages);
562     }
563 
564     @VisibleForTesting
isValidToShowEntry(String packageName)565     boolean isValidToShowEntry(String packageName) {
566         return !contains(packageName, mNotAllowShowEntryPackages);
567     }
568 
569     @VisibleForTesting
setTimestampLabel()570     void setTimestampLabel() {
571         if (mBatteryChartView == null || mBatteryHistoryKeys == null) {
572             return;
573         }
574         final long latestTimestamp =
575             mBatteryHistoryKeys[mBatteryHistoryKeys.length - 1];
576         mBatteryChartView.setLatestTimestamp(latestTimestamp);
577     }
578 
addFooterPreferenceIfNeeded(boolean containAppItems)579     private void addFooterPreferenceIfNeeded(boolean containAppItems) {
580         if (mIsFooterPrefAdded || mFooterPreference == null) {
581             return;
582         }
583         mIsFooterPrefAdded = true;
584         mFooterPreference.setTitle(mPrefContext.getString(
585             containAppItems
586                 ? R.string.battery_usage_screen_footer
587                 : R.string.battery_usage_screen_footer_empty));
588         mHandler.post(() -> mPreferenceScreen.addPreference(mFooterPreference));
589     }
590 
contains(String target, CharSequence[] packageNames)591     private static boolean contains(String target, CharSequence[] packageNames) {
592         if (target != null && packageNames != null) {
593             for (CharSequence packageName : packageNames) {
594                 if (TextUtils.equals(target, packageName)) {
595                     return true;
596                 }
597             }
598         }
599         return false;
600     }
601 
602     @VisibleForTesting
validateUsageTime(BatteryDiffEntry entry)603     static boolean validateUsageTime(BatteryDiffEntry entry) {
604         final long foregroundUsageTimeInMs = entry.mForegroundUsageTimeInMs;
605         final long backgroundUsageTimeInMs = entry.mBackgroundUsageTimeInMs;
606         final long totalUsageTimeInMs = foregroundUsageTimeInMs + backgroundUsageTimeInMs;
607         if (foregroundUsageTimeInMs > VALID_USAGE_TIME_DURATION
608                 || backgroundUsageTimeInMs > VALID_USAGE_TIME_DURATION
609                 || totalUsageTimeInMs > VALID_USAGE_TIME_DURATION) {
610             Log.e(TAG, "validateUsageTime() fail for\n" + entry);
611             return false;
612         }
613         return true;
614     }
615 
getBatteryLast24HrUsageData(Context context)616     public static List<BatteryDiffEntry> getBatteryLast24HrUsageData(Context context) {
617         final long start = System.currentTimeMillis();
618         final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap =
619             FeatureFactory.getFactory(context)
620                 .getPowerUsageFeatureProvider(context)
621                 .getBatteryHistory(context);
622         if (batteryHistoryMap == null || batteryHistoryMap.isEmpty()) {
623             return null;
624         }
625         Log.d(TAG, String.format("getBatteryLast24HrData() size=%d time=&d/ms",
626             batteryHistoryMap.size(), (System.currentTimeMillis() - start)));
627         final Map<Integer, List<BatteryDiffEntry>> batteryIndexedMap =
628             ConvertUtils.getIndexedUsageMap(
629                 context,
630                 /*timeSlotSize=*/ CHART_LEVEL_ARRAY_SIZE - 1,
631                 getBatteryHistoryKeys(batteryHistoryMap),
632                 batteryHistoryMap,
633                 /*purgeLowPercentageAndFakeData=*/ true);
634         return batteryIndexedMap.get(BatteryChartView.SELECTED_INDEX_ALL);
635     }
636 
getBatteryHistoryKeys( final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap)637     private static long[] getBatteryHistoryKeys(
638             final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap) {
639         final List<Long> batteryHistoryKeyList =
640             new ArrayList<>(batteryHistoryMap.keySet());
641         Collections.sort(batteryHistoryKeyList);
642         final long[] batteryHistoryKeys = new long[CHART_KEY_ARRAY_SIZE];
643         for (int index = 0; index < CHART_KEY_ARRAY_SIZE; index++) {
644             batteryHistoryKeys[index] = batteryHistoryKeyList.get(index);
645         }
646         return batteryHistoryKeys;
647     }
648 
649     // Loads all items icon and label in the background.
650     private final class LoadAllItemsInfoTask
651             extends AsyncTask<Void, Void, Map<Integer, List<BatteryDiffEntry>>> {
652 
653         private long[] mBatteryHistoryKeysCache;
654         private Map<Long, Map<String, BatteryHistEntry>> mBatteryHistoryMap;
655 
LoadAllItemsInfoTask( Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap)656         private LoadAllItemsInfoTask(
657                 Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap) {
658             this.mBatteryHistoryMap = batteryHistoryMap;
659             this.mBatteryHistoryKeysCache = mBatteryHistoryKeys;
660         }
661 
662         @Override
doInBackground(Void... voids)663         protected Map<Integer, List<BatteryDiffEntry>> doInBackground(Void... voids) {
664             if (mPrefContext == null || mBatteryHistoryKeysCache == null) {
665                 return null;
666             }
667             final long startTime = System.currentTimeMillis();
668             final Map<Integer, List<BatteryDiffEntry>> indexedUsageMap =
669                 ConvertUtils.getIndexedUsageMap(
670                     mPrefContext, /*timeSlotSize=*/ CHART_LEVEL_ARRAY_SIZE - 1,
671                     mBatteryHistoryKeysCache, mBatteryHistoryMap,
672                     /*purgeLowPercentageAndFakeData=*/ true);
673             // Pre-loads each BatteryDiffEntry relative icon and label for all slots.
674             for (List<BatteryDiffEntry> entries : indexedUsageMap.values()) {
675                 entries.forEach(entry -> entry.loadLabelAndIcon());
676             }
677             Log.d(TAG, String.format("execute LoadAllItemsInfoTask in %d/ms",
678                 (System.currentTimeMillis() - startTime)));
679             return indexedUsageMap;
680         }
681 
682         @Override
onPostExecute( Map<Integer, List<BatteryDiffEntry>> indexedUsageMap)683         protected void onPostExecute(
684                 Map<Integer, List<BatteryDiffEntry>> indexedUsageMap) {
685             mBatteryHistoryMap = null;
686             mBatteryHistoryKeysCache = null;
687             if (indexedUsageMap == null) {
688                 return;
689             }
690             // Posts results back to main thread to refresh UI.
691             mHandler.post(() -> {
692                 mBatteryIndexedMap = indexedUsageMap;
693                 forceRefreshUi();
694             });
695         }
696     }
697 }
698