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.text.format.DateUtils;
import android.util.Log; import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference; import androidx.preference.Preference;
import androidx.preference.PreferenceGroup; import androidx.preference.PreferenceGroup;
@@ -53,7 +54,6 @@ import com.android.settingslib.utils.StringUtil;
import com.android.settingslib.widget.FooterPreference; import com.android.settingslib.widget.FooterPreference;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@@ -98,13 +98,13 @@ public class BatteryChartPreferenceControllerV2 extends AbstractPreferenceContro
@VisibleForTesting @VisibleForTesting
boolean mIsExpanded = false; boolean mIsExpanded = false;
@VisibleForTesting @VisibleForTesting
int[] mBatteryHistoryLevels;
@VisibleForTesting
long[] mBatteryHistoryKeys; long[] mBatteryHistoryKeys;
@VisibleForTesting @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 boolean mIsFooterPrefAdded = false;
private PreferenceScreen mPreferenceScreen; private PreferenceScreen mPreferenceScreen;
private FooterPreference mFooterPreference; private FooterPreference mFooterPreference;
@@ -252,10 +252,11 @@ public class BatteryChartPreferenceControllerV2 extends AbstractPreferenceContro
@Override @Override
public void onSelect(int trapezoidIndex) { public void onSelect(int trapezoidIndex) {
Log.d(TAG, "onChartSelect:" + trapezoidIndex); Log.d(TAG, "onChartSelect:" + trapezoidIndex);
refreshUi(trapezoidIndex, /*isForce=*/ false); mTrapezoidIndex = trapezoidIndex;
refreshUi();
mMetricsFeatureProvider.action( mMetricsFeatureProvider.action(
mPrefContext, mPrefContext,
trapezoidIndex == BatteryChartViewV2.SELECTED_INDEX_ALL trapezoidIndex == BatteryChartViewModel.SELECTED_INDEX_ALL
? SettingsEnums.ACTION_BATTERY_USAGE_SHOW_ALL ? SettingsEnums.ACTION_BATTERY_USAGE_SHOW_ALL
: SettingsEnums.ACTION_BATTERY_USAGE_TIME_SLOT); : SettingsEnums.ACTION_BATTERY_USAGE_TIME_SLOT);
} }
@@ -276,18 +277,19 @@ public class BatteryChartPreferenceControllerV2 extends AbstractPreferenceContro
if (batteryHistoryMap == null || batteryHistoryMap.isEmpty()) { if (batteryHistoryMap == null || batteryHistoryMap.isEmpty()) {
mBatteryIndexedMap = null; mBatteryIndexedMap = null;
mBatteryHistoryKeys = null; mBatteryHistoryKeys = null;
mBatteryHistoryLevels = null; mViewModel = null;
addFooterPreferenceIfNeeded(false); addFooterPreferenceIfNeeded(false);
return; return;
} }
mBatteryHistoryKeys = getBatteryHistoryKeys(batteryHistoryMap); 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++) { for (int index = 0; index < CHART_LEVEL_ARRAY_SIZE; index++) {
final long timestamp = mBatteryHistoryKeys[index * 2]; final long timestamp = mBatteryHistoryKeys[index * 2];
final Map<String, BatteryHistEntry> entryMap = batteryHistoryMap.get(timestamp); final Map<String, BatteryHistEntry> entryMap = batteryHistoryMap.get(timestamp);
if (entryMap == null || entryMap.isEmpty()) { if (entryMap == null || entryMap.isEmpty()) {
Log.e(TAG, "abnormal entry list in the timestamp:" Log.e(TAG, "abnormal entry list in the timestamp:"
+ ConvertUtils.utcToLocalTime(mPrefContext, timestamp)); + ConvertUtils.utcToLocalTime(mPrefContext, timestamp));
levels.add(0);
continue; continue;
} }
// Averages the battery level in each time slot to avoid corner conditions. // 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()) { for (BatteryHistEntry entry : entryMap.values()) {
batteryLevelCounter += entry.mBatteryLevel; batteryLevelCounter += entry.mBatteryLevel;
} }
mBatteryHistoryLevels[index] = levels.add(Math.round(batteryLevelCounter / entryMap.size()));
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( Log.d(TAG, String.format(
"setBatteryHistoryMap() size=%d key=%s\nlevels=%s", "setBatteryHistoryMap() size=%d key=%s\nview model=%s",
batteryHistoryMap.size(), batteryHistoryMap.size(),
ConvertUtils.utcToLocalTime(mPrefContext, ConvertUtils.utcToLocalTime(mPrefContext,
mBatteryHistoryKeys[mBatteryHistoryKeys.length - 1]), mBatteryHistoryKeys[mBatteryHistoryKeys.length - 1]),
Arrays.toString(mBatteryHistoryLevels))); mViewModel));
// Loads item icon and label in the background. // Loads item icon and label in the background.
new LoadAllItemsInfoTask(batteryHistoryMap).execute(); new LoadAllItemsInfoTask(batteryHistoryMap).execute();
@@ -319,35 +322,20 @@ public class BatteryChartPreferenceControllerV2 extends AbstractPreferenceContro
private void setBatteryChartViewInner(final BatteryChartViewV2 batteryChartView) { private void setBatteryChartViewInner(final BatteryChartViewV2 batteryChartView) {
mBatteryChartView = batteryChartView; mBatteryChartView = batteryChartView;
mBatteryChartView.setOnSelectListener(this); mBatteryChartView.setOnSelectListener(this);
forceRefreshUi(); refreshUi();
}
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);
} }
@VisibleForTesting @VisibleForTesting
boolean refreshUi(int trapezoidIndex, boolean isForce) { boolean refreshUi() {
// Invalid refresh condition. // Invalid refresh condition.
if (mBatteryIndexedMap == null if (mBatteryIndexedMap == null || mBatteryChartView == null) {
|| mBatteryChartView == null
|| (mTrapezoidIndex == trapezoidIndex && !isForce)) {
return false; return false;
} }
Log.d(TAG, String.format("refreshUi: index=%d size=%d isForce:%b", if (mViewModel != null) {
trapezoidIndex, mBatteryIndexedMap.size(), isForce)); mViewModel.setSelectedIndex(mTrapezoidIndex);
}
mBatteryChartView.setViewModel(mViewModel);
mTrapezoidIndex = trapezoidIndex;
mBatteryChartView.setSelectedIndex(mTrapezoidIndex);
mHandler.post(() -> { mHandler.post(() -> {
final long start = System.currentTimeMillis(); final long start = System.currentTimeMillis();
removeAndCacheAllPrefs(); removeAndCacheAllPrefs();
@@ -584,20 +572,6 @@ public class BatteryChartPreferenceControllerV2 extends AbstractPreferenceContro
return !contains(packageName, mNotAllowShowEntryPackages); 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) { private void addFooterPreferenceIfNeeded(boolean containAppItems) {
if (mIsFooterPrefAdded || mFooterPreference == null) { if (mIsFooterPrefAdded || mFooterPreference == null) {
return; return;
@@ -610,6 +584,17 @@ public class BatteryChartPreferenceControllerV2 extends AbstractPreferenceContro
mHandler.post(() -> mPreferenceScreen.addPreference(mFooterPreference)); 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) { private static boolean contains(String target, CharSequence[] packageNames) {
if (target != null && packageNames != null) { if (target != null && packageNames != null) {
for (CharSequence packageName : packageNames) { for (CharSequence packageName : packageNames) {
@@ -654,7 +639,7 @@ public class BatteryChartPreferenceControllerV2 extends AbstractPreferenceContro
getBatteryHistoryKeys(batteryHistoryMap), getBatteryHistoryKeys(batteryHistoryMap),
batteryHistoryMap, batteryHistoryMap,
/*purgeLowPercentageAndFakeData=*/ true); /*purgeLowPercentageAndFakeData=*/ true);
return batteryIndexedMap.get(BatteryChartViewV2.SELECTED_INDEX_ALL); return batteryIndexedMap.get(BatteryChartViewModel.SELECTED_INDEX_ALL);
} }
/** Used for {@link AppBatteryPreferenceController}. */ /** Used for {@link AppBatteryPreferenceController}. */
@@ -735,7 +720,7 @@ public class BatteryChartPreferenceControllerV2 extends AbstractPreferenceContro
// Posts results back to main thread to refresh UI. // Posts results back to main thread to refresh UI.
mHandler.post(() -> { mHandler.post(() -> {
mBatteryIndexedMap = indexedUsageMap; 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 static java.lang.Math.round;
import android.accessibilityservice.AccessibilityServiceInfo; import android.accessibilityservice.AccessibilityServiceInfo;
import android.annotation.NonNull;
import android.content.Context; import android.content.Context;
import android.content.res.Resources; import android.content.res.Resources;
import android.graphics.Canvas; import android.graphics.Canvas;
@@ -38,6 +37,7 @@ import android.view.View;
import android.view.accessibility.AccessibilityManager; import android.view.accessibility.AccessibilityManager;
import android.widget.TextView; import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import androidx.appcompat.widget.AppCompatImageView; 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 int DIVIDER_COLOR = Color.parseColor("#CDCCC5");
private static final long UPDATE_STATE_DELAYED_TIME = 500L; 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. */ /** A callback listener for selected group index is updated. */
public interface OnSelectListener { public interface OnSelectListener {
/** The callback function for selected group index is updated. */ /** The callback function for selected group index is updated. */
void onSelect(int trapezoidIndex); void onSelect(int trapezoidIndex);
} }
private BatteryChartViewModel mViewModel;
private int mDividerWidth; private int mDividerWidth;
private int mDividerHeight; private int mDividerHeight;
private float mTrapezoidVOffset; private float mTrapezoidVOffset;
@@ -79,9 +77,7 @@ public class BatteryChartViewV2 extends AppCompatImageView implements View.OnCli
private String[] mPercentages = getPercentages(); private String[] mPercentages = getPercentages();
@VisibleForTesting @VisibleForTesting
int mHoveredIndex = SELECTED_INDEX_INVALID; int mHoveredIndex = BatteryChartViewModel.SELECTED_INDEX_INVALID;
@VisibleForTesting
int mSelectedIndex = SELECTED_INDEX_INVALID;
@VisibleForTesting @VisibleForTesting
String[] mAxisLabels; String[] mAxisLabels;
@@ -103,7 +99,6 @@ public class BatteryChartViewV2 extends AppCompatImageView implements View.OnCli
@VisibleForTesting @VisibleForTesting
final Runnable mUpdateClickableStateRun = () -> updateClickableState(); final Runnable mUpdateClickableStateRun = () -> updateClickableState();
private int[] mLevels;
private Paint mTextPaint; private Paint mTextPaint;
private Paint mDividerPaint; private Paint mDividerPaint;
private Paint mTrapezoidPaint; private Paint mTrapezoidPaint;
@@ -126,44 +121,26 @@ public class BatteryChartViewV2 extends AppCompatImageView implements View.OnCli
initializeColors(context); initializeColors(context);
// Registers the click event listener. // Registers the click event listener.
setOnClickListener(this); setOnClickListener(this);
setSelectedIndex(SELECTED_INDEX_ALL);
setClickable(false); setClickable(false);
requestLayout();
} }
/** Sets all levels value to draw the trapezoid shape */ /** Sets the data model of this view. */
public void setLevels(int[] levels) { public void setViewModel(BatteryChartViewModel viewModel) {
Log.d(TAG, "setLevels() " + (levels == null ? "null" : levels.length)); if (viewModel == null) {
// At least 2 levels to draw a trapezoid. mViewModel = null;
if (levels == null || levels.length < 2) {
mLevels = null;
invalidate(); invalidate();
return; return;
} }
mLevels = levels;
// Initialize trapezoid slots. Log.d(TAG, String.format("setViewModel(): size: %d, selectedIndex: %d.",
mTrapezoidSlots = new TrapezoidSlot[mLevels.length - 1]; viewModel.size(), viewModel.selectedIndex()));
for (int index = 0; index < mTrapezoidSlots.length; index++) { mViewModel = viewModel;
mTrapezoidSlots[index] = new TrapezoidSlot();
}
setClickable(false); initializeTrapezoidSlots(viewModel.size() - 1);
invalidate(); initializeAxisLabels(viewModel.texts());
// Sets the chart is clickable if there is at least one valid item in it. setClickable(hasNonZeroTrapezoid(viewModel.levels()));
for (int index = 0; index < mLevels.length - 1; index++) { requestLayout();
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();
}
} }
/** Sets the callback to monitor the selected group index. */ /** Sets the callback to monitor the selected group index. */
@@ -184,26 +161,6 @@ public class BatteryChartViewV2 extends AppCompatImageView implements View.OnCli
requestLayout(); 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 @Override
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, 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 // Before mLevels initialized, the count of trapezoids is unknown. Only draws the
// horizontal percentages and dividers. // horizontal percentages and dividers.
drawHorizontalDividers(canvas); drawHorizontalDividers(canvas);
if (mLevels == null) { if (mViewModel == null) {
return; return;
} }
drawVerticalDividers(canvas); drawVerticalDividers(canvas);
@@ -282,7 +239,7 @@ public class BatteryChartViewV2 extends AppCompatImageView implements View.OnCli
public void onHoverChanged(boolean hovered) { public void onHoverChanged(boolean hovered) {
super.onHoverChanged(hovered); super.onHoverChanged(hovered);
if (!hovered) { if (!hovered) {
mHoveredIndex = SELECTED_INDEX_INVALID; // reset mHoveredIndex = BatteryChartViewModel.SELECTED_INDEX_INVALID; // reset
invalidate(); invalidate();
} }
} }
@@ -295,13 +252,15 @@ public class BatteryChartViewV2 extends AppCompatImageView implements View.OnCli
} }
final int trapezoidIndex = getTrapezoidIndex(mTouchUpEventX); final int trapezoidIndex = getTrapezoidIndex(mTouchUpEventX);
// Ignores the click event if the level is zero. // Ignores the click event if the level is zero.
if (trapezoidIndex == SELECTED_INDEX_INVALID if (trapezoidIndex == BatteryChartViewModel.SELECTED_INDEX_INVALID
|| !isValidToDraw(trapezoidIndex)) { || !isValidToDraw(trapezoidIndex)) {
return; return;
} }
if (mOnSelectListener != null) { if (mOnSelectListener != null) {
// Selects all if users click the same trapezoid item two times.
mOnSelectListener.onSelect( mOnSelectListener.onSelect(
trapezoidIndex == mSelectedIndex ? SELECTED_INDEX_ALL : trapezoidIndex); trapezoidIndex == mViewModel.selectedIndex()
? BatteryChartViewModel.SELECTED_INDEX_ALL : trapezoidIndex);
} }
view.performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK); view.performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK);
} }
@@ -350,8 +309,8 @@ public class BatteryChartViewV2 extends AppCompatImageView implements View.OnCli
mTrapezoidCurvePaint.setStrokeWidth(mDividerWidth * 2); mTrapezoidCurvePaint.setStrokeWidth(mDividerWidth * 2);
} else if (mIsSlotsClickabled) { } else if (mIsSlotsClickabled) {
mTrapezoidCurvePaint = null; mTrapezoidCurvePaint = null;
// Sets levels again to force update the click state. // Sets view model again to force update the click state.
setLevels(mLevels); setViewModel(mViewModel);
} }
invalidate(); invalidate();
} }
@@ -366,6 +325,28 @@ public class BatteryChartViewV2 extends AppCompatImageView implements View.OnCli
super.setClickable(clickable); 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) { private void initializeColors(Context context) {
setBackgroundColor(Color.TRANSPARENT); setBackgroundColor(Color.TRANSPARENT);
mTrapezoidSolidColor = Utils.getColorAccentDefaultColor(context); mTrapezoidSolidColor = Utils.getColorAccentDefaultColor(context);
@@ -498,7 +479,7 @@ public class BatteryChartViewV2 extends AppCompatImageView implements View.OnCli
private void drawTrapezoids(Canvas canvas) { private void drawTrapezoids(Canvas canvas) {
// Ignores invalid trapezoid data. // Ignores invalid trapezoid data.
if (mLevels == null) { if (mViewModel == null) {
return; return;
} }
final float trapezoidBottom = final float trapezoidBottom =
@@ -519,17 +500,17 @@ public class BatteryChartViewV2 extends AppCompatImageView implements View.OnCli
continue; continue;
} }
// Configures the trapezoid paint color. // Configures the trapezoid paint color.
final int trapezoidColor = final int trapezoidColor = mIsSlotsClickabled && (mViewModel.selectedIndex() == index
!mIsSlotsClickabled || mViewModel.selectedIndex() == BatteryChartViewModel.SELECTED_INDEX_ALL)
? mTrapezoidColor ? mTrapezoidSolidColor : mTrapezoidColor;
: mSelectedIndex == index || mSelectedIndex == SELECTED_INDEX_ALL
? mTrapezoidSolidColor : mTrapezoidColor;
final boolean isHoverState = final boolean isHoverState =
mIsSlotsClickabled && mHoveredIndex == index && isValidToDraw(mHoveredIndex); mIsSlotsClickabled && mHoveredIndex == index && isValidToDraw(mHoveredIndex);
mTrapezoidPaint.setColor(isHoverState ? mTrapezoidHoverColor : trapezoidColor); mTrapezoidPaint.setColor(isHoverState ? mTrapezoidHoverColor : trapezoidColor);
final float leftTop = round(trapezoidBottom - mLevels[index] * unitHeight); final float leftTop = round(
final float rightTop = round(trapezoidBottom - mLevels[index + 1] * unitHeight); trapezoidBottom - mViewModel.levels().get(index) * unitHeight);
final float rightTop = round(
trapezoidBottom - mViewModel.levels().get(index + 1) * unitHeight);
trapezoidPath.reset(); trapezoidPath.reset();
trapezoidPath.moveTo(mTrapezoidSlots[index].mLeft, trapezoidBottom); trapezoidPath.moveTo(mTrapezoidSlots[index].mLeft, trapezoidBottom);
trapezoidPath.lineTo(mTrapezoidSlots[index].mLeft, leftTop); trapezoidPath.lineTo(mTrapezoidSlots[index].mLeft, leftTop);
@@ -568,15 +549,25 @@ public class BatteryChartViewV2 extends AppCompatImageView implements View.OnCli
return index; return index;
} }
} }
return SELECTED_INDEX_INVALID; return BatteryChartViewModel.SELECTED_INDEX_INVALID;
} }
private boolean isValidToDraw(int trapezoidIndex) { private boolean isValidToDraw(int trapezoidIndex) {
return mLevels != null return mViewModel != null
&& trapezoidIndex >= 0 && trapezoidIndex >= 0
&& trapezoidIndex < mLevels.length - 1 && trapezoidIndex < mViewModel.size() - 1
&& mLevels[trapezoidIndex] != 0 && mViewModel.levels().get(trapezoidIndex) != 0
&& mLevels[trapezoidIndex + 1] != 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() { private static String[] getPercentages() {

View File

@@ -55,6 +55,7 @@ import org.robolectric.RuntimeEnvironment;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.TimeZone; import java.util.TimeZone;
@@ -173,11 +174,11 @@ public final class BatteryChartPreferenceControllerV2Test {
for (int index = 0; index < DESIRED_HISTORY_SIZE; index++) { for (int index = 0; index < DESIRED_HISTORY_SIZE; index++) {
assertThat(mBatteryChartPreferenceController.mBatteryHistoryKeys[index]) assertThat(mBatteryChartPreferenceController.mBatteryHistoryKeys[index])
// These values is are calculated by hand from createBatteryHistoryMap(). // These values is are calculated by hand from createBatteryHistoryMap().
.isEqualTo(index + 1); .isEqualTo(generateTimestamp(index));
} }
// Verifies the created battery levels array. // Verifies the created battery levels array.
for (int index = 0; index < 13; index++) { for (int index = 0; index < 13; index++) {
assertThat(mBatteryChartPreferenceController.mBatteryHistoryLevels[index]) assertThat(mBatteryChartPreferenceController.mViewModel.levels().get(index))
// These values is are calculated by hand from createBatteryHistoryMap(). // These values is are calculated by hand from createBatteryHistoryMap().
.isEqualTo(100 - index * 2); .isEqualTo(100 - index * 2);
} }
@@ -193,79 +194,63 @@ public final class BatteryChartPreferenceControllerV2Test {
for (int index = 0; index < DESIRED_HISTORY_SIZE; index++) { for (int index = 0; index < DESIRED_HISTORY_SIZE; index++) {
assertThat(mBatteryChartPreferenceController.mBatteryHistoryKeys[index]) assertThat(mBatteryChartPreferenceController.mBatteryHistoryKeys[index])
// These values is are calculated by hand from createBatteryHistoryMap(). // These values is are calculated by hand from createBatteryHistoryMap().
.isEqualTo(index + 1); .isEqualTo(generateTimestamp(index));
} }
// Verifies the created battery levels array. // Verifies the created battery levels array.
for (int index = 0; index < 13; index++) { for (int index = 0; index < 13; index++) {
assertThat(mBatteryChartPreferenceController.mBatteryHistoryLevels[index]) assertThat(mBatteryChartPreferenceController.mViewModel.levels().get(index))
// These values is are calculated by hand from createBatteryHistoryMap(). // These values is are calculated by hand from createBatteryHistoryMap().
.isEqualTo(100 - index * 2); .isEqualTo(100 - index * 2);
} }
assertThat(mBatteryChartPreferenceController.mBatteryIndexedMap).hasSize(13); assertThat(mBatteryChartPreferenceController.mBatteryIndexedMap).hasSize(13);
} }
@Test
public void testSetBatteryChartViewModel() {
verify(mBatteryChartPreferenceController.mBatteryChartView)
.setViewModel(new BatteryChartViewModel(
List.of(100, 98, 96, 94, 92, 90, 88, 86, 84, 82, 80, 78, 76),
List.of("7 am", "9 am", "11 am", "1 pm", "3 pm", "5 pm", "7 pm",
"9 pm", "11 pm", "1 am", "3 am", "5 am", "7 am"),
BatteryChartViewModel.SELECTED_INDEX_ALL));
}
@Test
public void testRefreshUi_refresh() {
assertThat(mBatteryChartPreferenceController.refreshUi()).isTrue();
}
@Test @Test
public void testRefreshUi_batteryIndexedMapIsNull_ignoreRefresh() { public void testRefreshUi_batteryIndexedMapIsNull_ignoreRefresh() {
mBatteryChartPreferenceController.setBatteryHistoryMap(null); mBatteryChartPreferenceController.setBatteryHistoryMap(null);
assertThat(mBatteryChartPreferenceController.refreshUi( assertThat(mBatteryChartPreferenceController.refreshUi()).isFalse();
/*trapezoidIndex=*/ 1, /*isForce=*/ false)).isFalse();
} }
@Test @Test
public void testRefreshUi_batteryChartViewIsNull_ignoreRefresh() { public void testRefreshUi_batteryChartViewIsNull_ignoreRefresh() {
mBatteryChartPreferenceController.mBatteryChartView = null; mBatteryChartPreferenceController.mBatteryChartView = null;
assertThat(mBatteryChartPreferenceController.refreshUi( assertThat(mBatteryChartPreferenceController.refreshUi()).isFalse();
/*trapezoidIndex=*/ 1, /*isForce=*/ false)).isFalse();
}
@Test
public void testRefreshUi_trapezoidIndexIsNotChanged_ignoreRefresh() {
final int trapezoidIndex = 1;
mBatteryChartPreferenceController.mTrapezoidIndex = trapezoidIndex;
assertThat(mBatteryChartPreferenceController.refreshUi(
trapezoidIndex, /*isForce=*/ false)).isFalse();
}
@Test
public void testRefreshUi_forceUpdate_refreshUi() {
final int trapezoidIndex = 1;
mBatteryChartPreferenceController.mTrapezoidIndex = trapezoidIndex;
assertThat(mBatteryChartPreferenceController.refreshUi(
trapezoidIndex, /*isForce=*/ true)).isTrue();
}
@Test
public void testForceRefreshUi_updateTrapezoidIndexIntoSelectAll() {
mBatteryChartPreferenceController.mTrapezoidIndex =
BatteryChartViewV2.SELECTED_INDEX_INVALID;
mBatteryChartPreferenceController.setBatteryHistoryMap(
createBatteryHistoryMap());
assertThat(mBatteryChartPreferenceController.mTrapezoidIndex)
.isEqualTo(BatteryChartViewV2.SELECTED_INDEX_ALL);
} }
@Test @Test
public void testRemoveAndCacheAllPrefs_emptyContent_ignoreRemoveAll() { public void testRemoveAndCacheAllPrefs_emptyContent_ignoreRemoveAll() {
final int trapezoidIndex = 1; mBatteryChartPreferenceController.mTrapezoidIndex = 1;
doReturn(0).when(mAppListGroup).getPreferenceCount(); doReturn(0).when(mAppListGroup).getPreferenceCount();
mBatteryChartPreferenceController.refreshUi( mBatteryChartPreferenceController.refreshUi();
trapezoidIndex, /*isForce=*/ true);
verify(mAppListGroup, never()).removeAll(); verify(mAppListGroup, never()).removeAll();
} }
@Test @Test
public void testRemoveAndCacheAllPrefs_buildCacheAndRemoveAllPreference() { public void testRemoveAndCacheAllPrefs_buildCacheAndRemoveAllPreference() {
final int trapezoidIndex = 1; mBatteryChartPreferenceController.mTrapezoidIndex = 1;
doReturn(1).when(mAppListGroup).getPreferenceCount(); doReturn(1).when(mAppListGroup).getPreferenceCount();
doReturn(mPowerGaugePreference).when(mAppListGroup).getPreference(0); doReturn(mPowerGaugePreference).when(mAppListGroup).getPreference(0);
doReturn(PREF_KEY).when(mPowerGaugePreference).getKey(); doReturn(PREF_KEY).when(mPowerGaugePreference).getKey();
// Ensures the testing data is correct. // Ensures the testing data is correct.
assertThat(mBatteryChartPreferenceController.mPreferenceCache).isEmpty(); assertThat(mBatteryChartPreferenceController.mPreferenceCache).isEmpty();
mBatteryChartPreferenceController.refreshUi( mBatteryChartPreferenceController.refreshUi();
trapezoidIndex, /*isForce=*/ true);
assertThat(mBatteryChartPreferenceController.mPreferenceCache.get(PREF_KEY)) assertThat(mBatteryChartPreferenceController.mPreferenceCache.get(PREF_KEY))
.isEqualTo(mPowerGaugePreference); .isEqualTo(mPowerGaugePreference);
@@ -522,7 +507,7 @@ public final class BatteryChartPreferenceControllerV2Test {
@Test @Test
public void testOnSelect_selectAll_logMetric() { public void testOnSelect_selectAll_logMetric() {
mBatteryChartPreferenceController.onSelect( mBatteryChartPreferenceController.onSelect(
BatteryChartViewV2.SELECTED_INDEX_ALL /*slot index*/); BatteryChartViewModel.SELECTED_INDEX_ALL /*slot index*/);
verify(mMetricsFeatureProvider) verify(mMetricsFeatureProvider)
.action(mContext, SettingsEnums.ACTION_BATTERY_USAGE_SHOW_ALL); .action(mContext, SettingsEnums.ACTION_BATTERY_USAGE_SHOW_ALL);
@@ -562,7 +547,7 @@ public final class BatteryChartPreferenceControllerV2Test {
spy(new ExpandDividerPreference(mContext)); spy(new ExpandDividerPreference(mContext));
// Simulates select all condition. // Simulates select all condition.
mBatteryChartPreferenceController.mTrapezoidIndex = mBatteryChartPreferenceController.mTrapezoidIndex =
BatteryChartViewV2.SELECTED_INDEX_ALL; BatteryChartViewModel.SELECTED_INDEX_ALL;
mBatteryChartPreferenceController.refreshCategoryTitle(); mBatteryChartPreferenceController.refreshCategoryTitle();
@@ -580,44 +565,6 @@ public final class BatteryChartPreferenceControllerV2Test {
.isEqualTo("System usage for past 24 hr"); .isEqualTo("System usage for past 24 hr");
} }
@Test
public void testSetTimestampLabel_nullBatteryHistoryKeys_ignore() {
mBatteryChartPreferenceController = createController();
mBatteryChartPreferenceController.mBatteryHistoryKeys = null;
mBatteryChartPreferenceController.mBatteryChartView =
spy(new BatteryChartViewV2(mContext));
mBatteryChartPreferenceController.setTimestampLabel();
verify(mBatteryChartPreferenceController.mBatteryChartView, never())
.setAxisLabels(any());
}
@Test
public void testSetTimestampLabel_setExpectedTimestampData() {
mBatteryChartPreferenceController = createController();
mBatteryChartPreferenceController.mBatteryChartView =
spy(new BatteryChartViewV2(mContext));
setUpBatteryHistoryKeys();
mBatteryChartPreferenceController.setTimestampLabel();
verify(mBatteryChartPreferenceController.mBatteryChartView)
.setAxisLabels(new String[] {"4 pm", "12 am", "7 am"});
}
@Test
public void testSetTimestampLabel_withoutValidTimestamp_setExpectedTimestampData() {
mBatteryChartPreferenceController = createController();
mBatteryChartPreferenceController.mBatteryChartView =
spy(new BatteryChartViewV2(mContext));
mBatteryChartPreferenceController.mBatteryHistoryKeys = new long[]{0L};
mBatteryChartPreferenceController.setTimestampLabel();
verify(mBatteryChartPreferenceController.mBatteryChartView)
.setAxisLabels(new String[] {"12 am"});
}
@Test @Test
public void testOnSaveInstanceState_restoreSelectedIndexAndExpandState() { public void testOnSaveInstanceState_restoreSelectedIndexAndExpandState() {
final int expectedIndex = 1; final int expectedIndex = 1;
@@ -663,6 +610,11 @@ public final class BatteryChartPreferenceControllerV2Test {
.isFalse(); .isFalse();
} }
private static Long generateTimestamp(int index) {
// "2021-04-23 07:00:00 UTC" + index hours
return 1619247600000L + index * DateUtils.HOUR_IN_MILLIS;
}
private static Map<Long, Map<String, BatteryHistEntry>> createBatteryHistoryMap() { private static Map<Long, Map<String, BatteryHistEntry>> createBatteryHistoryMap() {
final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap = new HashMap<>(); final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap = new HashMap<>();
for (int index = 0; index < DESIRED_HISTORY_SIZE; index++) { for (int index = 0; index < DESIRED_HISTORY_SIZE; index++) {
@@ -671,7 +623,7 @@ public final class BatteryChartPreferenceControllerV2Test {
final BatteryHistEntry entry = new BatteryHistEntry(values); final BatteryHistEntry entry = new BatteryHistEntry(values);
final Map<String, BatteryHistEntry> entryMap = new HashMap<>(); final Map<String, BatteryHistEntry> entryMap = new HashMap<>();
entryMap.put("fake_entry_key" + index, entry); entryMap.put("fake_entry_key" + index, entry);
batteryHistoryMap.put(Long.valueOf(index + 1), entryMap); batteryHistoryMap.put(generateTimestamp(index), entryMap);
} }
return batteryHistoryMap; return batteryHistoryMap;
} }

View File

@@ -42,6 +42,7 @@ import org.robolectric.RuntimeEnvironment;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List;
import java.util.Locale; import java.util.Locale;
@RunWith(RobolectricTestRunner.class) @RunWith(RobolectricTestRunner.class)
@@ -100,13 +101,15 @@ public final class BatteryChartViewV2Test {
@Test @Test
public void onClick_invokesCallback() { public void onClick_invokesCallback() {
mBatteryChartView.setLevels(new int[] {90, 80, 70, 60}); final int originalSelectedIndex = 2;
mBatteryChartView.setViewModel(
new BatteryChartViewModel(List.of(90, 80, 70, 60), List.of("", "", "", ""),
originalSelectedIndex));
for (int i = 0; i < mBatteryChartView.mTrapezoidSlots.length; i++) { for (int i = 0; i < mBatteryChartView.mTrapezoidSlots.length; i++) {
mBatteryChartView.mTrapezoidSlots[i] = new BatteryChartViewV2.TrapezoidSlot(); mBatteryChartView.mTrapezoidSlots[i] = new BatteryChartViewV2.TrapezoidSlot();
mBatteryChartView.mTrapezoidSlots[i].mLeft = i; mBatteryChartView.mTrapezoidSlots[i].mLeft = i;
mBatteryChartView.mTrapezoidSlots[i].mRight = i + 0.5f; mBatteryChartView.mTrapezoidSlots[i].mRight = i + 0.5f;
} }
mBatteryChartView.mSelectedIndex = 2;
final int[] selectedIndex = new int[1]; final int[] selectedIndex = new int[1];
mBatteryChartView.setOnSelectListener( mBatteryChartView.setOnSelectListener(
trapezoidIndex -> { trapezoidIndex -> {
@@ -123,7 +126,7 @@ public final class BatteryChartViewV2Test {
mBatteryChartView.mTouchUpEventX = 2; mBatteryChartView.mTouchUpEventX = 2;
selectedIndex[0] = Integer.MIN_VALUE; selectedIndex[0] = Integer.MIN_VALUE;
mBatteryChartView.onClick(mMockView); mBatteryChartView.onClick(mMockView);
assertThat(selectedIndex[0]).isEqualTo(BatteryChartViewV2.SELECTED_INDEX_ALL); assertThat(selectedIndex[0]).isEqualTo(BatteryChartViewModel.SELECTED_INDEX_ALL);
} }
@Test @Test
@@ -178,11 +181,14 @@ public final class BatteryChartViewV2Test {
@Test @Test
public void clickable_restoreFromNonClickableState() { public void clickable_restoreFromNonClickableState() {
final int[] levels = new int[13]; final List<Integer> levels = new ArrayList<Integer>();
for (int index = 0; index < levels.length; index++) { final List<String> texts = new ArrayList<String>();
levels[index] = index + 1; for (int index = 0; index < 13; index++) {
levels.add(index + 1);
texts.add("");
} }
mBatteryChartView.setLevels(levels); mBatteryChartView.setViewModel(new BatteryChartViewModel(
levels, texts, BatteryChartViewModel.SELECTED_INDEX_ALL));
mBatteryChartView.setClickableForce(true); mBatteryChartView.setClickableForce(true);
when(mPowerUsageFeatureProvider.isChartGraphSlotsEnabled(mContext)) when(mPowerUsageFeatureProvider.isChartGraphSlotsEnabled(mContext))
.thenReturn(true); .thenReturn(true);