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