| /* |
| * 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<Integer, List<BatteryDiffEntry>> 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<String, Preference> mPreferenceCache = new HashMap<>(); |
| @VisibleForTesting |
| final List<BatteryDiffEntry> 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<Long, Map<String, BatteryHistEntry>> 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<String, BatteryHistEntry> 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<BatteryDiffEntry> 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<BatteryDiffEntry> 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<BatteryDiffEntry> 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<BatteryDiffEntry> getBatteryLast24HrUsageData(Context context) { |
| final long start = System.currentTimeMillis(); |
| final Map<Long, Map<String, BatteryHistEntry>> 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<Integer, List<BatteryDiffEntry>> 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<Long, Map<String, BatteryHistEntry>> batteryHistoryMap) { |
| final List<Long> 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<Void, Void, Map<Integer, List<BatteryDiffEntry>>> { |
| |
| private long[] mBatteryHistoryKeysCache; |
| private Map<Long, Map<String, BatteryHistEntry>> mBatteryHistoryMap; |
| |
| private LoadAllItemsInfoTask( |
| Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap) { |
| this.mBatteryHistoryMap = batteryHistoryMap; |
| this.mBatteryHistoryKeysCache = mBatteryHistoryKeys; |
| } |
| |
| @Override |
| protected Map<Integer, List<BatteryDiffEntry>> doInBackground(Void... voids) { |
| if (mPrefContext == null || mBatteryHistoryKeysCache == null) { |
| return null; |
| } |
| final long startTime = System.currentTimeMillis(); |
| final Map<Integer, List<BatteryDiffEntry>> 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<BatteryDiffEntry> 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<Integer, List<BatteryDiffEntry>> indexedUsageMap) { |
| mBatteryHistoryMap = null; |
| mBatteryHistoryKeysCache = null; |
| if (indexedUsageMap == null) { |
| return; |
| } |
| // Posts results back to main thread to refresh UI. |
| mHandler.post(() -> { |
| mBatteryIndexedMap = indexedUsageMap; |
| forceRefreshUi(); |
| }); |
| } |
| } |
| } |