Port new version battery usage chart implementation from master to tm-qpr-dev.

This cl is a merge of the following 36 cls:
ag/19250259	Duplicate BatteryChartPreferenceController and BatteryChartView into new files for better diff review purpose
ag/19279660	Use Mockito 4.6.1 API for BatteryChartPreferenceControllerV2Test
ag/19267975	Add class BatteryLevelData used to parcel the battery timestamps and levels. It behaves as an interface between UI and data.
ag/19289086	Refactor BatteryChartView X-axis labels. Instead of only timestamps, also support any string[] labels.
ag/19238586	Add interpolation for the history data since last full charge loaded from database.
ag/19331746	Return raw history map in function getHistoryMapSinceLastFullCharge.
ag/19308838	In BatteryChartViewV2, use levels.length-1 to replace mTrapezoidCount. So the chartview could show any number of slots as the given levels length-1.
ag/19332266	Add class BatteryDiffData used to parcel battery usage data
ag/19331467	Refactor Battery Chart View State Controll
ag/19358207	Add DataProcessor to process raw history map for UI.
ag/19332276	Add battery chart view model.
ag/19394744	Update trapezoid validation in battery chart view.
ag/19379730	Support daily and hourly battery chartview.
ag/19428426	Improve X axis labels in battery chart (1)
ag/19446215	Improve X axis labels in battery chart (2)
ag/19394745	Add the async task to compute diff usage data and load labels and icons.
ag/19447624	Support showing app usage list for two battery charts
ag/19500907	Updates battery usage messages from last 24hr to last full charge. (Part1: V2 files)
ag/19505324	Update the selected period message in battery chart
ag/19500905	Updates battery usage messages from last 24hr to last full charge. (Part2: non-V2 files)
ag/19510363	Update usage data for EBS app usage list and App usage detail from 24 hours to last full charge.
ag/19523184	Update usage data for EBS app usage list and App usage detail from 24 hours to last full charge.
ag/19534864	Add margin between battery daily and hourly charts
ag/19491093	Always do interpolation for battery level data in daily chart.
ag/19565630	Avoid NullPointerException when batteryLevelData is null.
ag/19561239	Fix b/241872474 Battery usage page will crash when selecting the last hour chart bar, going to app detail page, and going back
ag/19565633	Fix b/241885070: Unexpected texts moving when going back to battery usage page
ag/19534850	New way to draw battery chart axis labels
ag/19561240	Switch Battery Usage Chart from V1 to V2.
ag/19561338	Switch Battery Usage Chart from V1 to V2.
ag/19600174	Fix b/242254055 Battery usage initial screen improvements (long data loading time)
ag/19600284	Fix b/242252080: Add padding space on the top of the battery chart
ag/19647338	Consider usage map valid even if [all][all] is null.
ag/19634227	Use new content uri everytime to avoid cache
ag/19600177	Fix b/242009481: Refine the battery usage chart timestamp label rule
ag/19647337	Fix b/242809981 Charge battery to 100% when battery usage page opened, the chart will refresh, but the app list isn't refreshed in that case.

Test: manual
Bug: 239491373
Bug: 236101166
Bug: 236101687
Fix: 236101166
Change-Id: I7de8d9dcee14627da10752534991f1ec9f616020
Merged-In: I9142c0d4e00dea3771777ba9aedeab07b635fa1a
This commit is contained in:
Kuan Wang
2022-07-12 15:34:21 +08:00
committed by Zaiyue Xue
parent 1215a5dff3
commit 4db5c6ba57
22 changed files with 3095 additions and 800 deletions

View File

@@ -539,16 +539,13 @@ public class AdvancedPowerUsageDetail extends DashboardFragment implements
return null;
}
if (totalTimeMs == 0) {
final int batteryWithoutUsageTime = consumedPower > 0
? R.string.battery_usage_without_time : R.string.battery_not_usage_24hr;
usageTimeSummary = getText(isChartGraphEnabled
? batteryWithoutUsageTime : R.string.battery_not_usage);
usageTimeSummary = getText(
isChartGraphEnabled && consumedPower > 0 ? R.string.battery_usage_without_time
: R.string.battery_not_usage);
} else if (slotTime == null) {
// Shows summary text with past 24 hr or full charge if slot time is null.
usageTimeSummary = isChartGraphEnabled
? getAppPast24HrActiveSummary(foregroundTimeMs, backgroundTimeMs, totalTimeMs)
: getAppFullChargeActiveSummary(
foregroundTimeMs, backgroundTimeMs, totalTimeMs);
// Shows summary text with last full charge if slot time is null.
usageTimeSummary = getAppFullChargeActiveSummary(
foregroundTimeMs, backgroundTimeMs, totalTimeMs);
} else {
// Shows summary text with slot time.
usageTimeSummary = getAppActiveSummaryWithSlotTime(

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,7 +27,9 @@ 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;
import androidx.preference.Preference;
import androidx.preference.PreferenceGroup;
@@ -53,8 +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;
import java.util.Map;
@@ -62,27 +61,23 @@ 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 {
OnSaveInstanceState, OnResume, ExpandDividerPreference.OnExpandListener {
private static final String TAG = "BatteryChartPreferenceController";
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 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;
@VisibleForTesting
Map<Integer, List<BatteryDiffEntry>> mBatteryIndexedMap;
Map<Integer, Map<Integer, BatteryDiffData>> mBatteryUsageMap;
@VisibleForTesting
Context mPrefContext;
@@ -91,28 +86,34 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
@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;
@VisibleForTesting
BatteryChartView mDailyChartView;
@VisibleForTesting
BatteryChartView mHourlyChartView;
@VisibleForTesting
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;
// Daily view model only saves abbreviated day of week texts (e.g. MON). This field saves the
// full day of week texts (e.g. Monday), which is used in category title and battery detail
// page.
private List<String> mDailyTimestampFullTexts;
private BatteryChartViewModel mDailyViewModel;
private List<BatteryChartViewModel> mHourlyViewModels;
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());
@@ -120,8 +121,6 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
// 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,
@@ -134,10 +133,6 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
mIs24HourFormat = DateFormat.is24HourFormat(context);
mMetricsFeatureProvider =
FeatureFactory.getFactory(mContext).getMetricsFeatureProvider();
mNotAllowShowEntryPackages =
FeatureFactory.getFactory(context)
.getPowerUsageFeatureProvider(context)
.getHideApplicationEntries(context);
mNotAllowShowSummaryPackages =
FeatureFactory.getFactory(context)
.getPowerUsageFeatureProvider(context)
@@ -152,12 +147,14 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
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 +176,11 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
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
@@ -204,8 +202,7 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
mPrefContext = screen.getContext();
mAppListPrefGroup = screen.findPreference(mPreferenceKey);
mAppListPrefGroup.setOrderingAsAdded(false);
mAppListPrefGroup.setTitle(
mPrefContext.getString(R.string.battery_app_usage_for_past_24));
mAppListPrefGroup.setTitle(mPrefContext.getString(R.string.battery_app_usage));
mFooterPreference = screen.findPreference(KEY_FOOTER_PREF);
// Removes footer first until usage data is loaded to avoid flashing.
if (mFooterPreference != null) {
@@ -249,17 +246,6 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
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;
@@ -272,81 +258,120 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
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);
Log.d(TAG, "setBatteryHistoryMap() " + (batteryHistoryMap == null ? "null"
: ("size=" + batteryHistoryMap.size())));
final BatteryLevelData batteryLevelData =
DataProcessor.getBatteryLevelData(mContext, mHandler, batteryHistoryMap,
batteryUsageMap -> {
mBatteryUsageMap = batteryUsageMap;
refreshUi();
});
Log.d(TAG, "getBatteryLevelData: " + batteryLevelData);
if (batteryLevelData == null) {
mDailyTimestampFullTexts = null;
mDailyViewModel = null;
mHourlyViewModels = null;
refreshUi();
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;
mDailyTimestampFullTexts = generateTimestampDayOfWeekTexts(
mContext, batteryLevelData.getDailyBatteryLevels().getTimestamps(),
/* isAbbreviation= */ false);
mDailyViewModel = new BatteryChartViewModel(
batteryLevelData.getDailyBatteryLevels().getLevels(),
generateTimestampDayOfWeekTexts(
mContext, batteryLevelData.getDailyBatteryLevels().getTimestamps(),
/* isAbbreviation= */ true),
BatteryChartViewModel.AxisLabelPosition.CENTER_OF_TRAPEZOIDS);
mHourlyViewModels = new ArrayList<>();
for (BatteryLevelData.PeriodBatteryLevelData hourlyBatteryLevelsPerDay :
batteryLevelData.getHourlyBatteryLevelsPerDay()) {
mHourlyViewModels.add(new BatteryChartViewModel(
hourlyBatteryLevelsPerDay.getLevels(),
generateTimestampHourTexts(
mContext, hourlyBatteryLevelsPerDay.getTimestamps()),
BatteryChartViewModel.AxisLabelPosition.BETWEEN_TRAPEZOIDS));
}
refreshUi();
}
void setBatteryChartView(@NonNull final BatteryChartView dailyChartView,
@NonNull final BatteryChartView hourlyChartView) {
if (mDailyChartView != dailyChartView || mHourlyChartView != hourlyChartView) {
mHandler.post(() -> setBatteryChartViewInner(dailyChartView, hourlyChartView));
}
}
private void setBatteryChartViewInner(@NonNull final BatteryChartView dailyChartView,
@NonNull final BatteryChartView hourlyChartView) {
mDailyChartView = dailyChartView;
mDailyChartView.setOnSelectListener(trapezoidIndex -> {
if (mDailyChartIndex == trapezoidIndex) {
return;
}
// Averages the battery level in each time slot to avoid corner conditions.
float batteryLevelCounter = 0;
for (BatteryHistEntry entry : entryMap.values()) {
batteryLevelCounter += entry.mBatteryLevel;
Log.d(TAG, "onDailyChartSelect:" + trapezoidIndex);
mDailyChartIndex = trapezoidIndex;
mHourlyChartIndex = BatteryChartViewModel.SELECTED_INDEX_ALL;
refreshUi();
// TODO: Change to log daily data.
});
mHourlyChartView = hourlyChartView;
mHourlyChartView.setOnSelectListener(trapezoidIndex -> {
if (mHourlyChartIndex == trapezoidIndex) {
return;
}
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);
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(int trapezoidIndex, boolean isForce) {
// Invalid refresh condition.
if (mBatteryIndexedMap == null
|| mBatteryChartView == null
|| (mTrapezoidIndex == trapezoidIndex && !isForce)) {
boolean refreshUi() {
if (mDailyChartView == null || mHourlyChartView == null) {
// Chart views are not initialized.
return false;
}
if (mDailyViewModel == null || mHourlyViewModels == null) {
// Fail to get battery level data, show an empty hourly chart view.
mDailyChartView.setVisibility(View.GONE);
mHourlyChartView.setVisibility(View.VISIBLE);
mHourlyChartView.setViewModel(null);
removeAndCacheAllPrefs();
addFooterPreferenceIfNeeded(false);
return false;
}
if (mBatteryUsageMap == null) {
// Battery usage data is not ready, wait for data ready to refresh UI.
return false;
}
Log.d(TAG, String.format("refreshUi: index=%d size=%d isForce:%b",
trapezoidIndex, mBatteryIndexedMap.size(), isForce));
mTrapezoidIndex = trapezoidIndex;
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);
}
mHandler.post(() -> {
final long start = System.currentTimeMillis();
removeAndCacheAllPrefs();
@@ -359,43 +384,22 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
}
private void addAllPreferences() {
final List<BatteryDiffEntry> entries =
mBatteryIndexedMap.get(Integer.valueOf(mTrapezoidIndex));
addFooterPreferenceIfNeeded(entries != null && !entries.isEmpty());
if (entries == null) {
Log.w(TAG, "cannot find BatteryDiffEntry for:" + mTrapezoidIndex);
final BatteryDiffData batteryDiffData =
mBatteryUsageMap.get(mDailyChartIndex).get(mHourlyChartIndex);
addFooterPreferenceIfNeeded(batteryDiffData != null
&& (!batteryDiffData.getAppDiffEntryList().isEmpty()
|| !batteryDiffData.getSystemDiffEntryList().isEmpty()));
if (batteryDiffData == null) {
Log.w(TAG, "cannot find BatteryDiffEntry for daily_index: " + mDailyChartIndex
+ " hourly_index: " + mHourlyChartIndex);
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);
if (!batteryDiffData.getAppDiffEntryList().isEmpty()) {
addPreferenceToScreen(batteryDiffData.getAppDiffEntryList());
}
// Adds the expabable divider if we have system entries data.
if (!mSystemEntries.isEmpty()) {
if (!batteryDiffData.getSystemDiffEntryList().isEmpty()) {
if (mExpandDividerPreference == null) {
mExpandDividerPreference = new ExpandDividerPreference(mPrefContext);
mExpandDividerPreference.setOnExpandListener(this);
@@ -469,11 +473,13 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
}
private void refreshExpandUi() {
final List<BatteryDiffEntry> systemEntries = mBatteryUsageMap.get(mDailyChartIndex).get(
mHourlyChartIndex).getSystemDiffEntryList();
if (mIsExpanded) {
addPreferenceToScreen(mSystemEntries);
addPreferenceToScreen(systemEntries);
} else {
// Removes and recycles all system entries to hide all of them.
for (BatteryDiffEntry entry : mSystemEntries) {
for (BatteryDiffEntry entry : systemEntries) {
final String prefKey = entry.mBatteryHistEntry.getKey();
final Preference pref = mAppListPrefGroup.findPreference(prefKey);
if (pref != null) {
@@ -499,11 +505,12 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
}
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
? mPrefContext.getString(R.string.battery_app_usage_for_past_24)
: mPrefContext.getString(R.string.battery_system_usage_for_past_24);
? mPrefContext.getString(R.string.battery_app_usage)
: mPrefContext.getString(R.string.battery_system_usage);
} else {
return isApp
? mPrefContext.getString(R.string.battery_app_usage_for, slotInformation)
@@ -511,17 +518,33 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
}
}
private String getSlotInformation() {
if (mTrapezoidIndex < 0) {
@VisibleForTesting
String getSlotInformation() {
if (mDailyTimestampFullTexts == null || mDailyViewModel == null
|| mHourlyViewModels == null) {
// No data
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);
if (isAllSelected()) {
return null;
}
final String selectedDayText = mDailyTimestampFullTexts.get(mDailyChartIndex);
if (mHourlyChartIndex == BatteryChartViewModel.SELECTED_INDEX_ALL) {
return selectedDayText;
}
final String fromHourText = mHourlyViewModels.get(mDailyChartIndex).texts().get(
mHourlyChartIndex);
final String toHourText = mHourlyViewModels.get(mDailyChartIndex).texts().get(
mHourlyChartIndex + 1);
final String selectedHourText =
String.format("%s%s%s", fromHourText, mIs24HourFormat ? "-" : " - ", toHourText);
if (isBatteryLevelDataInOneDay()) {
return selectedHourText;
}
return String.format("%s %s", selectedDayText, selectedHourText);
}
@VisibleForTesting
@@ -575,22 +598,7 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
@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);
return !DataProcessor.contains(packageName, mNotAllowShowSummaryPackages);
}
private void addFooterPreferenceIfNeeded(boolean containAppItems) {
@@ -605,60 +613,65 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
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;
private boolean isBatteryLevelDataInOneDay() {
return mHourlyViewModels != null && mHourlyViewModels.size() == 1;
}
@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;
private boolean isAllSelected() {
return (isBatteryLevelDataInOneDay()
|| mDailyChartIndex == BatteryChartViewModel.SELECTED_INDEX_ALL)
&& mHourlyChartIndex == BatteryChartViewModel.SELECTED_INDEX_ALL;
}
private static List<String> generateTimestampDayOfWeekTexts(@NonNull final Context context,
@NonNull final List<Long> timestamps, final boolean isAbbreviation) {
final ArrayList<String> texts = new ArrayList<>();
for (Long timestamp : timestamps) {
texts.add(ConvertUtils.utcToLocalTimeDayOfWeek(context, timestamp, isAbbreviation));
}
return true;
return texts;
}
private static List<String> generateTimestampHourTexts(
@NonNull final Context context, @NonNull final List<Long> timestamps) {
final boolean is24HourFormat = DateFormat.is24HourFormat(context);
final ArrayList<String> texts = new ArrayList<>();
for (Long timestamp : timestamps) {
texts.add(ConvertUtils.utcToLocalTimeHour(context, timestamp, is24HourFormat));
}
return texts;
}
/** Used for {@link AppBatteryPreferenceController}. */
public static List<BatteryDiffEntry> getBatteryLast24HrUsageData(Context context) {
public static List<BatteryDiffEntry> getAppBatteryUsageData(Context context) {
final long start = System.currentTimeMillis();
final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap =
FeatureFactory.getFactory(context)
.getPowerUsageFeatureProvider(context)
.getBatteryHistory(context);
.getBatteryHistorySinceLastFullCharge(context);
if (batteryHistoryMap == null || batteryHistoryMap.isEmpty()) {
return null;
}
Log.d(TAG, String.format("getBatteryLast24HrData() size=%d time=&d/ms",
Log.d(TAG, String.format("getBatterySinceLastFullChargeUsageData() 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);
final Map<Integer, Map<Integer, BatteryDiffData>> batteryUsageData =
DataProcessor.getBatteryUsageData(context, batteryHistoryMap);
return batteryUsageData == null
? null
: batteryUsageData
.get(BatteryChartViewModel.SELECTED_INDEX_ALL)
.get(BatteryChartViewModel.SELECTED_INDEX_ALL)
.getAppDiffEntryList();
}
/** Used for {@link AppBatteryPreferenceController}. */
public static BatteryDiffEntry getBatteryLast24HrUsageData(
public static BatteryDiffEntry getAppBatteryUsageData(
Context context, String packageName, int userId) {
if (packageName == null) {
return null;
}
final List<BatteryDiffEntry> entries = getBatteryLast24HrUsageData(context);
final List<BatteryDiffEntry> entries = getAppBatteryUsageData(context);
if (entries == null) {
return null;
}
@@ -673,65 +686,4 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
}
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;
forceRefreshUi();
});
}
}
}

View File

@@ -18,6 +18,7 @@ package com.android.settings.fuelgauge.batteryusage;
import static com.android.settings.Utils.formatPercentage;
import static java.lang.Math.round;
import static java.util.Objects.requireNonNull;
import android.accessibilityservice.AccessibilityServiceInfo;
import android.content.Context;
@@ -29,8 +30,6 @@ import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
import android.os.Handler;
import android.text.format.DateFormat;
import android.text.format.DateUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.view.HapticFeedbackConstants;
@@ -39,6 +38,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;
@@ -46,7 +46,7 @@ import com.android.settings.R;
import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.Utils;
import java.time.Clock;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
@@ -58,36 +58,26 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
private static final List<String> ACCESSIBILITY_SERVICE_NAMES =
Arrays.asList("SwitchAccessService", "TalkBackService", "JustSpeakService");
private static final int DEFAULT_TRAPEZOID_COUNT = 12;
private static final int DEFAULT_TIMESTAMP_COUNT = 4;
private static final int TIMESTAMP_GAPS_COUNT = DEFAULT_TIMESTAMP_COUNT - 1;
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 int mTrapezoidCount;
private float mTrapezoidVOffset;
private float mTrapezoidHOffset;
private boolean mIsSlotsClickabled;
private String[] mPercentages = getPercentages();
@VisibleForTesting
int mHoveredIndex = SELECTED_INDEX_INVALID;
@VisibleForTesting
int mSelectedIndex = SELECTED_INDEX_INVALID;
@VisibleForTesting
String[] mTimestamps;
int mHoveredIndex = BatteryChartViewModel.SELECTED_INDEX_INVALID;
// Colors for drawing the trapezoid shape and dividers.
private int mTrapezoidColor;
@@ -98,25 +88,26 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
private final Rect mIndent = new Rect();
private final Rect[] mPercentageBounds =
new Rect[]{new Rect(), new Rect(), new Rect()};
// For drawing the timestamp information.
private final Rect[] mTimestampsBounds =
new Rect[]{new Rect(), new Rect(), new Rect(), new Rect()};
// For drawing the axis label information.
private final List<Rect> mAxisLabelsBounds = new ArrayList<>();
@VisibleForTesting
Handler mHandler = new Handler();
@VisibleForTesting
final Runnable mUpdateClickableStateRun = () -> updateClickableState();
private int[] mLevels;
private Paint mTextPaint;
private Paint mDividerPaint;
private Paint mTrapezoidPaint;
@VisibleForTesting
Paint mTrapezoidCurvePaint = null;
private TrapezoidSlot[] mTrapezoidSlots;
@VisibleForTesting
TrapezoidSlot[] mTrapezoidSlots;
// Records the location to calculate selected index.
private float mTouchUpEventX = Float.MIN_VALUE;
@VisibleForTesting
float mTouchUpEventX = Float.MIN_VALUE;
private BatteryChartView.OnSelectListener mOnSelectListener;
public BatteryChartView(Context context) {
@@ -128,57 +119,25 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
initializeColors(context);
// Registers the click event listener.
setOnClickListener(this);
setSelectedIndex(SELECTED_INDEX_ALL);
setTrapezoidCount(DEFAULT_TRAPEZOID_COUNT);
setClickable(false);
setLatestTimestamp(0);
requestLayout();
}
/** Sets the total trapezoid count for drawing. */
public void setTrapezoidCount(int trapezoidCount) {
Log.i(TAG, "trapezoidCount:" + trapezoidCount);
mTrapezoidCount = trapezoidCount;
mTrapezoidSlots = new TrapezoidSlot[trapezoidCount];
// Allocates the trapezoid slot array.
for (int index = 0; index < trapezoidCount; index++) {
mTrapezoidSlots[index] = new TrapezoidSlot();
}
invalidate();
}
/** Sets all levels value to draw the trapezoid shape */
public void setLevels(int[] levels) {
Log.d(TAG, "setLevels() " + (levels == null ? "null" : levels.length));
if (levels == null) {
mLevels = null;
return;
}
// We should provide trapezoid count + 1 data to draw all trapezoids.
mLevels = levels.length == mTrapezoidCount + 1 ? levels : null;
setClickable(false);
invalidate();
if (mLevels == null) {
return;
}
// 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;
/** Sets the data model of this view. */
public void setViewModel(BatteryChartViewModel viewModel) {
if (viewModel == null) {
mViewModel = null;
invalidate();
// Callbacks to the listener if we have.
if (mOnSelectListener != null) {
mOnSelectListener.onSelect(mSelectedIndex);
}
return;
}
Log.d(TAG, String.format("setViewModel(): size: %d, selectedIndex: %d.",
viewModel.size(), viewModel.selectedIndex()));
mViewModel = viewModel;
initializeAxisLabelsBounds();
initializeTrapezoidSlots(viewModel.size() - 1);
setClickable(hasAnyValidTrapezoid(viewModel));
requestLayout();
}
/** Sets the callback to monitor the selected group index. */
@@ -195,29 +154,6 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
} else {
mTextPaint = null;
}
setVisibility(View.VISIBLE);
requestLayout();
}
/** Sets the latest timestamp for drawing into x-axis information. */
public void setLatestTimestamp(long latestTimestamp) {
if (latestTimestamp == 0) {
latestTimestamp = Clock.systemUTC().millis();
}
if (mTimestamps == null) {
mTimestamps = new String[DEFAULT_TIMESTAMP_COUNT];
}
final long timeSlotOffset =
DateUtils.HOUR_IN_MILLIS * (/*total 24 hours*/ 24 / TIMESTAMP_GAPS_COUNT);
final boolean is24HourFormat = DateFormat.is24HourFormat(getContext());
for (int index = 0; index < DEFAULT_TIMESTAMP_COUNT; index++) {
mTimestamps[index] =
ConvertUtils.utcToLocalTimeHour(
getContext(),
latestTimestamp - (TIMESTAMP_GAPS_COUNT - index)
* timeSlotOffset,
is24HourFormat);
}
requestLayout();
}
@@ -226,6 +162,7 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// Measures text bounds and updates indent configuration.
if (mTextPaint != null) {
mTextPaint.setTextAlign(Paint.Align.LEFT);
for (int index = 0; index < mPercentages.length; index++) {
mTextPaint.getTextBounds(
mPercentages[index], 0, mPercentages[index].length(),
@@ -235,15 +172,14 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
mIndent.top = mPercentageBounds[0].height();
mIndent.right = mPercentageBounds[0].width() + mTextPadding;
if (mTimestamps != null) {
int maxHeight = 0;
for (int index = 0; index < DEFAULT_TIMESTAMP_COUNT; index++) {
mTextPaint.getTextBounds(
mTimestamps[index], 0, mTimestamps[index].length(),
mTimestampsBounds[index]);
maxHeight = Math.max(maxHeight, mTimestampsBounds[index].height());
if (mViewModel != null) {
int maxTop = 0;
for (int index = 0; index < mViewModel.size(); index++) {
final String text = mViewModel.texts().get(index);
mTextPaint.getTextBounds(text, 0, text.length(), mAxisLabelsBounds.get(index));
maxTop = Math.max(maxTop, -mAxisLabelsBounds.get(index).top);
}
mIndent.bottom = maxHeight + round(mTextPadding * 1.5f);
mIndent.bottom = maxTop + round(mTextPadding * 2f);
}
Log.d(TAG, "setIndent:" + mPercentageBounds[0]);
} else {
@@ -254,7 +190,12 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
// Before mLevels initialized, the count of trapezoids is unknown. Only draws the
// horizontal percentages and dividers.
drawHorizontalDividers(canvas);
if (mViewModel == null) {
return;
}
drawVerticalDividers(canvas);
drawTrapezoids(canvas);
}
@@ -294,7 +235,7 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
public void onHoverChanged(boolean hovered) {
super.onHoverChanged(hovered);
if (!hovered) {
mHoveredIndex = SELECTED_INDEX_INVALID; // reset
mHoveredIndex = BatteryChartViewModel.SELECTED_INDEX_INVALID; // reset
invalidate();
}
}
@@ -307,15 +248,15 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
}
final int trapezoidIndex = getTrapezoidIndex(mTouchUpEventX);
// Ignores the click event if the level is zero.
if (trapezoidIndex == SELECTED_INDEX_INVALID
|| !isValidToDraw(trapezoidIndex)) {
if (trapezoidIndex == BatteryChartViewModel.SELECTED_INDEX_INVALID
|| !isValidToDraw(mViewModel, trapezoidIndex)) {
return;
}
// Selects all if users click the same trapezoid item two times.
if (trapezoidIndex == mSelectedIndex) {
setSelectedIndex(SELECTED_INDEX_ALL);
} else {
setSelectedIndex(trapezoidIndex);
if (mOnSelectListener != null) {
// Selects all if users click the same trapezoid item two times.
mOnSelectListener.onSelect(
trapezoidIndex == mViewModel.selectedIndex()
? BatteryChartViewModel.SELECTED_INDEX_ALL : trapezoidIndex);
}
view.performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK);
}
@@ -364,8 +305,8 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
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();
}
@@ -380,6 +321,13 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
super.setClickable(clickable);
}
private void initializeTrapezoidSlots(int count) {
mTrapezoidSlots = new TrapezoidSlot[count];
for (int index = 0; index < mTrapezoidSlots.length; index++) {
mTrapezoidSlots[index] = new TrapezoidSlot();
}
}
private void initializeColors(Context context) {
setBackgroundColor(Color.TRANSPARENT);
mTrapezoidSolidColor = Utils.getColorAccentDefaultColor(context);
@@ -434,10 +382,10 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
private void drawPercentage(Canvas canvas, int index, float offsetY) {
if (mTextPaint != null) {
mTextPaint.setTextAlign(Paint.Align.RIGHT);
canvas.drawText(
mPercentages[index],
getWidth() - mPercentageBounds[index].width()
- mPercentageBounds[index].left,
getWidth(),
offsetY + mPercentageBounds[index].height() * .5f,
mTextPaint);
}
@@ -445,9 +393,9 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
private void drawVerticalDividers(Canvas canvas) {
final int width = getWidth() - mIndent.right;
final int dividerCount = mTrapezoidCount + 1;
final int dividerCount = mTrapezoidSlots.length + 1;
final float dividerSpace = dividerCount * mDividerWidth;
final float unitWidth = (width - dividerSpace) / (float) mTrapezoidCount;
final float unitWidth = (width - dividerSpace) / (float) mTrapezoidSlots.length;
final float bottomY = getHeight() - mIndent.bottom;
final float startY = bottomY - mDividerHeight;
final float trapezoidSlotOffset = mTrapezoidHOffset + mDividerWidth * .5f;
@@ -463,66 +411,140 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
}
startX = nextX;
}
// Draws the timestamp slot information.
if (mTimestamps != null) {
final float[] xOffsets = new float[DEFAULT_TIMESTAMP_COUNT];
final float baselineX = mDividerWidth * .5f;
final float offsetX = mDividerWidth + unitWidth;
final int slotBarOffset = (/*total 12 bars*/ 12) / TIMESTAMP_GAPS_COUNT;
for (int index = 0; index < DEFAULT_TIMESTAMP_COUNT; index++) {
xOffsets[index] = baselineX + index * offsetX * slotBarOffset;
// Draws the axis label slot information.
if (mViewModel != null) {
final float baselineY = getHeight() - mTextPadding * 1.5f;
Rect[] axisLabelDisplayAreas;
switch (mViewModel.axisLabelPosition()) {
case CENTER_OF_TRAPEZOIDS:
axisLabelDisplayAreas = getAxisLabelDisplayAreas(
/* size= */ mViewModel.size() - 1,
/* baselineX= */ mDividerWidth + unitWidth * .5f,
/* offsetX= */ mDividerWidth + unitWidth,
baselineY,
/* shiftFirstAndLast= */ false);
break;
case BETWEEN_TRAPEZOIDS:
default:
axisLabelDisplayAreas = getAxisLabelDisplayAreas(
/* size= */ mViewModel.size(),
/* baselineX= */ mDividerWidth * .5f,
/* offsetX= */ mDividerWidth + unitWidth,
baselineY,
/* shiftFirstAndLast= */ true);
break;
}
drawTimestamp(canvas, xOffsets);
drawAxisLabels(canvas, axisLabelDisplayAreas, baselineY);
}
}
private void drawTimestamp(Canvas canvas, float[] xOffsets) {
// Draws the 1st timestamp info.
canvas.drawText(
mTimestamps[0],
xOffsets[0] - mTimestampsBounds[0].left,
getTimestampY(0), mTextPaint);
final int latestIndex = DEFAULT_TIMESTAMP_COUNT - 1;
// Draws the last timestamp info.
canvas.drawText(
mTimestamps[latestIndex],
xOffsets[latestIndex] - mTimestampsBounds[latestIndex].width()
- mTimestampsBounds[latestIndex].left,
getTimestampY(latestIndex), mTextPaint);
// Draws the rest of timestamp info since it is located in the center.
for (int index = 1; index <= DEFAULT_TIMESTAMP_COUNT - 2; index++) {
canvas.drawText(
mTimestamps[index],
xOffsets[index]
- (mTimestampsBounds[index].width() - mTimestampsBounds[index].left)
* .5f,
getTimestampY(index), mTextPaint);
/** Gets all the axis label texts displaying area positions if they are shown. */
private Rect[] getAxisLabelDisplayAreas(final int size, final float baselineX,
final float offsetX, final float baselineY, final boolean shiftFirstAndLast) {
final Rect[] result = new Rect[size];
for (int index = 0; index < result.length; index++) {
final float width = mAxisLabelsBounds.get(index).width();
float middle = baselineX + index * offsetX;
if (shiftFirstAndLast) {
if (index == 0) {
middle += width * .5f;
}
if (index == size - 1) {
middle -= width * .5f;
}
}
final float left = middle - width * .5f;
final float right = left + width;
final float top = baselineY + mAxisLabelsBounds.get(index).top;
final float bottom = top + mAxisLabelsBounds.get(index).height();
result[index] = new Rect(round(left), round(top), round(right), round(bottom));
}
return result;
}
private void drawAxisLabels(Canvas canvas, final Rect[] displayAreas, final float baselineY) {
final int lastIndex = displayAreas.length - 1;
// Suppose first and last labels are always able to draw.
drawAxisLabelText(canvas, 0, displayAreas[0], baselineY);
drawAxisLabelText(canvas, lastIndex, displayAreas[lastIndex], baselineY);
drawAxisLabelsBetweenStartIndexAndEndIndex(canvas, displayAreas, 0, lastIndex, baselineY);
}
/**
* Recursively draws axis labels between the start index and the end index. If the inner number
* can be exactly divided into 2 parts, check and draw the middle index label and then
* recursively draw the 2 parts. Otherwise, divide into 3 parts. Check and draw the middle two
* labels and then recursively draw the 3 parts. If there are any overlaps, skip drawing and go
* back to the uplevel of the recursion.
*/
private void drawAxisLabelsBetweenStartIndexAndEndIndex(Canvas canvas,
final Rect[] displayAreas, final int startIndex, final int endIndex,
final float baselineY) {
if (endIndex - startIndex <= 1) {
return;
}
if ((endIndex - startIndex) % 2 == 0) {
int middleIndex = (startIndex + endIndex) / 2;
if (hasOverlap(displayAreas, startIndex, middleIndex)
|| hasOverlap(displayAreas, middleIndex, endIndex)) {
return;
}
drawAxisLabelText(canvas, middleIndex, displayAreas[middleIndex], baselineY);
drawAxisLabelsBetweenStartIndexAndEndIndex(
canvas, displayAreas, startIndex, middleIndex, baselineY);
drawAxisLabelsBetweenStartIndexAndEndIndex(
canvas, displayAreas, middleIndex, endIndex, baselineY);
} else {
int middleIndex1 = startIndex + round((endIndex - startIndex) / 3f);
int middleIndex2 = startIndex + round((endIndex - startIndex) * 2 / 3f);
if (hasOverlap(displayAreas, startIndex, middleIndex1)
|| hasOverlap(displayAreas, middleIndex1, middleIndex2)
|| hasOverlap(displayAreas, middleIndex2, endIndex)) {
return;
}
drawAxisLabelText(canvas, middleIndex1, displayAreas[middleIndex1], baselineY);
drawAxisLabelText(canvas, middleIndex2, displayAreas[middleIndex2], baselineY);
drawAxisLabelsBetweenStartIndexAndEndIndex(
canvas, displayAreas, startIndex, middleIndex1, baselineY);
drawAxisLabelsBetweenStartIndexAndEndIndex(
canvas, displayAreas, middleIndex1, middleIndex2, baselineY);
drawAxisLabelsBetweenStartIndexAndEndIndex(
canvas, displayAreas, middleIndex2, endIndex, baselineY);
}
}
private int getTimestampY(int index) {
return getHeight() - mTimestampsBounds[index].height()
+ (mTimestampsBounds[index].height() + mTimestampsBounds[index].top)
+ round(mTextPadding * 1.5f);
private boolean hasOverlap(
final Rect[] displayAreas, final int leftIndex, final int rightIndex) {
return displayAreas[leftIndex].right + mTextPadding * 2f > displayAreas[rightIndex].left;
}
private void drawAxisLabelText(
Canvas canvas, final int index, final Rect displayArea, final float baselineY) {
mTextPaint.setTextAlign(Paint.Align.CENTER);
canvas.drawText(
mViewModel.texts().get(index),
displayArea.centerX(),
baselineY,
mTextPaint);
}
private void drawTrapezoids(Canvas canvas) {
// Ignores invalid trapezoid data.
if (mLevels == null) {
if (mViewModel == null) {
return;
}
final float trapezoidBottom =
getHeight() - mIndent.bottom - mDividerHeight - mDividerWidth
- mTrapezoidVOffset;
final float availableSpace = trapezoidBottom - mDividerWidth * .5f - mIndent.top;
final float availableSpace =
trapezoidBottom - mDividerWidth * .5f - mIndent.top - mTrapezoidVOffset;
final float unitHeight = availableSpace / 100f;
// Draws all trapezoid shapes into the canvas.
final Path trapezoidPath = new Path();
Path trapezoidCurvePath = null;
for (int index = 0; index < mTrapezoidCount; index++) {
for (int index = 0; index < mTrapezoidSlots.length; index++) {
// Not draws the trapezoid for corner or not initialization cases.
if (!isValidToDraw(index)) {
if (!isValidToDraw(mViewModel, index)) {
if (mTrapezoidCurvePaint != null && trapezoidCurvePath != null) {
canvas.drawPath(trapezoidCurvePath, mTrapezoidCurvePaint);
trapezoidCurvePath = null;
@@ -530,17 +552,18 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
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);
mIsSlotsClickabled && mHoveredIndex == index
&& isValidToDraw(mViewModel, 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 - requireNonNull(mViewModel.levels().get(index)) * unitHeight);
final float rightTop = round(trapezoidBottom
- requireNonNull(mViewModel.levels().get(index + 1)) * unitHeight);
trapezoidPath.reset();
trapezoidPath.moveTo(mTrapezoidSlots[index].mLeft, trapezoidBottom);
trapezoidPath.lineTo(mTrapezoidSlots[index].mLeft, leftTop);
@@ -579,15 +602,37 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
return index;
}
}
return SELECTED_INDEX_INVALID;
return BatteryChartViewModel.SELECTED_INDEX_INVALID;
}
private boolean isValidToDraw(int trapezoidIndex) {
return mLevels != null
private void initializeAxisLabelsBounds() {
mAxisLabelsBounds.clear();
for (int i = 0; i < mViewModel.size(); i++) {
mAxisLabelsBounds.add(new Rect());
}
}
private static boolean isTrapezoidValid(
@NonNull BatteryChartViewModel viewModel, int trapezoidIndex) {
return viewModel.levels().get(trapezoidIndex) != null
&& viewModel.levels().get(trapezoidIndex + 1) != null;
}
private static boolean isValidToDraw(BatteryChartViewModel viewModel, int trapezoidIndex) {
return viewModel != null
&& trapezoidIndex >= 0
&& trapezoidIndex < mLevels.length - 1
&& mLevels[trapezoidIndex] != 0
&& mLevels[trapezoidIndex + 1] != 0;
&& trapezoidIndex < viewModel.size() - 1
&& isTrapezoidValid(viewModel, trapezoidIndex);
}
private static boolean hasAnyValidTrapezoid(@NonNull BatteryChartViewModel viewModel) {
// Sets the chart is clickable if there is at least one valid item in it.
for (int trapezoidIndex = 0; trapezoidIndex < viewModel.size() - 1; trapezoidIndex++) {
if (isTrapezoidValid(viewModel, trapezoidIndex)) {
return true;
}
}
return false;
}
private static String[] getPercentages() {
@@ -621,7 +666,8 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
}
// A container class for each trapezoid left and right location.
private static final class TrapezoidSlot {
@VisibleForTesting
static final class TrapezoidSlot {
public float mLeft;
public float mRight;

View File

@@ -0,0 +1,109 @@
/*
* 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 BatteryChartView} */
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;
enum AxisLabelPosition {
BETWEEN_TRAPEZOIDS,
CENTER_OF_TRAPEZOIDS,
}
private final List<Integer> mLevels;
private final List<String> mTexts;
private final AxisLabelPosition mAxisLabelPosition;
private int mSelectedIndex = SELECTED_INDEX_ALL;
BatteryChartViewModel(
@NonNull List<Integer> levels, @NonNull List<String> texts,
@NonNull AxisLabelPosition axisLabelPosition) {
Preconditions.checkArgument(
levels.size() == texts.size() && levels.size() >= MIN_LEVELS_DATA_SIZE,
String.format(Locale.ENGLISH,
"Invalid BatteryChartViewModel levels.size: %d, texts.size: %d.",
levels.size(), texts.size()));
mLevels = levels;
mTexts = texts;
mAxisLabelPosition = axisLabelPosition;
}
public int size() {
return mLevels.size();
}
public List<Integer> levels() {
return mLevels;
}
public List<String> texts() {
return mTexts;
}
public AxisLabelPosition axisLabelPosition() {
return mAxisLabelPosition;
}
public int selectedIndex() {
return mSelectedIndex;
}
public void setSelectedIndex(int index) {
mSelectedIndex = index;
}
@Override
public int hashCode() {
return Objects.hash(mLevels, mTexts, mSelectedIndex, mAxisLabelPosition);
}
@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)
&& mAxisLabelPosition == batteryChartViewModel.mAxisLabelPosition
&& mSelectedIndex == batteryChartViewModel.mSelectedIndex;
}
@Override
public String toString() {
return String.format(Locale.ENGLISH,
"levels: %s,\ntexts: %s,\naxisLabelPosition: %s, selectedIndex: %d",
Objects.toString(mLevels), Objects.toString(mTexts), mAxisLabelPosition,
mSelectedIndex);
}
}

View File

@@ -0,0 +1,68 @@
/*
* 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 java.util.Collections;
import java.util.List;
/** Wraps the battery usage diff data for each entry used for battery usage app list. */
public class BatteryDiffData {
private final List<BatteryDiffEntry> mAppEntries;
private final List<BatteryDiffEntry> mSystemEntries;
/** Constructor for the diff entries which already have totalConsumePower value. */
public BatteryDiffData(
@NonNull List<BatteryDiffEntry> appDiffEntries,
@NonNull List<BatteryDiffEntry> systemDiffEntries) {
mAppEntries = appDiffEntries;
mSystemEntries = systemDiffEntries;
sortEntries();
}
/** Constructor for the diff entries which have not set totalConsumePower value. */
public BatteryDiffData(
@NonNull List<BatteryDiffEntry> appDiffEntries,
@NonNull List<BatteryDiffEntry> systemDiffEntries,
final double totalConsumePower) {
mAppEntries = appDiffEntries;
mSystemEntries = systemDiffEntries;
setTotalConsumePowerForAllEntries(totalConsumePower);
sortEntries();
}
public List<BatteryDiffEntry> getAppDiffEntryList() {
return mAppEntries;
}
public List<BatteryDiffEntry> getSystemDiffEntryList() {
return mSystemEntries;
}
// Sets total consume power for each entry.
private void setTotalConsumePowerForAllEntries(final double totalConsumePower) {
mAppEntries.forEach(diffEntry -> diffEntry.setTotalConsumePower(totalConsumePower));
mSystemEntries.forEach(diffEntry -> diffEntry.setTotalConsumePower(totalConsumePower));
}
// Sorts entries based on consumed percentage.
private void sortEntries() {
Collections.sort(mAppEntries, BatteryDiffEntry.COMPARATOR);
Collections.sort(mSystemEntries, BatteryDiffEntry.COMPARATOR);
}
}

View File

@@ -43,6 +43,6 @@ public class BatteryHistoryLoader
public Map<Long, Map<String, BatteryHistEntry>> loadInBackground() {
final PowerUsageFeatureProvider powerUsageFeatureProvider =
FeatureFactory.getFactory(mContext).getPowerUsageFeatureProvider(mContext);
return powerUsageFeatureProvider.getBatteryHistory(mContext);
return powerUsageFeatureProvider.getBatteryHistorySinceLastFullCharge(mContext);
}
}

View File

@@ -49,7 +49,8 @@ public class BatteryHistoryPreference extends Preference {
private TextView mSummaryView;
private CharSequence mSummaryContent;
private BatteryChartView mBatteryChartView;
private BatteryChartView mDailyChartView;
private BatteryChartView mHourlyChartView;
private BatteryChartPreferenceController mChartPreferenceController;
public BatteryHistoryPreference(Context context, AttributeSet attrs) {
@@ -92,8 +93,8 @@ public class BatteryHistoryPreference extends Preference {
void setChartPreferenceController(BatteryChartPreferenceController controller) {
mChartPreferenceController = controller;
if (mBatteryChartView != null) {
mChartPreferenceController.setBatteryChartView(mBatteryChartView);
if (mDailyChartView != null && mHourlyChartView != null) {
mChartPreferenceController.setBatteryChartView(mDailyChartView, mHourlyChartView);
}
}
@@ -105,11 +106,14 @@ public class BatteryHistoryPreference extends Preference {
return;
}
if (mIsChartGraphEnabled) {
mBatteryChartView = (BatteryChartView) view.findViewById(R.id.battery_chart);
mBatteryChartView.setCompanionTextView(
mDailyChartView = (BatteryChartView) view.findViewById(R.id.daily_battery_chart);
mDailyChartView.setCompanionTextView(
(TextView) view.findViewById(R.id.companion_text));
mHourlyChartView = (BatteryChartView) view.findViewById(R.id.hourly_battery_chart);
mHourlyChartView.setCompanionTextView(
(TextView) view.findViewById(R.id.companion_text));
if (mChartPreferenceController != null) {
mChartPreferenceController.setBatteryChartView(mBatteryChartView);
mChartPreferenceController.setBatteryChartView(mDailyChartView, mHourlyChartView);
}
} else {
final TextView chargeView = (TextView) view.findViewById(R.id.charge);

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;
/** Wraps the battery timestamp and level data used for battery usage chart. */
public final class BatteryLevelData {
/** A container for the battery timestamp and level data. */
public static final class PeriodBatteryLevelData {
// The length of mTimestamps and mLevels must be the same. mLevels[index] might be null when
// there is no level data for the corresponding timestamp.
private final List<Long> mTimestamps;
private final List<Integer> mLevels;
public PeriodBatteryLevelData(
@NonNull List<Long> timestamps, @NonNull List<Integer> levels) {
Preconditions.checkArgument(timestamps.size() == levels.size(),
/* errorMessage= */ "Timestamp: " + timestamps.size() + ", Level: "
+ levels.size());
mTimestamps = timestamps;
mLevels = levels;
}
public List<Long> getTimestamps() {
return mTimestamps;
}
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));
}
}
/**
* There could be 2 cases for the daily battery levels:
* 1) length is 2: The usage data is within 1 day. Only contains start and end data, such as
* data of 2022-01-01 06:00 and 2022-01-01 16:00.
* 2) length > 2: The usage data is more than 1 days. The data should be the start, end and 0am
* data of every day between the start and end, such as data of 2022-01-01 06:00,
* 2022-01-02 00:00, 2022-01-03 00:00 and 2022-01-03 08:00.
*/
private final PeriodBatteryLevelData mDailyBatteryLevels;
// The size of hourly data must be the size of daily data - 1.
private final List<PeriodBatteryLevelData> mHourlyBatteryLevelsPerDay;
public BatteryLevelData(
@NonNull PeriodBatteryLevelData dailyBatteryLevels,
@NonNull List<PeriodBatteryLevelData> hourlyBatteryLevelsPerDay) {
final long dailySize = dailyBatteryLevels.getTimestamps().size();
final long hourlySize = hourlyBatteryLevelsPerDay.size();
Preconditions.checkArgument(hourlySize == dailySize - 1,
/* errorMessage= */ "DailySize: " + dailySize + ", HourlySize: " + hourlySize);
mDailyBatteryLevels = dailyBatteryLevels;
mHourlyBatteryLevelsPerDay = hourlyBatteryLevelsPerDay;
}
public PeriodBatteryLevelData getDailyBatteryLevels() {
return mDailyBatteryLevels;
}
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,15 @@ 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 boolean isAbbreviation) {
final Locale locale = getLocale(context);
final String pattern = DateFormat.getBestDateTimePattern(locale,
isAbbreviation ? "E" : "EEEE");
return DateFormat.format(pattern, timestamp).toString();
}
/** Gets indexed battery usage data for each corresponding time slot. */
public static Map<Integer, List<BatteryDiffEntry>> getIndexedUsageMap(
final Context context,
@@ -267,7 +276,7 @@ public final class ConvertUtils {
diffEntry.setTotalConsumePower(totalConsumePower);
}
}
insert24HoursData(BatteryChartView.SELECTED_INDEX_ALL, resultMap);
insert24HoursData(BatteryChartViewModel.SELECTED_INDEX_ALL, resultMap);
resolveMultiUsersData(context, resultMap);
if (purgeLowPercentageAndFakeData) {
purgeLowPercentageAndFakeData(context, resultMap);

File diff suppressed because it is too large Load Diff

View File

@@ -259,10 +259,7 @@ public class PowerUsageSummary extends PowerUsageBase implements
@VisibleForTesting
void initPreference() {
mBatteryUsagePreference = findPreference(KEY_BATTERY_USAGE);
mBatteryUsagePreference.setSummary(
mPowerFeatureProvider.isChartGraphEnabled(getContext())
? getString(R.string.advanced_battery_preference_summary_with_hours)
: getString(R.string.advanced_battery_preference_summary));
mBatteryUsagePreference.setSummary(getString(R.string.advanced_battery_preference_summary));
mHelpPreference = findPreference(KEY_BATTERY_ERROR);
mHelpPreference.setVisible(false);