Support daily and hourly battery chartview.

https://drive.google.com/file/d/1vjt6FPFAiFakb158t1IBku1IQNzVPW46/view?usp=sharing&resourcekey=0-f5m2eUKWbth6gdEFh5lOIQ

Test: manual
Bug: 236101166
Change-Id: I47d899b936742514801cd4fa7a82ddaea5d21c31
This commit is contained in:
Zaiyue Xue
2022-07-22 15:24:09 +08:00
parent 03182b5781
commit eaf4d1bb50
4 changed files with 295 additions and 318 deletions

View File

@@ -20,7 +20,6 @@ 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;
@@ -28,6 +27,7 @@ import android.text.TextUtils;
import android.text.format.DateFormat;
import android.text.format.DateUtils;
import android.util.Log;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
@@ -62,22 +62,19 @@ import java.util.Map;
/** Controls the update for chart graph and the list items. */
public class BatteryChartPreferenceControllerV2 extends AbstractPreferenceController
implements PreferenceControllerMixin, LifecycleObserver, OnCreate, OnDestroy,
OnSaveInstanceState, BatteryChartViewV2.OnSelectListener, OnResume,
ExpandDividerPreference.OnExpandListener {
OnSaveInstanceState, OnResume, ExpandDividerPreference.OnExpandListener {
private static final String TAG = "BatteryChartPreferenceControllerV2";
private static final String KEY_FOOTER_PREF = "battery_graph_footer";
private static final String PACKAGE_NAME_NONE = "none";
/** 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 int TWENTY_FOUR_HOURS_TIME_SLOT_SIZE = 12;
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 final String KEY_DAILY_CHART_INDEX = "daily_chart_index";
private static final String KEY_HOURLY_CHART_INDEX = "hourly_chart_index";
private static int sUiMode = Configuration.UI_MODE_NIGHT_UNDEFINED;
@@ -91,23 +88,26 @@ public class BatteryChartPreferenceControllerV2 extends AbstractPreferenceContro
@VisibleForTesting
PreferenceGroup mAppListPrefGroup;
@VisibleForTesting
BatteryChartViewV2 mBatteryChartView;
@VisibleForTesting
ExpandDividerPreference mExpandDividerPreference;
@VisibleForTesting
boolean mIsExpanded = false;
@VisibleForTesting
long[] mBatteryHistoryKeys;
BatteryChartViewV2 mDailyChartView;
@VisibleForTesting
BatteryChartViewModel mViewModel;
BatteryChartViewV2 mHourlyChartView;
@VisibleForTesting
int mTrapezoidIndex = BatteryChartViewModel.SELECTED_INDEX_ALL;
int mDailyChartIndex = BatteryChartViewModel.SELECTED_INDEX_ALL;
@VisibleForTesting
int mHourlyChartIndex = BatteryChartViewModel.SELECTED_INDEX_ALL;
private boolean mIs24HourFormat;
private boolean mIsFooterPrefAdded = false;
private PreferenceScreen mPreferenceScreen;
private FooterPreference mFooterPreference;
private BatteryChartViewModel mDailyViewModel;
private List<BatteryChartViewModel> mHourlyViewModels;
private final String mPreferenceKey;
private final SettingsActivity mActivity;
@@ -152,12 +152,14 @@ public class BatteryChartPreferenceControllerV2 extends AbstractPreferenceContro
if (savedInstanceState == null) {
return;
}
mTrapezoidIndex =
savedInstanceState.getInt(KEY_CURRENT_TIME_SLOT, mTrapezoidIndex);
mDailyChartIndex =
savedInstanceState.getInt(KEY_DAILY_CHART_INDEX, mDailyChartIndex);
mHourlyChartIndex =
savedInstanceState.getInt(KEY_HOURLY_CHART_INDEX, mHourlyChartIndex);
mIsExpanded =
savedInstanceState.getBoolean(KEY_EXPAND_SYSTEM_INFO, mIsExpanded);
Log.d(TAG, String.format("onCreate() slotIndex=%d isExpanded=%b",
mTrapezoidIndex, mIsExpanded));
Log.d(TAG, String.format("onCreate() dailyIndex=%d hourlyIndex=%d isExpanded=%b",
mDailyChartIndex, mHourlyChartIndex, mIsExpanded));
}
@Override
@@ -179,10 +181,11 @@ public class BatteryChartPreferenceControllerV2 extends AbstractPreferenceContro
if (savedInstance == null) {
return;
}
savedInstance.putInt(KEY_CURRENT_TIME_SLOT, mTrapezoidIndex);
savedInstance.putInt(KEY_DAILY_CHART_INDEX, mDailyChartIndex);
savedInstance.putInt(KEY_HOURLY_CHART_INDEX, mHourlyChartIndex);
savedInstance.putBoolean(KEY_EXPAND_SYSTEM_INFO, mIsExpanded);
Log.d(TAG, String.format("onSaveInstanceState() slotIndex=%d isExpanded=%b",
mTrapezoidIndex, mIsExpanded));
Log.d(TAG, String.format("onSaveInstanceState() dailyIndex=%d hourlyIndex=%d isExpanded=%b",
mDailyChartIndex, mHourlyChartIndex, mIsExpanded));
}
@Override
@@ -249,18 +252,6 @@ public class BatteryChartPreferenceControllerV2 extends AbstractPreferenceContro
return true;
}
@Override
public void onSelect(int trapezoidIndex) {
Log.d(TAG, "onChartSelect:" + trapezoidIndex);
mTrapezoidIndex = trapezoidIndex;
refreshUi();
mMetricsFeatureProvider.action(
mPrefContext,
trapezoidIndex == BatteryChartViewModel.SELECTED_INDEX_ALL
? SettingsEnums.ACTION_BATTERY_USAGE_SHOW_ALL
: SettingsEnums.ACTION_BATTERY_USAGE_TIME_SLOT);
}
@Override
public void onExpand(boolean isExpanded) {
mIsExpanded = isExpanded;
@@ -273,68 +264,104 @@ public class BatteryChartPreferenceControllerV2 extends AbstractPreferenceContro
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;
mViewModel = null;
Log.d(TAG, "setBatteryHistoryMap() " + (batteryHistoryMap == null ? "null"
: ("size=" + batteryHistoryMap.size())));
final BatteryLevelData batteryLevelData =
DataProcessor.getBatteryLevelData(mContext, batteryHistoryMap);
Log.d(TAG, "getBatteryLevelData: " + batteryLevelData);
if (batteryLevelData == null) {
mDailyViewModel = null;
mHourlyViewModels = null;
addFooterPreferenceIfNeeded(false);
return;
}
mBatteryHistoryKeys = getBatteryHistoryKeys(batteryHistoryMap);
List<Integer> levels = new ArrayList<Integer>();
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));
levels.add(0);
continue;
}
// Averages the battery level in each time slot to avoid corner conditions.
float batteryLevelCounter = 0;
for (BatteryHistEntry entry : entryMap.values()) {
batteryLevelCounter += entry.mBatteryLevel;
}
levels.add(Math.round(batteryLevelCounter / entryMap.size()));
mDailyViewModel = new BatteryChartViewModel(
batteryLevelData.getDailyBatteryLevels().getLevels(),
generateTimestampDayOfWeekTexts(
mContext, batteryLevelData.getDailyBatteryLevels().getTimestamps()),
mDailyChartIndex);
mHourlyViewModels = new ArrayList<>();
for (BatteryLevelData.PeriodBatteryLevelData perDayData :
batteryLevelData.getHourlyBatteryLevelsPerDay()) {
mHourlyViewModels.add(new BatteryChartViewModel(
perDayData.getLevels(),
generateTimestampHourTexts(
mContext, perDayData.getTimestamps()),
mHourlyChartIndex));
}
final List<String> texts = generateTimestampTexts(mBatteryHistoryKeys, mContext);
mViewModel = new BatteryChartViewModel(levels, texts, mTrapezoidIndex);
refreshUi();
Log.d(TAG, String.format(
"setBatteryHistoryMap() size=%d key=%s\nview model=%s",
batteryHistoryMap.size(),
ConvertUtils.utcToLocalTime(mPrefContext,
mBatteryHistoryKeys[mBatteryHistoryKeys.length - 1]),
mViewModel));
// Loads item icon and label in the background.
new LoadAllItemsInfoTask(batteryHistoryMap).execute();
// TODO: Loads item icon and label and build mBatteryIndexedMap.
}
void setBatteryChartView(final BatteryChartViewV2 batteryChartView) {
if (mBatteryChartView != batteryChartView) {
mHandler.post(() -> setBatteryChartViewInner(batteryChartView));
void setBatteryChartView(@NonNull final BatteryChartViewV2 dailyChartView,
@NonNull final BatteryChartViewV2 hourlyChartView) {
if (mDailyChartView != dailyChartView || mHourlyChartView != hourlyChartView) {
mHandler.post(() -> setBatteryChartViewInner(dailyChartView, hourlyChartView));
}
}
private void setBatteryChartViewInner(final BatteryChartViewV2 batteryChartView) {
mBatteryChartView = batteryChartView;
mBatteryChartView.setOnSelectListener(this);
private void setBatteryChartViewInner(@NonNull final BatteryChartViewV2 dailyChartView,
@NonNull final BatteryChartViewV2 hourlyChartView) {
mDailyChartView = dailyChartView;
mDailyChartView.setOnSelectListener(trapezoidIndex -> {
if (mDailyChartIndex == trapezoidIndex) {
return;
}
Log.d(TAG, "onDailyChartSelect:" + trapezoidIndex);
mDailyChartIndex = trapezoidIndex;
mHourlyChartIndex = BatteryChartView.SELECTED_INDEX_ALL;
refreshUi();
// TODO: Change to log daily data.
});
mHourlyChartView = hourlyChartView;
mHourlyChartView.setOnSelectListener(trapezoidIndex -> {
if (mHourlyChartIndex == trapezoidIndex) {
return;
}
Log.d(TAG, "onHourlyChartSelect:" + trapezoidIndex);
mHourlyChartIndex = trapezoidIndex;
refreshUi();
mMetricsFeatureProvider.action(
mPrefContext,
trapezoidIndex == BatteryChartViewModel.SELECTED_INDEX_ALL
? SettingsEnums.ACTION_BATTERY_USAGE_SHOW_ALL
: SettingsEnums.ACTION_BATTERY_USAGE_TIME_SLOT);
});
refreshUi();
}
@VisibleForTesting
boolean refreshUi() {
// Invalid refresh condition.
if (mBatteryIndexedMap == null || mBatteryChartView == null) {
if (mBatteryIndexedMap == null || mDailyChartView == null || mHourlyChartView == null) {
return false;
}
if (mViewModel != null) {
mViewModel.setSelectedIndex(mTrapezoidIndex);
if (mDailyViewModel == null || mHourlyViewModels == null) {
// Fail to get battery level data, show an empty hourly chart view.
mDailyChartView.setVisibility(View.GONE);
mHourlyChartView.setViewModel(null);
return false;
}
if (isBatteryLevelDataInOneDay()) {
// Only 1 day data, hide the daily chart view.
mDailyChartView.setVisibility(View.GONE);
mDailyChartIndex = 0;
} else {
mDailyChartView.setVisibility(View.VISIBLE);
mDailyViewModel.setSelectedIndex(mDailyChartIndex);
mDailyChartView.setViewModel(mDailyViewModel);
}
if (mDailyChartIndex == BatteryChartViewModel.SELECTED_INDEX_ALL) {
// Multiple days are selected, hide the hourly chart view.
mHourlyChartView.setVisibility(View.GONE);
} else {
mHourlyChartView.setVisibility(View.VISIBLE);
final BatteryChartViewModel hourlyViewModel = mHourlyViewModels.get(mDailyChartIndex);
hourlyViewModel.setSelectedIndex(mHourlyChartIndex);
mHourlyChartView.setViewModel(hourlyViewModel);
}
mBatteryChartView.setViewModel(mViewModel);
mHandler.post(() -> {
final long start = System.currentTimeMillis();
@@ -348,11 +375,13 @@ public class BatteryChartPreferenceControllerV2 extends AbstractPreferenceContro
}
private void addAllPreferences() {
// TODO: Get the right diff entry according to daily and hourly chart selection.
final int index = mDailyChartIndex;
final List<BatteryDiffEntry> entries =
mBatteryIndexedMap.get(Integer.valueOf(mTrapezoidIndex));
mBatteryIndexedMap.get(Integer.valueOf(index));
addFooterPreferenceIfNeeded(entries != null && !entries.isEmpty());
if (entries == null) {
Log.w(TAG, "cannot find BatteryDiffEntry for:" + mTrapezoidIndex);
Log.w(TAG, "cannot find BatteryDiffEntry for:" + index);
return;
}
// Separates data into two groups and sort them individually.
@@ -370,7 +399,7 @@ public class BatteryChartPreferenceControllerV2 extends AbstractPreferenceContro
appEntries.add(entry);
}
// Validates the usage time if users click a specific slot.
if (mTrapezoidIndex >= 0) {
if (index >= 0) {
validateUsageTime(entry);
}
});
@@ -488,6 +517,7 @@ public class BatteryChartPreferenceControllerV2 extends AbstractPreferenceContro
}
private String getSlotInformation(boolean isApp, String slotInformation) {
// TODO: Updates the right slot information from daily and hourly chart selection.
// Null means we show all information without a specific time slot.
if (slotInformation == null) {
return isApp
@@ -501,16 +531,8 @@ public class BatteryChartPreferenceControllerV2 extends AbstractPreferenceContro
}
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 mIs24HourFormat
? String.format("%s%s", fromHour, toHour)
: String.format("%s %s", fromHour, toHour);
// TODO: Generate the right slot information from daily and hourly chart selection.
return null;
}
@VisibleForTesting
@@ -584,13 +606,25 @@ public class BatteryChartPreferenceControllerV2 extends AbstractPreferenceContro
mHandler.post(() -> mPreferenceScreen.addPreference(mFooterPreference));
}
private static List<String> generateTimestampTexts(
@NonNull long[] timestamps, Context context) {
private boolean isBatteryLevelDataInOneDay() {
return mHourlyViewModels.size() == 1;
}
private static List<String> generateTimestampDayOfWeekTexts(
@NonNull final Context context, @NonNull final List<Long> timestamps) {
final ArrayList<String> texts = new ArrayList<>();
for (Long timestamp : timestamps) {
texts.add(ConvertUtils.utcToLocalTimeDayOfWeek(context, timestamp));
}
return texts;
}
private static List<String> generateTimestampHourTexts(
@NonNull final Context context, @NonNull final List<Long> timestamps) {
final boolean is24HourFormat = DateFormat.is24HourFormat(context);
final List<String> texts = new ArrayList<String>();
for (int index = 0; index < CHART_LEVEL_ARRAY_SIZE; index++) {
texts.add(ConvertUtils.utcToLocalTimeHour(context, timestamps[index * 2],
is24HourFormat));
final ArrayList<String> texts = new ArrayList<>();
for (Long timestamp : timestamps) {
texts.add(ConvertUtils.utcToLocalTimeHour(context, timestamp, is24HourFormat));
}
return texts;
}
@@ -620,6 +654,8 @@ public class BatteryChartPreferenceControllerV2 extends AbstractPreferenceContro
return true;
}
// TODO: Change this method to fromLastFullCharged.
/** Used for {@link AppBatteryPreferenceController}. */
public static List<BatteryDiffEntry> getBatteryLast24HrUsageData(Context context) {
final long start = System.currentTimeMillis();
@@ -632,16 +668,27 @@ public class BatteryChartPreferenceControllerV2 extends AbstractPreferenceContro
}
Log.d(TAG, String.format("getBatteryLast24HrData() size=%d time=%d/ms",
batteryHistoryMap.size(), (System.currentTimeMillis() - start)));
final List<Long> batteryHistoryKeyList =
new ArrayList<>(batteryHistoryMap.keySet());
Collections.sort(batteryHistoryKeyList);
final long[] batteryHistoryKeys = new long[TWENTY_FOUR_HOURS_TIME_SLOT_SIZE + 1];
for (int index = 0; index < batteryHistoryKeys.length; index++) {
batteryHistoryKeys[index] = batteryHistoryKeyList.get(index);
}
final Map<Integer, List<BatteryDiffEntry>> batteryIndexedMap =
ConvertUtils.getIndexedUsageMap(
context,
/*timeSlotSize=*/ CHART_LEVEL_ARRAY_SIZE - 1,
getBatteryHistoryKeys(batteryHistoryMap),
/*timeSlotSize=*/ TWENTY_FOUR_HOURS_TIME_SLOT_SIZE,
batteryHistoryKeys,
batteryHistoryMap,
/*purgeLowPercentageAndFakeData=*/ true);
return batteryIndexedMap.get(BatteryChartViewModel.SELECTED_INDEX_ALL);
}
// TODO: Change this method to fromLastFullCharged.
/** Used for {@link AppBatteryPreferenceController}. */
public static BatteryDiffEntry getBatteryLast24HrUsageData(
Context context, String packageName, int userId) {
@@ -663,65 +710,4 @@ public class BatteryChartPreferenceControllerV2 extends AbstractPreferenceContro
}
return null;
}
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;
refreshUi();
});
}
}
}

View File

@@ -20,6 +20,8 @@ import androidx.annotation.NonNull;
import androidx.core.util.Preconditions;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
/** Wraps the battery timestamp and level data used for battery usage chart. */
public final class BatteryLevelData {
@@ -46,6 +48,12 @@ public final class BatteryLevelData {
public List<Integer> getLevels() {
return mLevels;
}
@Override
public String toString() {
return String.format(Locale.ENGLISH, "timestamps: %s; levels: %s",
Objects.toString(mTimestamps), Objects.toString(mLevels));
}
}
/**
@@ -78,4 +86,13 @@ public final class BatteryLevelData {
public List<PeriodBatteryLevelData> getHourlyBatteryLevelsPerDay() {
return mHourlyBatteryLevelsPerDay;
}
}
@Override
public String toString() {
return String.format(Locale.ENGLISH,
"dailyBatteryLevels: %s; hourlyBatteryLevelsPerDay: %s",
Objects.toString(mDailyBatteryLevels),
Objects.toString(mHourlyBatteryLevelsPerDay));
}
}

View File

@@ -140,7 +140,7 @@ public final class ConvertUtils {
/** Converts UTC timestamp to local time hour data. */
public static String utcToLocalTimeHour(
Context context, long timestamp, boolean is24HourFormat) {
final Context context, final long timestamp, final boolean is24HourFormat) {
final Locale locale = getLocale(context);
// e.g. for 12-hour format: 9 pm
// e.g. for 24-hour format: 09:00
@@ -149,6 +149,13 @@ public final class ConvertUtils {
return DateFormat.format(pattern, timestamp).toString().toLowerCase(locale);
}
/** Converts UTC timestamp to local time day of week data. */
public static String utcToLocalTimeDayOfWeek(final Context context, final long timestamp) {
final Locale locale = getLocale(context);
final String pattern = DateFormat.getBestDateTimePattern(locale, "E");
return DateFormat.format(pattern, timestamp).toString().toUpperCase(locale);
}
/** Gets indexed battery usage data for each corresponding time slot. */
public static Map<Integer, List<BatteryDiffEntry>> getIndexedUsageMap(
final Context context,