Add battery chart view model.

Test: manual
Bug: 239491373
Bug: 236101166
Change-Id: I1ae0e5fcc006855ac552fbbdfb4cd73f3dec52e7
This commit is contained in:
Zaiyue Xue
2022-07-19 17:39:11 +08:00
committed by YK Hung
parent efbb071933
commit 05bf785859
5 changed files with 251 additions and 219 deletions

View File

@@ -29,6 +29,7 @@ import android.text.format.DateFormat;
import android.text.format.DateUtils;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
import androidx.preference.PreferenceGroup;
@@ -53,7 +54,6 @@ 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;
@@ -98,13 +98,13 @@ public class BatteryChartPreferenceControllerV2 extends AbstractPreferenceContro
@VisibleForTesting
boolean mIsExpanded = false;
@VisibleForTesting
int[] mBatteryHistoryLevels;
@VisibleForTesting
long[] mBatteryHistoryKeys;
@VisibleForTesting
int mTrapezoidIndex = BatteryChartViewV2.SELECTED_INDEX_INVALID;
BatteryChartViewModel mViewModel;
@VisibleForTesting
int mTrapezoidIndex = BatteryChartViewModel.SELECTED_INDEX_ALL;
private boolean mIs24HourFormat = false;
private boolean mIs24HourFormat;
private boolean mIsFooterPrefAdded = false;
private PreferenceScreen mPreferenceScreen;
private FooterPreference mFooterPreference;
@@ -252,10 +252,11 @@ public class BatteryChartPreferenceControllerV2 extends AbstractPreferenceContro
@Override
public void onSelect(int trapezoidIndex) {
Log.d(TAG, "onChartSelect:" + trapezoidIndex);
refreshUi(trapezoidIndex, /*isForce=*/ false);
mTrapezoidIndex = trapezoidIndex;
refreshUi();
mMetricsFeatureProvider.action(
mPrefContext,
trapezoidIndex == BatteryChartViewV2.SELECTED_INDEX_ALL
trapezoidIndex == BatteryChartViewModel.SELECTED_INDEX_ALL
? SettingsEnums.ACTION_BATTERY_USAGE_SHOW_ALL
: SettingsEnums.ACTION_BATTERY_USAGE_TIME_SLOT);
}
@@ -276,18 +277,19 @@ public class BatteryChartPreferenceControllerV2 extends AbstractPreferenceContro
if (batteryHistoryMap == null || batteryHistoryMap.isEmpty()) {
mBatteryIndexedMap = null;
mBatteryHistoryKeys = null;
mBatteryHistoryLevels = null;
mViewModel = null;
addFooterPreferenceIfNeeded(false);
return;
}
mBatteryHistoryKeys = getBatteryHistoryKeys(batteryHistoryMap);
mBatteryHistoryLevels = new int[CHART_LEVEL_ARRAY_SIZE];
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.
@@ -295,16 +297,17 @@ public class BatteryChartPreferenceControllerV2 extends AbstractPreferenceContro
for (BatteryHistEntry entry : entryMap.values()) {
batteryLevelCounter += entry.mBatteryLevel;
}
mBatteryHistoryLevels[index] =
Math.round(batteryLevelCounter / entryMap.size());
levels.add(Math.round(batteryLevelCounter / entryMap.size()));
}
forceRefreshUi();
final List<String> texts = generateTimestampTexts(mBatteryHistoryKeys, mContext);
mViewModel = new BatteryChartViewModel(levels, texts, mTrapezoidIndex);
refreshUi();
Log.d(TAG, String.format(
"setBatteryHistoryMap() size=%d key=%s\nlevels=%s",
"setBatteryHistoryMap() size=%d key=%s\nview model=%s",
batteryHistoryMap.size(),
ConvertUtils.utcToLocalTime(mPrefContext,
mBatteryHistoryKeys[mBatteryHistoryKeys.length - 1]),
Arrays.toString(mBatteryHistoryLevels)));
mViewModel));
// Loads item icon and label in the background.
new LoadAllItemsInfoTask(batteryHistoryMap).execute();
@@ -319,35 +322,20 @@ public class BatteryChartPreferenceControllerV2 extends AbstractPreferenceContro
private void setBatteryChartViewInner(final BatteryChartViewV2 batteryChartView) {
mBatteryChartView = batteryChartView;
mBatteryChartView.setOnSelectListener(this);
forceRefreshUi();
}
private void forceRefreshUi() {
final int refreshIndex =
mTrapezoidIndex == BatteryChartViewV2.SELECTED_INDEX_INVALID
? BatteryChartViewV2.SELECTED_INDEX_ALL
: mTrapezoidIndex;
if (mBatteryChartView != null) {
mBatteryChartView.setLevels(mBatteryHistoryLevels);
mBatteryChartView.setSelectedIndex(refreshIndex);
setTimestampLabel();
}
refreshUi(refreshIndex, /*isForce=*/ true);
refreshUi();
}
@VisibleForTesting
boolean refreshUi(int trapezoidIndex, boolean isForce) {
boolean refreshUi() {
// Invalid refresh condition.
if (mBatteryIndexedMap == null
|| mBatteryChartView == null
|| (mTrapezoidIndex == trapezoidIndex && !isForce)) {
if (mBatteryIndexedMap == null || mBatteryChartView == null) {
return false;
}
Log.d(TAG, String.format("refreshUi: index=%d size=%d isForce:%b",
trapezoidIndex, mBatteryIndexedMap.size(), isForce));
if (mViewModel != null) {
mViewModel.setSelectedIndex(mTrapezoidIndex);
}
mBatteryChartView.setViewModel(mViewModel);
mTrapezoidIndex = trapezoidIndex;
mBatteryChartView.setSelectedIndex(mTrapezoidIndex);
mHandler.post(() -> {
final long start = System.currentTimeMillis();
removeAndCacheAllPrefs();
@@ -584,20 +572,6 @@ public class BatteryChartPreferenceControllerV2 extends AbstractPreferenceContro
return !contains(packageName, mNotAllowShowEntryPackages);
}
@VisibleForTesting
void setTimestampLabel() {
if (mBatteryChartView == null || mBatteryHistoryKeys == null) {
return;
}
final boolean is24HourFormat = DateFormat.is24HourFormat(mContext);
final String[] labels = new String[mBatteryHistoryKeys.length];
for (int i = 0; i < mBatteryHistoryKeys.length; i++) {
labels[i] = ConvertUtils.utcToLocalTimeHour(mContext, mBatteryHistoryKeys[i],
is24HourFormat);
}
mBatteryChartView.setAxisLabels(labels);
}
private void addFooterPreferenceIfNeeded(boolean containAppItems) {
if (mIsFooterPrefAdded || mFooterPreference == null) {
return;
@@ -610,6 +584,17 @@ public class BatteryChartPreferenceControllerV2 extends AbstractPreferenceContro
mHandler.post(() -> mPreferenceScreen.addPreference(mFooterPreference));
}
private static List<String> generateTimestampTexts(
@NonNull long[] timestamps, Context context) {
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));
}
return texts;
}
private static boolean contains(String target, CharSequence[] packageNames) {
if (target != null && packageNames != null) {
for (CharSequence packageName : packageNames) {
@@ -654,7 +639,7 @@ public class BatteryChartPreferenceControllerV2 extends AbstractPreferenceContro
getBatteryHistoryKeys(batteryHistoryMap),
batteryHistoryMap,
/*purgeLowPercentageAndFakeData=*/ true);
return batteryIndexedMap.get(BatteryChartViewV2.SELECTED_INDEX_ALL);
return batteryIndexedMap.get(BatteryChartViewModel.SELECTED_INDEX_ALL);
}
/** Used for {@link AppBatteryPreferenceController}. */
@@ -735,7 +720,7 @@ public class BatteryChartPreferenceControllerV2 extends AbstractPreferenceContro
// Posts results back to main thread to refresh UI.
mHandler.post(() -> {
mBatteryIndexedMap = indexedUsageMap;
forceRefreshUi();
refreshUi();
});
}
}

View File

@@ -0,0 +1,98 @@
/*
* Copyright (C) 2022 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.batteryusage;
import androidx.annotation.NonNull;
import androidx.core.util.Preconditions;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
/** The view model of {@code BatteryChartViewV2} */
class BatteryChartViewModel {
private static final String TAG = "BatteryChartViewModel";
public static final int SELECTED_INDEX_ALL = -1;
public static final int SELECTED_INDEX_INVALID = -2;
// We need at least 2 levels to draw a trapezoid.
private static final int MIN_LEVELS_DATA_SIZE = 2;
private final List<Integer> mLevels;
private final List<String> mTexts;
private int mSelectedIndex;
BatteryChartViewModel(
@NonNull List<Integer> levels, @NonNull List<String> texts, int selectedIndex) {
Preconditions.checkArgument(
levels.size() == texts.size()
&& levels.size() >= MIN_LEVELS_DATA_SIZE
&& selectedIndex >= SELECTED_INDEX_ALL
&& selectedIndex < levels.size(),
String.format(Locale.getDefault(), "Invalid BatteryChartViewModel"
+ " levels.size: %d\ntexts.size: %d\nselectedIndex: %d.",
levels.size(), texts.size(), selectedIndex));
mLevels = levels;
mTexts = texts;
mSelectedIndex = selectedIndex;
}
public int size() {
return mLevels.size();
}
public List<Integer> levels() {
return mLevels;
}
public List<String> texts() {
return mTexts;
}
public int selectedIndex() {
return mSelectedIndex;
}
public void setSelectedIndex(int index) {
mSelectedIndex = index;
}
@Override
public int hashCode() {
return Objects.hash(mLevels, mTexts, mSelectedIndex);
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
} else if (!(other instanceof BatteryChartViewModel)) {
return false;
}
final BatteryChartViewModel batteryChartViewModel = (BatteryChartViewModel) other;
return Objects.equals(mLevels, batteryChartViewModel.mLevels)
&& Objects.equals(mTexts, batteryChartViewModel.mTexts)
&& mSelectedIndex == batteryChartViewModel.mSelectedIndex;
}
@Override
public String toString() {
return String.format(Locale.getDefault(), "levels: %s\ntexts: %s\nselectedIndex: %d",
Objects.toString(mLevels), Objects.toString(mTexts), mSelectedIndex);
}
}

View File

@@ -20,7 +20,6 @@ import static com.android.settings.Utils.formatPercentage;
import static java.lang.Math.round;
import android.accessibilityservice.AccessibilityServiceInfo;
import android.annotation.NonNull;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
@@ -38,6 +37,7 @@ import android.view.View;
import android.view.accessibility.AccessibilityManager;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.appcompat.widget.AppCompatImageView;
@@ -61,16 +61,14 @@ public class BatteryChartViewV2 extends AppCompatImageView implements View.OnCli
private static final int DIVIDER_COLOR = Color.parseColor("#CDCCC5");
private static final long UPDATE_STATE_DELAYED_TIME = 500L;
/** Selects all trapezoid shapes. */
public static final int SELECTED_INDEX_ALL = -1;
public static final int SELECTED_INDEX_INVALID = -2;
/** A callback listener for selected group index is updated. */
public interface OnSelectListener {
/** The callback function for selected group index is updated. */
void onSelect(int trapezoidIndex);
}
private BatteryChartViewModel mViewModel;
private int mDividerWidth;
private int mDividerHeight;
private float mTrapezoidVOffset;
@@ -79,9 +77,7 @@ public class BatteryChartViewV2 extends AppCompatImageView implements View.OnCli
private String[] mPercentages = getPercentages();
@VisibleForTesting
int mHoveredIndex = SELECTED_INDEX_INVALID;
@VisibleForTesting
int mSelectedIndex = SELECTED_INDEX_INVALID;
int mHoveredIndex = BatteryChartViewModel.SELECTED_INDEX_INVALID;
@VisibleForTesting
String[] mAxisLabels;
@@ -103,7 +99,6 @@ public class BatteryChartViewV2 extends AppCompatImageView implements View.OnCli
@VisibleForTesting
final Runnable mUpdateClickableStateRun = () -> updateClickableState();
private int[] mLevels;
private Paint mTextPaint;
private Paint mDividerPaint;
private Paint mTrapezoidPaint;
@@ -126,44 +121,26 @@ public class BatteryChartViewV2 extends AppCompatImageView implements View.OnCli
initializeColors(context);
// Registers the click event listener.
setOnClickListener(this);
setSelectedIndex(SELECTED_INDEX_ALL);
setClickable(false);
requestLayout();
}
/** Sets all levels value to draw the trapezoid shape */
public void setLevels(int[] levels) {
Log.d(TAG, "setLevels() " + (levels == null ? "null" : levels.length));
// At least 2 levels to draw a trapezoid.
if (levels == null || levels.length < 2) {
mLevels = null;
/** Sets the data model of this view. */
public void setViewModel(BatteryChartViewModel viewModel) {
if (viewModel == null) {
mViewModel = null;
invalidate();
return;
}
mLevels = levels;
// Initialize trapezoid slots.
mTrapezoidSlots = new TrapezoidSlot[mLevels.length - 1];
for (int index = 0; index < mTrapezoidSlots.length; index++) {
mTrapezoidSlots[index] = new TrapezoidSlot();
}
Log.d(TAG, String.format("setViewModel(): size: %d, selectedIndex: %d.",
viewModel.size(), viewModel.selectedIndex()));
mViewModel = viewModel;
setClickable(false);
invalidate();
// Sets the chart is clickable if there is at least one valid item in it.
for (int index = 0; index < mLevels.length - 1; index++) {
if (mLevels[index] != 0 && mLevels[index + 1] != 0) {
setClickable(true);
break;
}
}
}
/** Sets the selected group index to draw highlight effect. */
public void setSelectedIndex(int index) {
if (mSelectedIndex != index) {
mSelectedIndex = index;
invalidate();
}
initializeTrapezoidSlots(viewModel.size() - 1);
initializeAxisLabels(viewModel.texts());
setClickable(hasNonZeroTrapezoid(viewModel.levels()));
requestLayout();
}
/** Sets the callback to monitor the selected group index. */
@@ -184,26 +161,6 @@ public class BatteryChartViewV2 extends AppCompatImageView implements View.OnCli
requestLayout();
}
/**
* Sets the X-axis labels list for each level. This class will choose some labels among the
* input list to show.
*
* @param labels The length of this parameter should be the same as the length of
* {@code levels}.
*/
public void setAxisLabels(@NonNull String[] labels) {
if (mAxisLabels == null) {
mAxisLabels = new String[DEFAULT_AXIS_LABEL_COUNT];
}
// Current logic is always showing {@code AXIS_LABEL_GAPS_COUNT} labels.
// TODO: Support different count of labels for different levels sizes.
final int step = (labels.length - 1) / AXIS_LABEL_GAPS_COUNT;
for (int index = 0; index < DEFAULT_AXIS_LABEL_COUNT; index++) {
mAxisLabels[index] = labels[index * step];
}
requestLayout();
}
@Override
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
@@ -240,7 +197,7 @@ public class BatteryChartViewV2 extends AppCompatImageView implements View.OnCli
// Before mLevels initialized, the count of trapezoids is unknown. Only draws the
// horizontal percentages and dividers.
drawHorizontalDividers(canvas);
if (mLevels == null) {
if (mViewModel == null) {
return;
}
drawVerticalDividers(canvas);
@@ -282,7 +239,7 @@ public class BatteryChartViewV2 extends AppCompatImageView implements View.OnCli
public void onHoverChanged(boolean hovered) {
super.onHoverChanged(hovered);
if (!hovered) {
mHoveredIndex = SELECTED_INDEX_INVALID; // reset
mHoveredIndex = BatteryChartViewModel.SELECTED_INDEX_INVALID; // reset
invalidate();
}
}
@@ -295,13 +252,15 @@ public class BatteryChartViewV2 extends AppCompatImageView implements View.OnCli
}
final int trapezoidIndex = getTrapezoidIndex(mTouchUpEventX);
// Ignores the click event if the level is zero.
if (trapezoidIndex == SELECTED_INDEX_INVALID
if (trapezoidIndex == BatteryChartViewModel.SELECTED_INDEX_INVALID
|| !isValidToDraw(trapezoidIndex)) {
return;
}
if (mOnSelectListener != null) {
// Selects all if users click the same trapezoid item two times.
mOnSelectListener.onSelect(
trapezoidIndex == mSelectedIndex ? SELECTED_INDEX_ALL : trapezoidIndex);
trapezoidIndex == mViewModel.selectedIndex()
? BatteryChartViewModel.SELECTED_INDEX_ALL : trapezoidIndex);
}
view.performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK);
}
@@ -350,8 +309,8 @@ public class BatteryChartViewV2 extends AppCompatImageView implements View.OnCli
mTrapezoidCurvePaint.setStrokeWidth(mDividerWidth * 2);
} else if (mIsSlotsClickabled) {
mTrapezoidCurvePaint = null;
// Sets levels again to force update the click state.
setLevels(mLevels);
// Sets view model again to force update the click state.
setViewModel(mViewModel);
}
invalidate();
}
@@ -366,6 +325,28 @@ public class BatteryChartViewV2 extends AppCompatImageView implements View.OnCli
super.setClickable(clickable);
}
private void initializeTrapezoidSlots(int count) {
mTrapezoidSlots = new TrapezoidSlot[count];
for (int index = 0; index < mTrapezoidSlots.length; index++) {
mTrapezoidSlots[index] = new TrapezoidSlot();
}
}
/**
* Initializes the displayed X-axis labels list selected from the model all texts list.
*/
private void initializeAxisLabels(@NonNull List<String> allTexts) {
if (mAxisLabels == null) {
mAxisLabels = new String[DEFAULT_AXIS_LABEL_COUNT];
}
// Current logic is always showing {@code AXIS_LABEL_GAPS_COUNT} labels.
// TODO: Support different count of labels for different levels sizes.
final int step = (allTexts.size() - 1) / AXIS_LABEL_GAPS_COUNT;
for (int index = 0; index < DEFAULT_AXIS_LABEL_COUNT; index++) {
mAxisLabels[index] = allTexts.get(index * step);
}
}
private void initializeColors(Context context) {
setBackgroundColor(Color.TRANSPARENT);
mTrapezoidSolidColor = Utils.getColorAccentDefaultColor(context);
@@ -498,7 +479,7 @@ public class BatteryChartViewV2 extends AppCompatImageView implements View.OnCli
private void drawTrapezoids(Canvas canvas) {
// Ignores invalid trapezoid data.
if (mLevels == null) {
if (mViewModel == null) {
return;
}
final float trapezoidBottom =
@@ -519,17 +500,17 @@ public class BatteryChartViewV2 extends AppCompatImageView implements View.OnCli
continue;
}
// Configures the trapezoid paint color.
final int trapezoidColor =
!mIsSlotsClickabled
? mTrapezoidColor
: mSelectedIndex == index || mSelectedIndex == SELECTED_INDEX_ALL
? mTrapezoidSolidColor : mTrapezoidColor;
final int trapezoidColor = mIsSlotsClickabled && (mViewModel.selectedIndex() == index
|| mViewModel.selectedIndex() == BatteryChartViewModel.SELECTED_INDEX_ALL)
? mTrapezoidSolidColor : mTrapezoidColor;
final boolean isHoverState =
mIsSlotsClickabled && mHoveredIndex == index && isValidToDraw(mHoveredIndex);
mTrapezoidPaint.setColor(isHoverState ? mTrapezoidHoverColor : trapezoidColor);
final float leftTop = round(trapezoidBottom - mLevels[index] * unitHeight);
final float rightTop = round(trapezoidBottom - mLevels[index + 1] * unitHeight);
final float leftTop = round(
trapezoidBottom - mViewModel.levels().get(index) * unitHeight);
final float rightTop = round(
trapezoidBottom - mViewModel.levels().get(index + 1) * unitHeight);
trapezoidPath.reset();
trapezoidPath.moveTo(mTrapezoidSlots[index].mLeft, trapezoidBottom);
trapezoidPath.lineTo(mTrapezoidSlots[index].mLeft, leftTop);
@@ -568,15 +549,25 @@ public class BatteryChartViewV2 extends AppCompatImageView implements View.OnCli
return index;
}
}
return SELECTED_INDEX_INVALID;
return BatteryChartViewModel.SELECTED_INDEX_INVALID;
}
private boolean isValidToDraw(int trapezoidIndex) {
return mLevels != null
return mViewModel != null
&& trapezoidIndex >= 0
&& trapezoidIndex < mLevels.length - 1
&& mLevels[trapezoidIndex] != 0
&& mLevels[trapezoidIndex + 1] != 0;
&& trapezoidIndex < mViewModel.size() - 1
&& mViewModel.levels().get(trapezoidIndex) != 0
&& mViewModel.levels().get(trapezoidIndex + 1) != 0;
}
private static boolean hasNonZeroTrapezoid(List<Integer> levels) {
// Sets the chart is clickable if there is at least one valid item in it.
for (int index = 0; index < levels.size() - 1; index++) {
if (levels.get(index) != 0 && levels.get(index + 1) != 0) {
return true;
}
}
return false;
}
private static String[] getPercentages() {