/* * Copyright (C) 2021 The Android Open Source Project * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.android.settings.fuelgauge; import android.app.settings.SettingsEnums; import android.content.Context; import android.content.res.Configuration; import android.graphics.drawable.Drawable; import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.text.TextUtils; import android.text.format.DateFormat; import android.text.format.DateUtils; import android.util.Log; import android.util.Pair; import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; import androidx.preference.PreferenceGroup; import androidx.preference.PreferenceScreen; import com.android.settings.R; import com.android.settings.SettingsActivity; import com.android.settings.core.InstrumentedPreferenceFragment; import com.android.settings.core.PreferenceControllerMixin; import com.android.settings.overlay.FeatureFactory; import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; import com.android.settingslib.core.lifecycle.Lifecycle; import com.android.settingslib.core.lifecycle.LifecycleObserver; import com.android.settingslib.core.lifecycle.events.OnCreate; import com.android.settingslib.core.lifecycle.events.OnDestroy; import com.android.settingslib.core.lifecycle.events.OnResume; import com.android.settingslib.core.lifecycle.events.OnSaveInstanceState; import com.android.settingslib.utils.StringUtil; import com.android.settingslib.widget.FooterPreference; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; /** Controls the update for chart graph and the list items. */ public class BatteryChartPreferenceController extends AbstractPreferenceController implements PreferenceControllerMixin, LifecycleObserver, OnCreate, OnDestroy, OnSaveInstanceState, BatteryChartView.OnSelectListener, OnResume, ExpandDividerPreference.OnExpandListener { private static final String TAG = "BatteryChartPreferenceController"; private static final String KEY_FOOTER_PREF = "battery_graph_footer"; /** Desired battery history size for timestamp slots. */ public static final int DESIRED_HISTORY_SIZE = 25; private static final int CHART_LEVEL_ARRAY_SIZE = 13; private static final int CHART_KEY_ARRAY_SIZE = DESIRED_HISTORY_SIZE; private static final long VALID_USAGE_TIME_DURATION = DateUtils.HOUR_IN_MILLIS * 2; private static final long VALID_DIFF_DURATION = DateUtils.MINUTE_IN_MILLIS * 3; // Keys for bundle instance to restore configurations. private static final String KEY_EXPAND_SYSTEM_INFO = "expand_system_info"; private static final String KEY_CURRENT_TIME_SLOT = "current_time_slot"; private static int sUiMode = Configuration.UI_MODE_NIGHT_UNDEFINED; @VisibleForTesting Map> mBatteryIndexedMap; @VisibleForTesting Context mPrefContext; @VisibleForTesting BatteryUtils mBatteryUtils; @VisibleForTesting PreferenceGroup mAppListPrefGroup; @VisibleForTesting BatteryChartView mBatteryChartView; @VisibleForTesting ExpandDividerPreference mExpandDividerPreference; @VisibleForTesting boolean mIsExpanded = false; @VisibleForTesting int[] mBatteryHistoryLevels; @VisibleForTesting long[] mBatteryHistoryKeys; @VisibleForTesting int mTrapezoidIndex = BatteryChartView.SELECTED_INDEX_INVALID; private boolean mIs24HourFormat = false; private boolean mIsFooterPrefAdded = false; private PreferenceScreen mPreferenceScreen; private FooterPreference mFooterPreference; private final String mPreferenceKey; private final SettingsActivity mActivity; private final InstrumentedPreferenceFragment mFragment; private final CharSequence[] mNotAllowShowEntryPackages; private final CharSequence[] mNotAllowShowSummaryPackages; private final MetricsFeatureProvider mMetricsFeatureProvider; private final Handler mHandler = new Handler(Looper.getMainLooper()); // Preference cache to avoid create new instance each time. @VisibleForTesting final Map mPreferenceCache = new HashMap<>(); @VisibleForTesting final List mSystemEntries = new ArrayList<>(); public BatteryChartPreferenceController( Context context, String preferenceKey, Lifecycle lifecycle, SettingsActivity activity, InstrumentedPreferenceFragment fragment) { super(context); mActivity = activity; mFragment = fragment; mPreferenceKey = preferenceKey; mIs24HourFormat = DateFormat.is24HourFormat(context); mNotAllowShowSummaryPackages = context.getResources() .getTextArray(R.array.allowlist_hide_summary_in_battery_usage); mNotAllowShowEntryPackages = context.getResources() .getTextArray(R.array.allowlist_hide_entry_in_battery_usage); mMetricsFeatureProvider = FeatureFactory.getFactory(mContext).getMetricsFeatureProvider(); if (lifecycle != null) { lifecycle.addObserver(this); } } @Override public void onCreate(Bundle savedInstanceState) { if (savedInstanceState == null) { return; } mTrapezoidIndex = savedInstanceState.getInt(KEY_CURRENT_TIME_SLOT, mTrapezoidIndex); mIsExpanded = savedInstanceState.getBoolean(KEY_EXPAND_SYSTEM_INFO, mIsExpanded); Log.d(TAG, String.format("onCreate() slotIndex=%d isExpanded=%b", mTrapezoidIndex, mIsExpanded)); } @Override public void onResume() { final int currentUiMode = mContext.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; if (sUiMode != currentUiMode) { sUiMode = currentUiMode; BatteryDiffEntry.clearCache(); Log.d(TAG, "clear icon and label cache since uiMode is changed"); } mIs24HourFormat = DateFormat.is24HourFormat(mContext); mMetricsFeatureProvider.action(mPrefContext, SettingsEnums.OPEN_BATTERY_USAGE); } @Override public void onSaveInstanceState(Bundle savedInstance) { if (savedInstance == null) { return; } savedInstance.putInt(KEY_CURRENT_TIME_SLOT, mTrapezoidIndex); savedInstance.putBoolean(KEY_EXPAND_SYSTEM_INFO, mIsExpanded); Log.d(TAG, String.format("onSaveInstanceState() slotIndex=%d isExpanded=%b", mTrapezoidIndex, mIsExpanded)); } @Override public void onDestroy() { if (mActivity.isChangingConfigurations()) { BatteryDiffEntry.clearCache(); } mHandler.removeCallbacksAndMessages(/*token=*/ null); mPreferenceCache.clear(); if (mAppListPrefGroup != null) { mAppListPrefGroup.removeAll(); } } @Override public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); mPreferenceScreen = screen; mPrefContext = screen.getContext(); mAppListPrefGroup = screen.findPreference(mPreferenceKey); mAppListPrefGroup.setOrderingAsAdded(false); mAppListPrefGroup.setTitle( mPrefContext.getString(R.string.battery_app_usage_for_past_24)); mFooterPreference = screen.findPreference(KEY_FOOTER_PREF); // Removes footer first until usage data is loaded to avoid flashing. if (mFooterPreference != null) { screen.removePreference(mFooterPreference); } } @Override public boolean isAvailable() { return true; } @Override public String getPreferenceKey() { return mPreferenceKey; } @Override public boolean handlePreferenceTreeClick(Preference preference) { if (!(preference instanceof PowerGaugePreference)) { return false; } final PowerGaugePreference powerPref = (PowerGaugePreference) preference; final BatteryDiffEntry diffEntry = powerPref.getBatteryDiffEntry(); final BatteryHistEntry histEntry = diffEntry.mBatteryHistEntry; final String packageName = histEntry.mPackageName; final boolean isAppEntry = histEntry.isAppEntry(); mMetricsFeatureProvider.action( mPrefContext, isAppEntry ? SettingsEnums.ACTION_BATTERY_USAGE_APP_ITEM : SettingsEnums.ACTION_BATTERY_USAGE_SYSTEM_ITEM, new Pair(ConvertUtils.METRIC_KEY_PACKAGE, packageName), new Pair(ConvertUtils.METRIC_KEY_BATTERY_LEVEL, histEntry.mBatteryLevel), new Pair(ConvertUtils.METRIC_KEY_BATTERY_USAGE, powerPref.getPercent())); Log.d(TAG, String.format("handleClick() label=%s key=%s package=%s", diffEntry.getAppLabel(), histEntry.getKey(), histEntry.mPackageName)); AdvancedPowerUsageDetail.startBatteryDetailPage( mActivity, mFragment, diffEntry, powerPref.getPercent(), isValidToShowSummary(packageName), getSlotInformation()); return true; } @Override public void onSelect(int trapezoidIndex) { Log.d(TAG, "onChartSelect:" + trapezoidIndex); refreshUi(trapezoidIndex, /*isForce=*/ false); mMetricsFeatureProvider.action( mPrefContext, trapezoidIndex == BatteryChartView.SELECTED_INDEX_ALL ? SettingsEnums.ACTION_BATTERY_USAGE_SHOW_ALL : SettingsEnums.ACTION_BATTERY_USAGE_TIME_SLOT); } @Override public void onExpand(boolean isExpanded) { mIsExpanded = isExpanded; mMetricsFeatureProvider.action( mPrefContext, SettingsEnums.ACTION_BATTERY_USAGE_EXPAND_ITEM, isExpanded); refreshExpandUi(); } void setBatteryHistoryMap( final Map> batteryHistoryMap) { // Resets all battery history data relative variables. if (batteryHistoryMap == null || batteryHistoryMap.isEmpty()) { mBatteryIndexedMap = null; mBatteryHistoryKeys = null; mBatteryHistoryLevels = null; addFooterPreferenceIfNeeded(false); return; } mBatteryHistoryKeys = getBatteryHistoryKeys(batteryHistoryMap); mBatteryHistoryLevels = new int[CHART_LEVEL_ARRAY_SIZE]; for (int index = 0; index < CHART_LEVEL_ARRAY_SIZE; index++) { final long timestamp = mBatteryHistoryKeys[index * 2]; final Map entryMap = batteryHistoryMap.get(timestamp); if (entryMap == null || entryMap.isEmpty()) { Log.e(TAG, "abnormal entry list in the timestamp:" + ConvertUtils.utcToLocalTime(mPrefContext, timestamp)); continue; } // Averages the battery level in each time slot to avoid corner conditions. float batteryLevelCounter = 0; for (BatteryHistEntry entry : entryMap.values()) { batteryLevelCounter += entry.mBatteryLevel; } mBatteryHistoryLevels[index] = Math.round(batteryLevelCounter / entryMap.size()); } forceRefreshUi(); Log.d(TAG, String.format( "setBatteryHistoryMap() size=%d key=%s\nlevels=%s", batteryHistoryMap.size(), ConvertUtils.utcToLocalTime(mPrefContext, mBatteryHistoryKeys[mBatteryHistoryKeys.length - 1]), Arrays.toString(mBatteryHistoryLevels))); // Loads item icon and label in the background. new LoadAllItemsInfoTask(batteryHistoryMap).execute(); } void setBatteryChartView(final BatteryChartView batteryChartView) { if (mBatteryChartView != batteryChartView) { mHandler.post(() -> setBatteryChartViewInner(batteryChartView)); } } private void setBatteryChartViewInner(final BatteryChartView batteryChartView) { mBatteryChartView = batteryChartView; mBatteryChartView.setOnSelectListener(this); forceRefreshUi(); } private void forceRefreshUi() { final int refreshIndex = mTrapezoidIndex == BatteryChartView.SELECTED_INDEX_INVALID ? BatteryChartView.SELECTED_INDEX_ALL : mTrapezoidIndex; if (mBatteryChartView != null) { mBatteryChartView.setLevels(mBatteryHistoryLevels); mBatteryChartView.setSelectedIndex(refreshIndex); setTimestampLabel(); } refreshUi(refreshIndex, /*isForce=*/ true); } @VisibleForTesting boolean refreshUi(int trapezoidIndex, boolean isForce) { // Invalid refresh condition. if (mBatteryIndexedMap == null || mBatteryChartView == null || (mTrapezoidIndex == trapezoidIndex && !isForce)) { return false; } Log.d(TAG, String.format("refreshUi: index=%d size=%d isForce:%b", trapezoidIndex, mBatteryIndexedMap.size(), isForce)); mTrapezoidIndex = trapezoidIndex; mHandler.post(() -> { final long start = System.currentTimeMillis(); removeAndCacheAllPrefs(); addAllPreferences(); refreshCategoryTitle(); Log.d(TAG, String.format("refreshUi is finished in %d/ms", (System.currentTimeMillis() - start))); }); return true; } private void addAllPreferences() { final List entries = mBatteryIndexedMap.get(Integer.valueOf(mTrapezoidIndex)); addFooterPreferenceIfNeeded(!entries.isEmpty()); if (entries == null) { Log.w(TAG, "cannot find BatteryDiffEntry for:" + mTrapezoidIndex); return; } // Separates data into two groups and sort them individually. final List appEntries = new ArrayList<>(); mSystemEntries.clear(); entries.forEach(entry -> { final String packageName = entry.getPackageName(); if (!isValidToShowEntry(packageName)) { Log.w(TAG, "ignore showing item:" + packageName); return; } if (entry.isSystemEntry()) { mSystemEntries.add(entry); } else { appEntries.add(entry); } // Validates the usage time if users click a specific slot. if (mTrapezoidIndex >= 0) { validateUsageTime(entry); } }); Collections.sort(appEntries, BatteryDiffEntry.COMPARATOR); Collections.sort(mSystemEntries, BatteryDiffEntry.COMPARATOR); Log.d(TAG, String.format("addAllPreferences() app=%d system=%d", appEntries.size(), mSystemEntries.size())); // Adds app entries to the list if it is not empty. if (!appEntries.isEmpty()) { addPreferenceToScreen(appEntries); } // Adds the expabable divider if we have system entries data. if (!mSystemEntries.isEmpty()) { if (mExpandDividerPreference == null) { mExpandDividerPreference = new ExpandDividerPreference(mPrefContext); mExpandDividerPreference.setOnExpandListener(this); mExpandDividerPreference.setIsExpanded(mIsExpanded); } mExpandDividerPreference.setOrder( mAppListPrefGroup.getPreferenceCount()); mAppListPrefGroup.addPreference(mExpandDividerPreference); } refreshExpandUi(); } @VisibleForTesting void addPreferenceToScreen(List entries) { if (mAppListPrefGroup == null || entries.isEmpty()) { return; } int prefIndex = mAppListPrefGroup.getPreferenceCount(); for (BatteryDiffEntry entry : entries) { boolean isAdded = false; final String appLabel = entry.getAppLabel(); final Drawable appIcon = entry.getAppIcon(); if (TextUtils.isEmpty(appLabel) || appIcon == null) { Log.w(TAG, "cannot find app resource for:" + entry.getPackageName()); continue; } final String prefKey = entry.mBatteryHistEntry.getKey(); PowerGaugePreference pref = mAppListPrefGroup.findPreference(prefKey); if (pref != null) { isAdded = true; Log.w(TAG, "preference should be removed for:" + entry.getPackageName()); } else { pref = (PowerGaugePreference) mPreferenceCache.get(prefKey); } // Creates new innstance if cached preference is not found. if (pref == null) { pref = new PowerGaugePreference(mPrefContext); pref.setKey(prefKey); mPreferenceCache.put(prefKey, pref); } pref.setIcon(appIcon); pref.setTitle(appLabel); pref.setOrder(prefIndex); pref.setPercent(entry.getPercentOfTotal()); pref.setSingleLineTitle(true); // Sets the BatteryDiffEntry to preference for launching detailed page. pref.setBatteryDiffEntry(entry); pref.setEnabled(entry.validForRestriction()); setPreferenceSummary(pref, entry); if (!isAdded) { mAppListPrefGroup.addPreference(pref); } prefIndex++; } } private void removeAndCacheAllPrefs() { if (mAppListPrefGroup == null || mAppListPrefGroup.getPreferenceCount() == 0) { return; } final int prefsCount = mAppListPrefGroup.getPreferenceCount(); for (int index = 0; index < prefsCount; index++) { final Preference pref = mAppListPrefGroup.getPreference(index); if (TextUtils.isEmpty(pref.getKey())) { continue; } mPreferenceCache.put(pref.getKey(), pref); } mAppListPrefGroup.removeAll(); } private void refreshExpandUi() { if (mIsExpanded) { addPreferenceToScreen(mSystemEntries); } else { // Removes and recycles all system entries to hide all of them. for (BatteryDiffEntry entry : mSystemEntries) { final String prefKey = entry.mBatteryHistEntry.getKey(); final Preference pref = mAppListPrefGroup.findPreference(prefKey); if (pref != null) { mAppListPrefGroup.removePreference(pref); mPreferenceCache.put(pref.getKey(), pref); } } } } @VisibleForTesting void refreshCategoryTitle() { final String slotInformation = getSlotInformation(); Log.d(TAG, String.format("refreshCategoryTitle:%s", slotInformation)); if (mAppListPrefGroup != null) { mAppListPrefGroup.setTitle( getSlotInformation(/*isApp=*/ true, slotInformation)); } if (mExpandDividerPreference != null) { mExpandDividerPreference.setTitle( getSlotInformation(/*isApp=*/ false, slotInformation)); } } private String getSlotInformation(boolean isApp, String slotInformation) { // Null means we show all information without a specific time slot. if (slotInformation == null) { return isApp ? mPrefContext.getString(R.string.battery_app_usage_for_past_24) : mPrefContext.getString(R.string.battery_system_usage_for_past_24); } else { return isApp ? mPrefContext.getString(R.string.battery_app_usage_for, slotInformation) : mPrefContext.getString(R.string.battery_system_usage_for ,slotInformation); } } private String getSlotInformation() { if (mTrapezoidIndex < 0) { return null; } final String fromHour = ConvertUtils.utcToLocalTimeHour(mPrefContext, mBatteryHistoryKeys[mTrapezoidIndex * 2], mIs24HourFormat); final String toHour = ConvertUtils.utcToLocalTimeHour(mPrefContext, mBatteryHistoryKeys[(mTrapezoidIndex + 1) * 2], mIs24HourFormat); return String.format("%s - %s", fromHour, toHour); } @VisibleForTesting void setPreferenceSummary( PowerGaugePreference preference, BatteryDiffEntry entry) { final long foregroundUsageTimeInMs = entry.mForegroundUsageTimeInMs; final long backgroundUsageTimeInMs = entry.mBackgroundUsageTimeInMs; final long totalUsageTimeInMs = foregroundUsageTimeInMs + backgroundUsageTimeInMs; // Checks whether the package is allowed to show summary or not. if (!isValidToShowSummary(entry.getPackageName())) { preference.setSummary(null); return; } String usageTimeSummary = null; // Not shows summary for some system components without usage time. if (totalUsageTimeInMs == 0) { preference.setSummary(null); // Shows background summary only if we don't have foreground usage time. } else if (foregroundUsageTimeInMs == 0 && backgroundUsageTimeInMs != 0) { usageTimeSummary = buildUsageTimeInfo(backgroundUsageTimeInMs, true); // Shows total usage summary only if total usage time is small. } else if (totalUsageTimeInMs < DateUtils.MINUTE_IN_MILLIS) { usageTimeSummary = buildUsageTimeInfo(totalUsageTimeInMs, false); } else { usageTimeSummary = buildUsageTimeInfo(totalUsageTimeInMs, false); // Shows background usage time if it is larger than a minute. if (backgroundUsageTimeInMs > 0) { usageTimeSummary += "\n" + buildUsageTimeInfo(backgroundUsageTimeInMs, true); } } preference.setSummary(usageTimeSummary); } private String buildUsageTimeInfo(long usageTimeInMs, boolean isBackground) { if (usageTimeInMs < DateUtils.MINUTE_IN_MILLIS) { return mPrefContext.getString( isBackground ? R.string.battery_usage_background_less_than_one_minute : R.string.battery_usage_total_less_than_one_minute); } final CharSequence timeSequence = StringUtil.formatElapsedTime(mPrefContext, usageTimeInMs, /*withSeconds=*/ false, /*collapseTimeUnit=*/ false); final int resourceId = isBackground ? R.string.battery_usage_for_background_time : R.string.battery_usage_for_total_time; return mPrefContext.getString(resourceId, timeSequence); } @VisibleForTesting boolean isValidToShowSummary(String packageName) { return !contains(packageName, mNotAllowShowSummaryPackages); } @VisibleForTesting boolean isValidToShowEntry(String packageName) { return !contains(packageName, mNotAllowShowEntryPackages); } @VisibleForTesting void setTimestampLabel() { if (mBatteryChartView == null || mBatteryHistoryKeys == null) { return; } final long latestTimestamp = mBatteryHistoryKeys[mBatteryHistoryKeys.length - 1]; mBatteryChartView.setLatestTimestamp(latestTimestamp); } private void addFooterPreferenceIfNeeded(boolean containAppItems) { if (mIsFooterPrefAdded || mFooterPreference == null) { return; } mIsFooterPrefAdded = true; mFooterPreference.setTitle(mPrefContext.getString( containAppItems ? R.string.battery_usage_screen_footer : R.string.battery_usage_screen_footer_empty)); mHandler.post(() -> mPreferenceScreen.addPreference(mFooterPreference)); } private static boolean contains(String target, CharSequence[] packageNames) { if (target != null && packageNames != null) { for (CharSequence packageName : packageNames) { if (TextUtils.equals(target, packageName)) { return true; } } } return false; } @VisibleForTesting static boolean validateUsageTime(BatteryDiffEntry entry) { final long foregroundUsageTimeInMs = entry.mForegroundUsageTimeInMs; final long backgroundUsageTimeInMs = entry.mBackgroundUsageTimeInMs; final long totalUsageTimeInMs = foregroundUsageTimeInMs + backgroundUsageTimeInMs; if (foregroundUsageTimeInMs > VALID_USAGE_TIME_DURATION || backgroundUsageTimeInMs > VALID_USAGE_TIME_DURATION || totalUsageTimeInMs > VALID_USAGE_TIME_DURATION) { Log.e(TAG, "validateUsageTime() fail for\n" + entry); return false; } return true; } public static List getBatteryLast24HrUsageData(Context context) { final long start = System.currentTimeMillis(); final Map> batteryHistoryMap = FeatureFactory.getFactory(context) .getPowerUsageFeatureProvider(context) .getBatteryHistory(context); if (batteryHistoryMap == null || batteryHistoryMap.isEmpty()) { return null; } Log.d(TAG, String.format("getBatteryLast24HrData() size=%d time=&d/ms", batteryHistoryMap.size(), (System.currentTimeMillis() - start))); final Map> batteryIndexedMap = ConvertUtils.getIndexedUsageMap( context, /*timeSlotSize=*/ CHART_LEVEL_ARRAY_SIZE - 1, getBatteryHistoryKeys(batteryHistoryMap), batteryHistoryMap, /*purgeLowPercentageAndFakeData=*/ true); return batteryIndexedMap.get(BatteryChartView.SELECTED_INDEX_ALL); } private static long[] getBatteryHistoryKeys( final Map> batteryHistoryMap) { final List batteryHistoryKeyList = new ArrayList<>(batteryHistoryMap.keySet()); Collections.sort(batteryHistoryKeyList); final long[] batteryHistoryKeys = new long[CHART_KEY_ARRAY_SIZE]; for (int index = 0; index < CHART_KEY_ARRAY_SIZE; index++) { batteryHistoryKeys[index] = batteryHistoryKeyList.get(index); } return batteryHistoryKeys; } // Loads all items icon and label in the background. private final class LoadAllItemsInfoTask extends AsyncTask>> { private long[] mBatteryHistoryKeysCache; private Map> mBatteryHistoryMap; private LoadAllItemsInfoTask( Map> batteryHistoryMap) { this.mBatteryHistoryMap = batteryHistoryMap; this.mBatteryHistoryKeysCache = mBatteryHistoryKeys; } @Override protected Map> doInBackground(Void... voids) { if (mPrefContext == null || mBatteryHistoryKeysCache == null) { return null; } final long startTime = System.currentTimeMillis(); final Map> indexedUsageMap = ConvertUtils.getIndexedUsageMap( mPrefContext, /*timeSlotSize=*/ CHART_LEVEL_ARRAY_SIZE - 1, mBatteryHistoryKeysCache, mBatteryHistoryMap, /*purgeLowPercentageAndFakeData=*/ true); // Pre-loads each BatteryDiffEntry relative icon and label for all slots. for (List entries : indexedUsageMap.values()) { entries.forEach(entry -> entry.loadLabelAndIcon()); } Log.d(TAG, String.format("execute LoadAllItemsInfoTask in %d/ms", (System.currentTimeMillis() - startTime))); return indexedUsageMap; } @Override protected void onPostExecute( Map> indexedUsageMap) { mBatteryHistoryMap = null; mBatteryHistoryKeysCache = null; if (indexedUsageMap == null) { return; } // Posts results back to main thread to refresh UI. mHandler.post(() -> { mBatteryIndexedMap = indexedUsageMap; forceRefreshUi(); }); } } }