Duplicate BatteryChartPreferenceController and BatteryChartView into new
files for better diff review purpose
Bug: 236101687
Test: make RunSettingsRoboTests
Change-Id: I2d29bbfe14bcc5df7c09bceec2cbb0673685f522
diff --git a/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceControllerV2.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceControllerV2.java
new file mode 100644
index 0000000..10f19f9
--- /dev/null
+++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceControllerV2.java
@@ -0,0 +1,737 @@
+/*
+ * 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 android.app.settings.SettingsEnums;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.drawable.Drawable;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.text.TextUtils;
+import android.text.format.DateFormat;
+import android.text.format.DateUtils;
+import android.util.Log;
+
+import androidx.annotation.VisibleForTesting;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceGroup;
+import androidx.preference.PreferenceScreen;
+
+import com.android.settings.R;
+import com.android.settings.SettingsActivity;
+import com.android.settings.core.InstrumentedPreferenceFragment;
+import com.android.settings.core.PreferenceControllerMixin;
+import com.android.settings.fuelgauge.AdvancedPowerUsageDetail;
+import com.android.settings.fuelgauge.BatteryUtils;
+import com.android.settings.overlay.FeatureFactory;
+import com.android.settingslib.core.AbstractPreferenceController;
+import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
+import com.android.settingslib.core.lifecycle.Lifecycle;
+import com.android.settingslib.core.lifecycle.LifecycleObserver;
+import com.android.settingslib.core.lifecycle.events.OnCreate;
+import com.android.settingslib.core.lifecycle.events.OnDestroy;
+import com.android.settingslib.core.lifecycle.events.OnResume;
+import com.android.settingslib.core.lifecycle.events.OnSaveInstanceState;
+import com.android.settingslib.utils.StringUtil;
+import com.android.settingslib.widget.FooterPreference;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/** Controls the update for chart graph and the list items. */
+public class BatteryChartPreferenceControllerV2 extends AbstractPreferenceController
+ implements PreferenceControllerMixin, LifecycleObserver, OnCreate, OnDestroy,
+ OnSaveInstanceState, BatteryChartViewV2.OnSelectListener, OnResume,
+ ExpandDividerPreference.OnExpandListener {
+ private static final String TAG = "BatteryChartPreferenceControllerV2";
+ private static final String KEY_FOOTER_PREF = "battery_graph_footer";
+ private static final String PACKAGE_NAME_NONE = "none";
+
+ /** Desired battery history size for timestamp slots. */
+ public static final int DESIRED_HISTORY_SIZE = 25;
+ private static final int CHART_LEVEL_ARRAY_SIZE = 13;
+ private static final int CHART_KEY_ARRAY_SIZE = DESIRED_HISTORY_SIZE;
+ private static final long VALID_USAGE_TIME_DURATION = DateUtils.HOUR_IN_MILLIS * 2;
+ private static final long VALID_DIFF_DURATION = DateUtils.MINUTE_IN_MILLIS * 3;
+
+ // Keys for bundle instance to restore configurations.
+ private static final String KEY_EXPAND_SYSTEM_INFO = "expand_system_info";
+ private static final String KEY_CURRENT_TIME_SLOT = "current_time_slot";
+
+ private static int sUiMode = Configuration.UI_MODE_NIGHT_UNDEFINED;
+
+ @VisibleForTesting
+ Map<Integer, List<BatteryDiffEntry>> mBatteryIndexedMap;
+
+ @VisibleForTesting
+ Context mPrefContext;
+ @VisibleForTesting
+ BatteryUtils mBatteryUtils;
+ @VisibleForTesting
+ PreferenceGroup mAppListPrefGroup;
+ @VisibleForTesting
+ BatteryChartViewV2 mBatteryChartView;
+ @VisibleForTesting
+ ExpandDividerPreference mExpandDividerPreference;
+
+ @VisibleForTesting
+ boolean mIsExpanded = false;
+ @VisibleForTesting
+ int[] mBatteryHistoryLevels;
+ @VisibleForTesting
+ long[] mBatteryHistoryKeys;
+ @VisibleForTesting
+ int mTrapezoidIndex = BatteryChartViewV2.SELECTED_INDEX_INVALID;
+
+ private boolean mIs24HourFormat = false;
+ private boolean mIsFooterPrefAdded = false;
+ private PreferenceScreen mPreferenceScreen;
+ private FooterPreference mFooterPreference;
+
+ private final String mPreferenceKey;
+ private final SettingsActivity mActivity;
+ private final InstrumentedPreferenceFragment mFragment;
+ private final CharSequence[] mNotAllowShowEntryPackages;
+ private final CharSequence[] mNotAllowShowSummaryPackages;
+ private final MetricsFeatureProvider mMetricsFeatureProvider;
+ private final Handler mHandler = new Handler(Looper.getMainLooper());
+
+ // Preference cache to avoid create new instance each time.
+ @VisibleForTesting
+ final Map<String, Preference> mPreferenceCache = new HashMap<>();
+ @VisibleForTesting
+ final List<BatteryDiffEntry> mSystemEntries = new ArrayList<>();
+
+ public BatteryChartPreferenceControllerV2(
+ Context context, String preferenceKey,
+ Lifecycle lifecycle, SettingsActivity activity,
+ InstrumentedPreferenceFragment fragment) {
+ super(context);
+ mActivity = activity;
+ mFragment = fragment;
+ mPreferenceKey = preferenceKey;
+ mIs24HourFormat = DateFormat.is24HourFormat(context);
+ mMetricsFeatureProvider =
+ FeatureFactory.getFactory(mContext).getMetricsFeatureProvider();
+ mNotAllowShowEntryPackages =
+ FeatureFactory.getFactory(context)
+ .getPowerUsageFeatureProvider(context)
+ .getHideApplicationEntries(context);
+ mNotAllowShowSummaryPackages =
+ FeatureFactory.getFactory(context)
+ .getPowerUsageFeatureProvider(context)
+ .getHideApplicationSummary(context);
+ if (lifecycle != null) {
+ lifecycle.addObserver(this);
+ }
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ if (savedInstanceState == null) {
+ return;
+ }
+ mTrapezoidIndex =
+ savedInstanceState.getInt(KEY_CURRENT_TIME_SLOT, mTrapezoidIndex);
+ mIsExpanded =
+ savedInstanceState.getBoolean(KEY_EXPAND_SYSTEM_INFO, mIsExpanded);
+ Log.d(TAG, String.format("onCreate() slotIndex=%d isExpanded=%b",
+ mTrapezoidIndex, mIsExpanded));
+ }
+
+ @Override
+ public void onResume() {
+ final int currentUiMode =
+ mContext.getResources().getConfiguration().uiMode
+ & Configuration.UI_MODE_NIGHT_MASK;
+ if (sUiMode != currentUiMode) {
+ sUiMode = currentUiMode;
+ BatteryDiffEntry.clearCache();
+ Log.d(TAG, "clear icon and label cache since uiMode is changed");
+ }
+ mIs24HourFormat = DateFormat.is24HourFormat(mContext);
+ mMetricsFeatureProvider.action(mPrefContext, SettingsEnums.OPEN_BATTERY_USAGE);
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle savedInstance) {
+ if (savedInstance == null) {
+ return;
+ }
+ savedInstance.putInt(KEY_CURRENT_TIME_SLOT, mTrapezoidIndex);
+ savedInstance.putBoolean(KEY_EXPAND_SYSTEM_INFO, mIsExpanded);
+ Log.d(TAG, String.format("onSaveInstanceState() slotIndex=%d isExpanded=%b",
+ mTrapezoidIndex, mIsExpanded));
+ }
+
+ @Override
+ public void onDestroy() {
+ if (mActivity.isChangingConfigurations()) {
+ BatteryDiffEntry.clearCache();
+ }
+ mHandler.removeCallbacksAndMessages(/*token=*/ null);
+ mPreferenceCache.clear();
+ if (mAppListPrefGroup != null) {
+ mAppListPrefGroup.removeAll();
+ }
+ }
+
+ @Override
+ public void displayPreference(PreferenceScreen screen) {
+ super.displayPreference(screen);
+ mPreferenceScreen = screen;
+ mPrefContext = screen.getContext();
+ mAppListPrefGroup = screen.findPreference(mPreferenceKey);
+ mAppListPrefGroup.setOrderingAsAdded(false);
+ mAppListPrefGroup.setTitle(
+ mPrefContext.getString(R.string.battery_app_usage_for_past_24));
+ mFooterPreference = screen.findPreference(KEY_FOOTER_PREF);
+ // Removes footer first until usage data is loaded to avoid flashing.
+ if (mFooterPreference != null) {
+ screen.removePreference(mFooterPreference);
+ }
+ }
+
+ @Override
+ public boolean isAvailable() {
+ return true;
+ }
+
+ @Override
+ public String getPreferenceKey() {
+ return mPreferenceKey;
+ }
+
+ @Override
+ public boolean handlePreferenceTreeClick(Preference preference) {
+ if (!(preference instanceof PowerGaugePreference)) {
+ return false;
+ }
+ final PowerGaugePreference powerPref = (PowerGaugePreference) preference;
+ final BatteryDiffEntry diffEntry = powerPref.getBatteryDiffEntry();
+ final BatteryHistEntry histEntry = diffEntry.mBatteryHistEntry;
+ final String packageName = histEntry.mPackageName;
+ final boolean isAppEntry = histEntry.isAppEntry();
+ mMetricsFeatureProvider.action(
+ /* attribution */ SettingsEnums.OPEN_BATTERY_USAGE,
+ /* action */ isAppEntry
+ ? SettingsEnums.ACTION_BATTERY_USAGE_APP_ITEM
+ : SettingsEnums.ACTION_BATTERY_USAGE_SYSTEM_ITEM,
+ /* pageId */ SettingsEnums.OPEN_BATTERY_USAGE,
+ TextUtils.isEmpty(packageName) ? PACKAGE_NAME_NONE : packageName,
+ (int) Math.round(diffEntry.getPercentOfTotal()));
+ Log.d(TAG, String.format("handleClick() label=%s key=%s package=%s",
+ diffEntry.getAppLabel(), histEntry.getKey(), histEntry.mPackageName));
+ AdvancedPowerUsageDetail.startBatteryDetailPage(
+ mActivity, mFragment, diffEntry, powerPref.getPercent(),
+ isValidToShowSummary(packageName), getSlotInformation());
+ return true;
+ }
+
+ @Override
+ public void onSelect(int trapezoidIndex) {
+ Log.d(TAG, "onChartSelect:" + trapezoidIndex);
+ refreshUi(trapezoidIndex, /*isForce=*/ false);
+ mMetricsFeatureProvider.action(
+ mPrefContext,
+ trapezoidIndex == BatteryChartViewV2.SELECTED_INDEX_ALL
+ ? SettingsEnums.ACTION_BATTERY_USAGE_SHOW_ALL
+ : SettingsEnums.ACTION_BATTERY_USAGE_TIME_SLOT);
+ }
+
+ @Override
+ public void onExpand(boolean isExpanded) {
+ mIsExpanded = isExpanded;
+ mMetricsFeatureProvider.action(
+ mPrefContext,
+ SettingsEnums.ACTION_BATTERY_USAGE_EXPAND_ITEM,
+ isExpanded);
+ refreshExpandUi();
+ }
+
+ void setBatteryHistoryMap(
+ final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap) {
+ // Resets all battery history data relative variables.
+ if (batteryHistoryMap == null || batteryHistoryMap.isEmpty()) {
+ mBatteryIndexedMap = null;
+ mBatteryHistoryKeys = null;
+ mBatteryHistoryLevels = null;
+ addFooterPreferenceIfNeeded(false);
+ return;
+ }
+ mBatteryHistoryKeys = getBatteryHistoryKeys(batteryHistoryMap);
+ mBatteryHistoryLevels = new int[CHART_LEVEL_ARRAY_SIZE];
+ for (int index = 0; index < CHART_LEVEL_ARRAY_SIZE; index++) {
+ final long timestamp = mBatteryHistoryKeys[index * 2];
+ final Map<String, BatteryHistEntry> entryMap = batteryHistoryMap.get(timestamp);
+ if (entryMap == null || entryMap.isEmpty()) {
+ Log.e(TAG, "abnormal entry list in the timestamp:"
+ + ConvertUtils.utcToLocalTime(mPrefContext, timestamp));
+ continue;
+ }
+ // Averages the battery level in each time slot to avoid corner conditions.
+ float batteryLevelCounter = 0;
+ for (BatteryHistEntry entry : entryMap.values()) {
+ batteryLevelCounter += entry.mBatteryLevel;
+ }
+ mBatteryHistoryLevels[index] =
+ Math.round(batteryLevelCounter / entryMap.size());
+ }
+ forceRefreshUi();
+ Log.d(TAG, String.format(
+ "setBatteryHistoryMap() size=%d key=%s\nlevels=%s",
+ batteryHistoryMap.size(),
+ ConvertUtils.utcToLocalTime(mPrefContext,
+ mBatteryHistoryKeys[mBatteryHistoryKeys.length - 1]),
+ Arrays.toString(mBatteryHistoryLevels)));
+
+ // Loads item icon and label in the background.
+ new LoadAllItemsInfoTask(batteryHistoryMap).execute();
+ }
+
+ void setBatteryChartView(final BatteryChartViewV2 batteryChartView) {
+ if (mBatteryChartView != batteryChartView) {
+ mHandler.post(() -> setBatteryChartViewInner(batteryChartView));
+ }
+ }
+
+ private void setBatteryChartViewInner(final BatteryChartViewV2 batteryChartView) {
+ mBatteryChartView = batteryChartView;
+ mBatteryChartView.setOnSelectListener(this);
+ forceRefreshUi();
+ }
+
+ private void forceRefreshUi() {
+ final int refreshIndex =
+ mTrapezoidIndex == BatteryChartViewV2.SELECTED_INDEX_INVALID
+ ? BatteryChartViewV2.SELECTED_INDEX_ALL
+ : mTrapezoidIndex;
+ if (mBatteryChartView != null) {
+ mBatteryChartView.setLevels(mBatteryHistoryLevels);
+ mBatteryChartView.setSelectedIndex(refreshIndex);
+ setTimestampLabel();
+ }
+ refreshUi(refreshIndex, /*isForce=*/ true);
+ }
+
+ @VisibleForTesting
+ boolean refreshUi(int trapezoidIndex, boolean isForce) {
+ // Invalid refresh condition.
+ if (mBatteryIndexedMap == null
+ || mBatteryChartView == null
+ || (mTrapezoidIndex == trapezoidIndex && !isForce)) {
+ return false;
+ }
+ Log.d(TAG, String.format("refreshUi: index=%d size=%d isForce:%b",
+ trapezoidIndex, mBatteryIndexedMap.size(), isForce));
+
+ mTrapezoidIndex = trapezoidIndex;
+ mHandler.post(() -> {
+ final long start = System.currentTimeMillis();
+ removeAndCacheAllPrefs();
+ addAllPreferences();
+ refreshCategoryTitle();
+ Log.d(TAG, String.format("refreshUi is finished in %d/ms",
+ (System.currentTimeMillis() - start)));
+ });
+ return true;
+ }
+
+ private void addAllPreferences() {
+ final List<BatteryDiffEntry> entries =
+ mBatteryIndexedMap.get(Integer.valueOf(mTrapezoidIndex));
+ addFooterPreferenceIfNeeded(entries != null && !entries.isEmpty());
+ if (entries == null) {
+ Log.w(TAG, "cannot find BatteryDiffEntry for:" + mTrapezoidIndex);
+ return;
+ }
+ // Separates data into two groups and sort them individually.
+ final List<BatteryDiffEntry> appEntries = new ArrayList<>();
+ mSystemEntries.clear();
+ entries.forEach(entry -> {
+ final String packageName = entry.getPackageName();
+ if (!isValidToShowEntry(packageName)) {
+ Log.w(TAG, "ignore showing item:" + packageName);
+ return;
+ }
+ if (entry.isSystemEntry()) {
+ mSystemEntries.add(entry);
+ } else {
+ appEntries.add(entry);
+ }
+ // Validates the usage time if users click a specific slot.
+ if (mTrapezoidIndex >= 0) {
+ validateUsageTime(entry);
+ }
+ });
+ Collections.sort(appEntries, BatteryDiffEntry.COMPARATOR);
+ Collections.sort(mSystemEntries, BatteryDiffEntry.COMPARATOR);
+ Log.d(TAG, String.format("addAllPreferences() app=%d system=%d",
+ appEntries.size(), mSystemEntries.size()));
+
+ // Adds app entries to the list if it is not empty.
+ if (!appEntries.isEmpty()) {
+ addPreferenceToScreen(appEntries);
+ }
+ // Adds the expabable divider if we have system entries data.
+ if (!mSystemEntries.isEmpty()) {
+ if (mExpandDividerPreference == null) {
+ mExpandDividerPreference = new ExpandDividerPreference(mPrefContext);
+ mExpandDividerPreference.setOnExpandListener(this);
+ mExpandDividerPreference.setIsExpanded(mIsExpanded);
+ }
+ mExpandDividerPreference.setOrder(
+ mAppListPrefGroup.getPreferenceCount());
+ mAppListPrefGroup.addPreference(mExpandDividerPreference);
+ }
+ refreshExpandUi();
+ }
+
+ @VisibleForTesting
+ void addPreferenceToScreen(List<BatteryDiffEntry> entries) {
+ if (mAppListPrefGroup == null || entries.isEmpty()) {
+ return;
+ }
+ int prefIndex = mAppListPrefGroup.getPreferenceCount();
+ for (BatteryDiffEntry entry : entries) {
+ boolean isAdded = false;
+ final String appLabel = entry.getAppLabel();
+ final Drawable appIcon = entry.getAppIcon();
+ if (TextUtils.isEmpty(appLabel) || appIcon == null) {
+ Log.w(TAG, "cannot find app resource for:" + entry.getPackageName());
+ continue;
+ }
+ final String prefKey = entry.mBatteryHistEntry.getKey();
+ PowerGaugePreference pref = mAppListPrefGroup.findPreference(prefKey);
+ if (pref != null) {
+ isAdded = true;
+ Log.w(TAG, "preference should be removed for:" + entry.getPackageName());
+ } else {
+ pref = (PowerGaugePreference) mPreferenceCache.get(prefKey);
+ }
+ // Creates new innstance if cached preference is not found.
+ if (pref == null) {
+ pref = new PowerGaugePreference(mPrefContext);
+ pref.setKey(prefKey);
+ mPreferenceCache.put(prefKey, pref);
+ }
+ pref.setIcon(appIcon);
+ pref.setTitle(appLabel);
+ pref.setOrder(prefIndex);
+ pref.setPercent(entry.getPercentOfTotal());
+ pref.setSingleLineTitle(true);
+ // Sets the BatteryDiffEntry to preference for launching detailed page.
+ pref.setBatteryDiffEntry(entry);
+ pref.setEnabled(entry.validForRestriction());
+ setPreferenceSummary(pref, entry);
+ if (!isAdded) {
+ mAppListPrefGroup.addPreference(pref);
+ }
+ prefIndex++;
+ }
+ }
+
+ private void removeAndCacheAllPrefs() {
+ if (mAppListPrefGroup == null
+ || mAppListPrefGroup.getPreferenceCount() == 0) {
+ return;
+ }
+ final int prefsCount = mAppListPrefGroup.getPreferenceCount();
+ for (int index = 0; index < prefsCount; index++) {
+ final Preference pref = mAppListPrefGroup.getPreference(index);
+ if (TextUtils.isEmpty(pref.getKey())) {
+ continue;
+ }
+ mPreferenceCache.put(pref.getKey(), pref);
+ }
+ mAppListPrefGroup.removeAll();
+ }
+
+ private void refreshExpandUi() {
+ if (mIsExpanded) {
+ addPreferenceToScreen(mSystemEntries);
+ } else {
+ // Removes and recycles all system entries to hide all of them.
+ for (BatteryDiffEntry entry : mSystemEntries) {
+ final String prefKey = entry.mBatteryHistEntry.getKey();
+ final Preference pref = mAppListPrefGroup.findPreference(prefKey);
+ if (pref != null) {
+ mAppListPrefGroup.removePreference(pref);
+ mPreferenceCache.put(pref.getKey(), pref);
+ }
+ }
+ }
+ }
+
+ @VisibleForTesting
+ void refreshCategoryTitle() {
+ final String slotInformation = getSlotInformation();
+ Log.d(TAG, String.format("refreshCategoryTitle:%s", slotInformation));
+ if (mAppListPrefGroup != null) {
+ mAppListPrefGroup.setTitle(
+ getSlotInformation(/*isApp=*/ true, slotInformation));
+ }
+ if (mExpandDividerPreference != null) {
+ mExpandDividerPreference.setTitle(
+ getSlotInformation(/*isApp=*/ false, slotInformation));
+ }
+ }
+
+ private String getSlotInformation(boolean isApp, String slotInformation) {
+ // Null means we show all information without a specific time slot.
+ if (slotInformation == null) {
+ return isApp
+ ? mPrefContext.getString(R.string.battery_app_usage_for_past_24)
+ : mPrefContext.getString(R.string.battery_system_usage_for_past_24);
+ } else {
+ return isApp
+ ? mPrefContext.getString(R.string.battery_app_usage_for, slotInformation)
+ : mPrefContext.getString(R.string.battery_system_usage_for, slotInformation);
+ }
+ }
+
+ private String getSlotInformation() {
+ if (mTrapezoidIndex < 0) {
+ return null;
+ }
+ final String fromHour = ConvertUtils.utcToLocalTimeHour(mPrefContext,
+ mBatteryHistoryKeys[mTrapezoidIndex * 2], mIs24HourFormat);
+ final String toHour = ConvertUtils.utcToLocalTimeHour(mPrefContext,
+ mBatteryHistoryKeys[(mTrapezoidIndex + 1) * 2], mIs24HourFormat);
+ return mIs24HourFormat
+ ? String.format("%s–%s", fromHour, toHour)
+ : String.format("%s – %s", fromHour, toHour);
+ }
+
+ @VisibleForTesting
+ void setPreferenceSummary(
+ PowerGaugePreference preference, BatteryDiffEntry entry) {
+ final long foregroundUsageTimeInMs = entry.mForegroundUsageTimeInMs;
+ final long backgroundUsageTimeInMs = entry.mBackgroundUsageTimeInMs;
+ final long totalUsageTimeInMs = foregroundUsageTimeInMs + backgroundUsageTimeInMs;
+ // Checks whether the package is allowed to show summary or not.
+ if (!isValidToShowSummary(entry.getPackageName())) {
+ preference.setSummary(null);
+ return;
+ }
+ String usageTimeSummary = null;
+ // Not shows summary for some system components without usage time.
+ if (totalUsageTimeInMs == 0) {
+ preference.setSummary(null);
+ // Shows background summary only if we don't have foreground usage time.
+ } else if (foregroundUsageTimeInMs == 0 && backgroundUsageTimeInMs != 0) {
+ usageTimeSummary = buildUsageTimeInfo(backgroundUsageTimeInMs, true);
+ // Shows total usage summary only if total usage time is small.
+ } else if (totalUsageTimeInMs < DateUtils.MINUTE_IN_MILLIS) {
+ usageTimeSummary = buildUsageTimeInfo(totalUsageTimeInMs, false);
+ } else {
+ usageTimeSummary = buildUsageTimeInfo(totalUsageTimeInMs, false);
+ // Shows background usage time if it is larger than a minute.
+ if (backgroundUsageTimeInMs > 0) {
+ usageTimeSummary +=
+ "\n" + buildUsageTimeInfo(backgroundUsageTimeInMs, true);
+ }
+ }
+ preference.setSummary(usageTimeSummary);
+ }
+
+ private String buildUsageTimeInfo(long usageTimeInMs, boolean isBackground) {
+ if (usageTimeInMs < DateUtils.MINUTE_IN_MILLIS) {
+ return mPrefContext.getString(
+ isBackground
+ ? R.string.battery_usage_background_less_than_one_minute
+ : R.string.battery_usage_total_less_than_one_minute);
+ }
+ final CharSequence timeSequence =
+ StringUtil.formatElapsedTime(mPrefContext, usageTimeInMs,
+ /*withSeconds=*/ false, /*collapseTimeUnit=*/ false);
+ final int resourceId =
+ isBackground
+ ? R.string.battery_usage_for_background_time
+ : R.string.battery_usage_for_total_time;
+ return mPrefContext.getString(resourceId, timeSequence);
+ }
+
+ @VisibleForTesting
+ boolean isValidToShowSummary(String packageName) {
+ return !contains(packageName, mNotAllowShowSummaryPackages);
+ }
+
+ @VisibleForTesting
+ boolean isValidToShowEntry(String packageName) {
+ return !contains(packageName, mNotAllowShowEntryPackages);
+ }
+
+ @VisibleForTesting
+ void setTimestampLabel() {
+ if (mBatteryChartView == null || mBatteryHistoryKeys == null) {
+ return;
+ }
+ final long latestTimestamp =
+ mBatteryHistoryKeys[mBatteryHistoryKeys.length - 1];
+ mBatteryChartView.setLatestTimestamp(latestTimestamp);
+ }
+
+ private void addFooterPreferenceIfNeeded(boolean containAppItems) {
+ if (mIsFooterPrefAdded || mFooterPreference == null) {
+ return;
+ }
+ mIsFooterPrefAdded = true;
+ mFooterPreference.setTitle(mPrefContext.getString(
+ containAppItems
+ ? R.string.battery_usage_screen_footer
+ : R.string.battery_usage_screen_footer_empty));
+ mHandler.post(() -> mPreferenceScreen.addPreference(mFooterPreference));
+ }
+
+ private static boolean contains(String target, CharSequence[] packageNames) {
+ if (target != null && packageNames != null) {
+ for (CharSequence packageName : packageNames) {
+ if (TextUtils.equals(target, packageName)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ @VisibleForTesting
+ static boolean validateUsageTime(BatteryDiffEntry entry) {
+ final long foregroundUsageTimeInMs = entry.mForegroundUsageTimeInMs;
+ final long backgroundUsageTimeInMs = entry.mBackgroundUsageTimeInMs;
+ final long totalUsageTimeInMs = foregroundUsageTimeInMs + backgroundUsageTimeInMs;
+ if (foregroundUsageTimeInMs > VALID_USAGE_TIME_DURATION
+ || backgroundUsageTimeInMs > VALID_USAGE_TIME_DURATION
+ || totalUsageTimeInMs > VALID_USAGE_TIME_DURATION) {
+ Log.e(TAG, "validateUsageTime() fail for\n" + entry);
+ return false;
+ }
+ return true;
+ }
+
+ /** Used for {@link AppBatteryPreferenceController}. */
+ public static List<BatteryDiffEntry> getBatteryLast24HrUsageData(Context context) {
+ final long start = System.currentTimeMillis();
+ final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap =
+ FeatureFactory.getFactory(context)
+ .getPowerUsageFeatureProvider(context)
+ .getBatteryHistory(context);
+ if (batteryHistoryMap == null || batteryHistoryMap.isEmpty()) {
+ return null;
+ }
+ Log.d(TAG, String.format("getBatteryLast24HrData() size=%d time=%d/ms",
+ batteryHistoryMap.size(), (System.currentTimeMillis() - start)));
+ final Map<Integer, List<BatteryDiffEntry>> batteryIndexedMap =
+ ConvertUtils.getIndexedUsageMap(
+ context,
+ /*timeSlotSize=*/ CHART_LEVEL_ARRAY_SIZE - 1,
+ getBatteryHistoryKeys(batteryHistoryMap),
+ batteryHistoryMap,
+ /*purgeLowPercentageAndFakeData=*/ true);
+ return batteryIndexedMap.get(BatteryChartViewV2.SELECTED_INDEX_ALL);
+ }
+
+ /** Used for {@link AppBatteryPreferenceController}. */
+ public static BatteryDiffEntry getBatteryLast24HrUsageData(
+ Context context, String packageName, int userId) {
+ if (packageName == null) {
+ return null;
+ }
+ final List<BatteryDiffEntry> entries = getBatteryLast24HrUsageData(context);
+ if (entries == null) {
+ return null;
+ }
+ for (BatteryDiffEntry entry : entries) {
+ final BatteryHistEntry batteryHistEntry = entry.mBatteryHistEntry;
+ if (batteryHistEntry != null
+ && batteryHistEntry.mConsumerType == ConvertUtils.CONSUMER_TYPE_UID_BATTERY
+ && batteryHistEntry.mUserId == userId
+ && packageName.equals(entry.getPackageName())) {
+ return entry;
+ }
+ }
+ 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();
+ });
+ }
+ }
+}
diff --git a/src/com/android/settings/fuelgauge/batteryusage/BatteryChartViewV2.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryChartViewV2.java
new file mode 100644
index 0000000..7c55c40
--- /dev/null
+++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryChartViewV2.java
@@ -0,0 +1,633 @@
+/*
+ * 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 static com.android.settings.Utils.formatPercentage;
+
+import static java.lang.Math.round;
+
+import android.accessibilityservice.AccessibilityServiceInfo;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.CornerPathEffect;
+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;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.accessibility.AccessibilityManager;
+import android.widget.TextView;
+
+import androidx.annotation.VisibleForTesting;
+import androidx.appcompat.widget.AppCompatImageView;
+
+import com.android.settings.R;
+import com.android.settings.overlay.FeatureFactory;
+import com.android.settingslib.Utils;
+
+import java.time.Clock;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+
+/** A widget component to draw chart graph. */
+public class BatteryChartViewV2 extends AppCompatImageView implements View.OnClickListener,
+ AccessibilityManager.AccessibilityStateChangeListener {
+ private static final String TAG = "BatteryChartViewV2";
+ 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 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;
+
+ // Colors for drawing the trapezoid shape and dividers.
+ private int mTrapezoidColor;
+ private int mTrapezoidSolidColor;
+ private int mTrapezoidHoverColor;
+ // For drawing the percentage information.
+ private int mTextPadding;
+ 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()};
+
+ @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;
+ // Records the location to calculate selected index.
+ private float mTouchUpEventX = Float.MIN_VALUE;
+ private BatteryChartViewV2.OnSelectListener mOnSelectListener;
+
+ public BatteryChartViewV2(Context context) {
+ super(context, null);
+ }
+
+ public BatteryChartViewV2(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ initializeColors(context);
+ // Registers the click event listener.
+ setOnClickListener(this);
+ setSelectedIndex(SELECTED_INDEX_ALL);
+ setTrapezoidCount(DEFAULT_TRAPEZOID_COUNT);
+ setClickable(false);
+ setLatestTimestamp(0);
+ }
+
+ /** 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;
+ invalidate();
+ // Callbacks to the listener if we have.
+ if (mOnSelectListener != null) {
+ mOnSelectListener.onSelect(mSelectedIndex);
+ }
+ }
+ }
+
+ /** Sets the callback to monitor the selected group index. */
+ public void setOnSelectListener(BatteryChartViewV2.OnSelectListener listener) {
+ mOnSelectListener = listener;
+ }
+
+ /** Sets the companion {@link TextView} for percentage information. */
+ public void setCompanionTextView(TextView textView) {
+ if (textView != null) {
+ // Pre-draws the view first to load style atttributions into paint.
+ textView.draw(new Canvas());
+ mTextPaint = textView.getPaint();
+ } 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();
+ }
+
+ @Override
+ public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ // Measures text bounds and updates indent configuration.
+ if (mTextPaint != null) {
+ for (int index = 0; index < mPercentages.length; index++) {
+ mTextPaint.getTextBounds(
+ mPercentages[index], 0, mPercentages[index].length(),
+ mPercentageBounds[index]);
+ }
+ // Updates the indent configurations.
+ 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());
+ }
+ mIndent.bottom = maxHeight + round(mTextPadding * 1.5f);
+ }
+ Log.d(TAG, "setIndent:" + mPercentageBounds[0]);
+ } else {
+ mIndent.set(0, 0, 0, 0);
+ }
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ super.draw(canvas);
+ drawHorizontalDividers(canvas);
+ drawVerticalDividers(canvas);
+ drawTrapezoids(canvas);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ // Caches the location to calculate selected trapezoid index.
+ final int action = event.getAction();
+ switch (action) {
+ case MotionEvent.ACTION_UP:
+ mTouchUpEventX = event.getX();
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ mTouchUpEventX = Float.MIN_VALUE; // reset
+ break;
+ }
+ return super.onTouchEvent(event);
+ }
+
+ @Override
+ public boolean onHoverEvent(MotionEvent event) {
+ final int action = event.getAction();
+ switch (action) {
+ case MotionEvent.ACTION_HOVER_ENTER:
+ case MotionEvent.ACTION_HOVER_MOVE:
+ final int trapezoidIndex = getTrapezoidIndex(event.getX());
+ if (mHoveredIndex != trapezoidIndex) {
+ mHoveredIndex = trapezoidIndex;
+ invalidate();
+ }
+ break;
+ }
+ return super.onHoverEvent(event);
+ }
+
+ @Override
+ public void onHoverChanged(boolean hovered) {
+ super.onHoverChanged(hovered);
+ if (!hovered) {
+ mHoveredIndex = SELECTED_INDEX_INVALID; // reset
+ invalidate();
+ }
+ }
+
+ @Override
+ public void onClick(View view) {
+ if (mTouchUpEventX == Float.MIN_VALUE) {
+ Log.w(TAG, "invalid motion event for onClick() callback");
+ return;
+ }
+ final int trapezoidIndex = getTrapezoidIndex(mTouchUpEventX);
+ // Ignores the click event if the level is zero.
+ if (trapezoidIndex == SELECTED_INDEX_INVALID
+ || !isValidToDraw(trapezoidIndex)) {
+ return;
+ }
+ // Selects all if users click the same trapezoid item two times.
+ if (trapezoidIndex == mSelectedIndex) {
+ setSelectedIndex(SELECTED_INDEX_ALL);
+ } else {
+ setSelectedIndex(trapezoidIndex);
+ }
+ view.performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK);
+ }
+
+ @Override
+ public void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ updateClickableState();
+ mContext.getSystemService(AccessibilityManager.class)
+ .addAccessibilityStateChangeListener(/*listener=*/ this);
+ }
+
+ @Override
+ public void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ mContext.getSystemService(AccessibilityManager.class)
+ .removeAccessibilityStateChangeListener(/*listener=*/ this);
+ mHandler.removeCallbacks(mUpdateClickableStateRun);
+ }
+
+ @Override
+ public void onAccessibilityStateChanged(boolean enabled) {
+ Log.d(TAG, "onAccessibilityStateChanged:" + enabled);
+ mHandler.removeCallbacks(mUpdateClickableStateRun);
+ // We should delay it a while since accessibility manager will spend
+ // some times to bind with new enabled accessibility services.
+ mHandler.postDelayed(
+ mUpdateClickableStateRun, UPDATE_STATE_DELAYED_TIME);
+ }
+
+ private void updateClickableState() {
+ final Context context = mContext;
+ mIsSlotsClickabled =
+ FeatureFactory.getFactory(context)
+ .getPowerUsageFeatureProvider(context)
+ .isChartGraphSlotsEnabled(context)
+ && !isAccessibilityEnabled(context);
+ Log.d(TAG, "isChartGraphSlotsEnabled:" + mIsSlotsClickabled);
+ setClickable(isClickable());
+ // Initializes the trapezoid curve paint for non-clickable case.
+ if (!mIsSlotsClickabled && mTrapezoidCurvePaint == null) {
+ mTrapezoidCurvePaint = new Paint();
+ mTrapezoidCurvePaint.setAntiAlias(true);
+ mTrapezoidCurvePaint.setColor(mTrapezoidSolidColor);
+ mTrapezoidCurvePaint.setStyle(Paint.Style.STROKE);
+ mTrapezoidCurvePaint.setStrokeWidth(mDividerWidth * 2);
+ } else if (mIsSlotsClickabled) {
+ mTrapezoidCurvePaint = null;
+ // Sets levels again to force update the click state.
+ setLevels(mLevels);
+ }
+ invalidate();
+ }
+
+ @Override
+ public void setClickable(boolean clickable) {
+ super.setClickable(mIsSlotsClickabled && clickable);
+ }
+
+ @VisibleForTesting
+ void setClickableForce(boolean clickable) {
+ super.setClickable(clickable);
+ }
+
+ private void initializeColors(Context context) {
+ setBackgroundColor(Color.TRANSPARENT);
+ mTrapezoidSolidColor = Utils.getColorAccentDefaultColor(context);
+ mTrapezoidColor = Utils.getDisabled(context, mTrapezoidSolidColor);
+ mTrapezoidHoverColor = Utils.getColorAttrDefaultColor(context,
+ com.android.internal.R.attr.colorAccentSecondaryVariant);
+ // Initializes the divider line paint.
+ final Resources resources = getContext().getResources();
+ mDividerWidth = resources.getDimensionPixelSize(R.dimen.chartview_divider_width);
+ mDividerHeight = resources.getDimensionPixelSize(R.dimen.chartview_divider_height);
+ mDividerPaint = new Paint();
+ mDividerPaint.setAntiAlias(true);
+ mDividerPaint.setColor(DIVIDER_COLOR);
+ mDividerPaint.setStyle(Paint.Style.STROKE);
+ mDividerPaint.setStrokeWidth(mDividerWidth);
+ Log.i(TAG, "mDividerWidth:" + mDividerWidth);
+ Log.i(TAG, "mDividerHeight:" + mDividerHeight);
+ // Initializes the trapezoid paint.
+ mTrapezoidHOffset = resources.getDimension(R.dimen.chartview_trapezoid_margin_start);
+ mTrapezoidVOffset = resources.getDimension(R.dimen.chartview_trapezoid_margin_bottom);
+ mTrapezoidPaint = new Paint();
+ mTrapezoidPaint.setAntiAlias(true);
+ mTrapezoidPaint.setColor(mTrapezoidSolidColor);
+ mTrapezoidPaint.setStyle(Paint.Style.FILL);
+ mTrapezoidPaint.setPathEffect(
+ new CornerPathEffect(
+ resources.getDimensionPixelSize(R.dimen.chartview_trapezoid_radius)));
+ // Initializes for drawing text information.
+ mTextPadding = resources.getDimensionPixelSize(R.dimen.chartview_text_padding);
+ }
+
+ private void drawHorizontalDividers(Canvas canvas) {
+ final int width = getWidth() - mIndent.right;
+ final int height = getHeight() - mIndent.top - mIndent.bottom;
+ // Draws the top divider line for 100% curve.
+ float offsetY = mIndent.top + mDividerWidth * .5f;
+ canvas.drawLine(0, offsetY, width, offsetY, mDividerPaint);
+ drawPercentage(canvas, /*index=*/ 0, offsetY);
+
+ // Draws the center divider line for 50% curve.
+ final float availableSpace =
+ height - mDividerWidth * 2 - mTrapezoidVOffset - mDividerHeight;
+ offsetY = mIndent.top + mDividerWidth + availableSpace * .5f;
+ canvas.drawLine(0, offsetY, width, offsetY, mDividerPaint);
+ drawPercentage(canvas, /*index=*/ 1, offsetY);
+
+ // Draws the bottom divider line for 0% curve.
+ offsetY = mIndent.top + (height - mDividerHeight - mDividerWidth * .5f);
+ canvas.drawLine(0, offsetY, width, offsetY, mDividerPaint);
+ drawPercentage(canvas, /*index=*/ 2, offsetY);
+ }
+
+ private void drawPercentage(Canvas canvas, int index, float offsetY) {
+ if (mTextPaint != null) {
+ canvas.drawText(
+ mPercentages[index],
+ getWidth() - mPercentageBounds[index].width()
+ - mPercentageBounds[index].left,
+ offsetY + mPercentageBounds[index].height() * .5f,
+ mTextPaint);
+ }
+ }
+
+ private void drawVerticalDividers(Canvas canvas) {
+ final int width = getWidth() - mIndent.right;
+ final int dividerCount = mTrapezoidCount + 1;
+ final float dividerSpace = dividerCount * mDividerWidth;
+ final float unitWidth = (width - dividerSpace) / (float) mTrapezoidCount;
+ final float bottomY = getHeight() - mIndent.bottom;
+ final float startY = bottomY - mDividerHeight;
+ final float trapezoidSlotOffset = mTrapezoidHOffset + mDividerWidth * .5f;
+ // Draws each vertical dividers.
+ float startX = mDividerWidth * .5f;
+ for (int index = 0; index < dividerCount; index++) {
+ canvas.drawLine(startX, startY, startX, bottomY, mDividerPaint);
+ final float nextX = startX + mDividerWidth + unitWidth;
+ // Updates the trapezoid slots for drawing.
+ if (index < mTrapezoidSlots.length) {
+ mTrapezoidSlots[index].mLeft = round(startX + trapezoidSlotOffset);
+ mTrapezoidSlots[index].mRight = round(nextX - trapezoidSlotOffset);
+ }
+ 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;
+ }
+ drawTimestamp(canvas, xOffsets);
+ }
+ }
+
+ 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);
+
+ }
+ }
+
+ private int getTimestampY(int index) {
+ return getHeight() - mTimestampsBounds[index].height()
+ + (mTimestampsBounds[index].height() + mTimestampsBounds[index].top)
+ + round(mTextPadding * 1.5f);
+ }
+
+ private void drawTrapezoids(Canvas canvas) {
+ // Ignores invalid trapezoid data.
+ if (mLevels == null) {
+ return;
+ }
+ final float trapezoidBottom =
+ getHeight() - mIndent.bottom - mDividerHeight - mDividerWidth
+ - mTrapezoidVOffset;
+ final float availableSpace = trapezoidBottom - mDividerWidth * .5f - mIndent.top;
+ 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++) {
+ // Not draws the trapezoid for corner or not initialization cases.
+ if (!isValidToDraw(index)) {
+ if (mTrapezoidCurvePaint != null && trapezoidCurvePath != null) {
+ canvas.drawPath(trapezoidCurvePath, mTrapezoidCurvePaint);
+ trapezoidCurvePath = null;
+ }
+ continue;
+ }
+ // Configures the trapezoid paint color.
+ final int trapezoidColor =
+ !mIsSlotsClickabled
+ ? mTrapezoidColor
+ : mSelectedIndex == index || mSelectedIndex == SELECTED_INDEX_ALL
+ ? mTrapezoidSolidColor : mTrapezoidColor;
+ final boolean isHoverState =
+ mIsSlotsClickabled && mHoveredIndex == index && isValidToDraw(mHoveredIndex);
+ mTrapezoidPaint.setColor(isHoverState ? mTrapezoidHoverColor : trapezoidColor);
+
+ final float leftTop = round(trapezoidBottom - mLevels[index] * unitHeight);
+ final float rightTop = round(trapezoidBottom - mLevels[index + 1] * unitHeight);
+ trapezoidPath.reset();
+ trapezoidPath.moveTo(mTrapezoidSlots[index].mLeft, trapezoidBottom);
+ trapezoidPath.lineTo(mTrapezoidSlots[index].mLeft, leftTop);
+ trapezoidPath.lineTo(mTrapezoidSlots[index].mRight, rightTop);
+ trapezoidPath.lineTo(mTrapezoidSlots[index].mRight, trapezoidBottom);
+ // A tricky way to make the trapezoid shape drawing the rounded corner.
+ trapezoidPath.lineTo(mTrapezoidSlots[index].mLeft, trapezoidBottom);
+ trapezoidPath.lineTo(mTrapezoidSlots[index].mLeft, leftTop);
+ // Draws the trapezoid shape into canvas.
+ canvas.drawPath(trapezoidPath, mTrapezoidPaint);
+
+ // Generates path for non-clickable trapezoid curve.
+ if (mTrapezoidCurvePaint != null) {
+ if (trapezoidCurvePath == null) {
+ trapezoidCurvePath = new Path();
+ trapezoidCurvePath.moveTo(mTrapezoidSlots[index].mLeft, leftTop);
+ } else {
+ trapezoidCurvePath.lineTo(mTrapezoidSlots[index].mLeft, leftTop);
+ }
+ trapezoidCurvePath.lineTo(mTrapezoidSlots[index].mRight, rightTop);
+ }
+ }
+ // Draws the trapezoid curve for non-clickable case.
+ if (mTrapezoidCurvePaint != null && trapezoidCurvePath != null) {
+ canvas.drawPath(trapezoidCurvePath, mTrapezoidCurvePaint);
+ trapezoidCurvePath = null;
+ }
+ }
+
+ // Searches the corresponding trapezoid index from x location.
+ private int getTrapezoidIndex(float x) {
+ for (int index = 0; index < mTrapezoidSlots.length; index++) {
+ final TrapezoidSlot slot = mTrapezoidSlots[index];
+ if (x >= slot.mLeft - mTrapezoidHOffset
+ && x <= slot.mRight + mTrapezoidHOffset) {
+ return index;
+ }
+ }
+ return SELECTED_INDEX_INVALID;
+ }
+
+ private boolean isValidToDraw(int trapezoidIndex) {
+ return mLevels != null
+ && trapezoidIndex >= 0
+ && trapezoidIndex < mLevels.length - 1
+ && mLevels[trapezoidIndex] != 0
+ && mLevels[trapezoidIndex + 1] != 0;
+ }
+
+ private static String[] getPercentages() {
+ return new String[]{
+ formatPercentage(/*percentage=*/ 100, /*round=*/ true),
+ formatPercentage(/*percentage=*/ 50, /*round=*/ true),
+ formatPercentage(/*percentage=*/ 0, /*round=*/ true)};
+ }
+
+ @VisibleForTesting
+ static boolean isAccessibilityEnabled(Context context) {
+ final AccessibilityManager accessibilityManager =
+ context.getSystemService(AccessibilityManager.class);
+ if (!accessibilityManager.isEnabled()) {
+ return false;
+ }
+ final List<AccessibilityServiceInfo> serviceInfoList =
+ accessibilityManager.getEnabledAccessibilityServiceList(
+ AccessibilityServiceInfo.FEEDBACK_SPOKEN
+ | AccessibilityServiceInfo.FEEDBACK_GENERIC);
+ for (AccessibilityServiceInfo info : serviceInfoList) {
+ for (String serviceName : ACCESSIBILITY_SERVICE_NAMES) {
+ final String serviceId = info.getId();
+ if (serviceId != null && serviceId.contains(serviceName)) {
+ Log.d(TAG, "acccessibilityEnabled:" + serviceId);
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ // A container class for each trapezoid left and right location.
+ private static final class TrapezoidSlot {
+ public float mLeft;
+ public float mRight;
+
+ @Override
+ public String toString() {
+ return String.format(Locale.US, "TrapezoidSlot[%f,%f]", mLeft, mRight);
+ }
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceControllerV2Test.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceControllerV2Test.java
new file mode 100644
index 0000000..e9d6268
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceControllerV2Test.java
@@ -0,0 +1,700 @@
+/*
+ * 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 static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Matchers.anyLong;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
+import android.app.settings.SettingsEnums;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.os.LocaleList;
+import android.text.format.DateUtils;
+
+import androidx.preference.Preference;
+import androidx.preference.PreferenceCategory;
+import androidx.preference.PreferenceGroup;
+
+import com.android.settings.SettingsActivity;
+import com.android.settings.core.InstrumentedPreferenceFragment;
+import com.android.settings.fuelgauge.BatteryUtils;
+import com.android.settings.testutils.FakeFeatureFactory;
+import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+
+@RunWith(RobolectricTestRunner.class)
+public final class BatteryChartPreferenceControllerV2Test {
+ private static final String PREF_KEY = "pref_key";
+ private static final String PREF_SUMMARY = "fake preference summary";
+ private static final int DESIRED_HISTORY_SIZE =
+ BatteryChartPreferenceControllerV2.DESIRED_HISTORY_SIZE;
+
+ @Mock
+ private InstrumentedPreferenceFragment mFragment;
+ @Mock
+ private SettingsActivity mSettingsActivity;
+ @Mock
+ private PreferenceGroup mAppListGroup;
+ @Mock
+ private Drawable mDrawable;
+ @Mock
+ private BatteryHistEntry mBatteryHistEntry;
+ @Mock
+ private BatteryChartViewV2 mBatteryChartView;
+ @Mock
+ private PowerGaugePreference mPowerGaugePreference;
+ @Mock
+ private BatteryUtils mBatteryUtils;
+
+ private Context mContext;
+ private FakeFeatureFactory mFeatureFactory;
+ private BatteryDiffEntry mBatteryDiffEntry;
+ private MetricsFeatureProvider mMetricsFeatureProvider;
+ private BatteryChartPreferenceControllerV2 mBatteryChartPreferenceController;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ Locale.setDefault(new Locale("en_US"));
+ org.robolectric.shadows.ShadowSettings.set24HourTimeFormat(false);
+ mFeatureFactory = FakeFeatureFactory.setupForTest();
+ mMetricsFeatureProvider = mFeatureFactory.metricsFeatureProvider;
+ mContext = spy(RuntimeEnvironment.application);
+ final Resources resources = spy(mContext.getResources());
+ resources.getConfiguration().setLocales(new LocaleList(new Locale("en_US")));
+ doReturn(resources).when(mContext).getResources();
+ doReturn(new String[]{"com.android.googlequicksearchbox"})
+ .when(mFeatureFactory.powerUsageFeatureProvider)
+ .getHideApplicationSummary(mContext);
+ doReturn(new String[]{"com.android.gms.persistent"})
+ .when(mFeatureFactory.powerUsageFeatureProvider)
+ .getHideApplicationEntries(mContext);
+ mBatteryChartPreferenceController = createController();
+ mBatteryChartPreferenceController.mPrefContext = mContext;
+ mBatteryChartPreferenceController.mAppListPrefGroup = mAppListGroup;
+ mBatteryChartPreferenceController.mBatteryChartView = mBatteryChartView;
+ mBatteryDiffEntry = new BatteryDiffEntry(
+ mContext,
+ /*foregroundUsageTimeInMs=*/ 1,
+ /*backgroundUsageTimeInMs=*/ 2,
+ /*consumePower=*/ 3,
+ mBatteryHistEntry);
+ mBatteryDiffEntry = spy(mBatteryDiffEntry);
+ // Adds fake testing data.
+ BatteryDiffEntry.sResourceCache.put(
+ "fakeBatteryDiffEntryKey",
+ new BatteryEntry.NameAndIcon("fakeName", /*icon=*/ null, /*iconId=*/ 1));
+ mBatteryChartPreferenceController.setBatteryHistoryMap(
+ createBatteryHistoryMap());
+ }
+
+ @Test
+ public void testOnDestroy_activityIsChanging_clearBatteryEntryCache() {
+ doReturn(true).when(mSettingsActivity).isChangingConfigurations();
+ // Ensures the testing environment is correct.
+ assertThat(BatteryDiffEntry.sResourceCache).hasSize(1);
+
+ mBatteryChartPreferenceController.onDestroy();
+ assertThat(BatteryDiffEntry.sResourceCache).isEmpty();
+ }
+
+ @Test
+ public void testOnDestroy_activityIsNotChanging_notClearBatteryEntryCache() {
+ doReturn(false).when(mSettingsActivity).isChangingConfigurations();
+ // Ensures the testing environment is correct.
+ assertThat(BatteryDiffEntry.sResourceCache).hasSize(1);
+
+ mBatteryChartPreferenceController.onDestroy();
+ assertThat(BatteryDiffEntry.sResourceCache).isNotEmpty();
+ }
+
+ @Test
+ public void testOnDestroy_clearPreferenceCache() {
+ // Ensures the testing environment is correct.
+ mBatteryChartPreferenceController.mPreferenceCache.put(
+ PREF_KEY, mPowerGaugePreference);
+ assertThat(mBatteryChartPreferenceController.mPreferenceCache).hasSize(1);
+
+ mBatteryChartPreferenceController.onDestroy();
+ // Verifies the result after onDestroy.
+ assertThat(mBatteryChartPreferenceController.mPreferenceCache).isEmpty();
+ }
+
+ @Test
+ public void testOnDestroy_removeAllPreferenceFromPreferenceGroup() {
+ mBatteryChartPreferenceController.onDestroy();
+ verify(mAppListGroup).removeAll();
+ }
+
+ @Test
+ public void testSetBatteryHistoryMap_createExpectedKeysAndLevels() {
+ mBatteryChartPreferenceController.setBatteryHistoryMap(
+ createBatteryHistoryMap());
+
+ // Verifies the created battery keys array.
+ for (int index = 0; index < DESIRED_HISTORY_SIZE; index++) {
+ assertThat(mBatteryChartPreferenceController.mBatteryHistoryKeys[index])
+ // These values is are calculated by hand from createBatteryHistoryMap().
+ .isEqualTo(index + 1);
+ }
+ // Verifies the created battery levels array.
+ for (int index = 0; index < 13; index++) {
+ assertThat(mBatteryChartPreferenceController.mBatteryHistoryLevels[index])
+ // These values is are calculated by hand from createBatteryHistoryMap().
+ .isEqualTo(100 - index * 2);
+ }
+ assertThat(mBatteryChartPreferenceController.mBatteryIndexedMap).hasSize(13);
+ }
+
+ @Test
+ public void testSetBatteryHistoryMap_largeSize_createExpectedKeysAndLevels() {
+ mBatteryChartPreferenceController.setBatteryHistoryMap(
+ createBatteryHistoryMap());
+
+ // Verifies the created battery keys array.
+ for (int index = 0; index < DESIRED_HISTORY_SIZE; index++) {
+ assertThat(mBatteryChartPreferenceController.mBatteryHistoryKeys[index])
+ // These values is are calculated by hand from createBatteryHistoryMap().
+ .isEqualTo(index + 1);
+ }
+ // Verifies the created battery levels array.
+ for (int index = 0; index < 13; index++) {
+ assertThat(mBatteryChartPreferenceController.mBatteryHistoryLevels[index])
+ // These values is are calculated by hand from createBatteryHistoryMap().
+ .isEqualTo(100 - index * 2);
+ }
+ assertThat(mBatteryChartPreferenceController.mBatteryIndexedMap).hasSize(13);
+ }
+
+ @Test
+ public void testRefreshUi_batteryIndexedMapIsNull_ignoreRefresh() {
+ mBatteryChartPreferenceController.setBatteryHistoryMap(null);
+ assertThat(mBatteryChartPreferenceController.refreshUi(
+ /*trapezoidIndex=*/ 1, /*isForce=*/ false)).isFalse();
+ }
+
+ @Test
+ public void testRefreshUi_batteryChartViewIsNull_ignoreRefresh() {
+ mBatteryChartPreferenceController.mBatteryChartView = null;
+ assertThat(mBatteryChartPreferenceController.refreshUi(
+ /*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
+ public void testRemoveAndCacheAllPrefs_emptyContent_ignoreRemoveAll() {
+ final int trapezoidIndex = 1;
+ doReturn(0).when(mAppListGroup).getPreferenceCount();
+
+ mBatteryChartPreferenceController.refreshUi(
+ trapezoidIndex, /*isForce=*/ true);
+ verify(mAppListGroup, never()).removeAll();
+ }
+
+ @Test
+ public void testRemoveAndCacheAllPrefs_buildCacheAndRemoveAllPreference() {
+ final int trapezoidIndex = 1;
+ doReturn(1).when(mAppListGroup).getPreferenceCount();
+ doReturn(mPowerGaugePreference).when(mAppListGroup).getPreference(0);
+ doReturn(PREF_KEY).when(mPowerGaugePreference).getKey();
+ // Ensures the testing data is correct.
+ assertThat(mBatteryChartPreferenceController.mPreferenceCache).isEmpty();
+
+ mBatteryChartPreferenceController.refreshUi(
+ trapezoidIndex, /*isForce=*/ true);
+
+ assertThat(mBatteryChartPreferenceController.mPreferenceCache.get(PREF_KEY))
+ .isEqualTo(mPowerGaugePreference);
+ verify(mAppListGroup).removeAll();
+ }
+
+ @Test
+ public void testAddPreferenceToScreen_emptyContent_ignoreAddPreference() {
+ mBatteryChartPreferenceController.addPreferenceToScreen(
+ new ArrayList<BatteryDiffEntry>());
+ verify(mAppListGroup, never()).addPreference(any());
+ }
+
+ @Test
+ public void testAddPreferenceToScreen_addPreferenceIntoScreen() {
+ final String appLabel = "fake app label";
+ doReturn(1).when(mAppListGroup).getPreferenceCount();
+ doReturn(mDrawable).when(mBatteryDiffEntry).getAppIcon();
+ doReturn(appLabel).when(mBatteryDiffEntry).getAppLabel();
+ doReturn(PREF_KEY).when(mBatteryHistEntry).getKey();
+ doReturn(null).when(mAppListGroup).findPreference(PREF_KEY);
+ doReturn(false).when(mBatteryDiffEntry).validForRestriction();
+
+ mBatteryChartPreferenceController.addPreferenceToScreen(
+ Arrays.asList(mBatteryDiffEntry));
+
+ // Verifies the preference cache.
+ final PowerGaugePreference pref =
+ (PowerGaugePreference) mBatteryChartPreferenceController.mPreferenceCache
+ .get(PREF_KEY);
+ assertThat(pref).isNotNull();
+ // Verifies the added preference configuration.
+ verify(mAppListGroup).addPreference(pref);
+ assertThat(pref.getKey()).isEqualTo(PREF_KEY);
+ assertThat(pref.getTitle()).isEqualTo(appLabel);
+ assertThat(pref.getIcon()).isEqualTo(mDrawable);
+ assertThat(pref.getOrder()).isEqualTo(1);
+ assertThat(pref.getBatteryDiffEntry()).isSameInstanceAs(mBatteryDiffEntry);
+ assertThat(pref.isSingleLineTitle()).isTrue();
+ assertThat(pref.isEnabled()).isFalse();
+ }
+
+ @Test
+ public void testAddPreferenceToScreen_alreadyInScreen_notAddPreferenceAgain() {
+ final String appLabel = "fake app label";
+ doReturn(1).when(mAppListGroup).getPreferenceCount();
+ doReturn(mDrawable).when(mBatteryDiffEntry).getAppIcon();
+ doReturn(appLabel).when(mBatteryDiffEntry).getAppLabel();
+ doReturn(PREF_KEY).when(mBatteryHistEntry).getKey();
+ doReturn(mPowerGaugePreference).when(mAppListGroup).findPreference(PREF_KEY);
+
+ mBatteryChartPreferenceController.addPreferenceToScreen(
+ Arrays.asList(mBatteryDiffEntry));
+
+ verify(mAppListGroup, never()).addPreference(any());
+ }
+
+ @Test
+ public void testHandlePreferenceTreeiClick_notPowerGaugePreference_returnFalse() {
+ assertThat(mBatteryChartPreferenceController.handlePreferenceTreeClick(mAppListGroup))
+ .isFalse();
+
+ verify(mMetricsFeatureProvider, never())
+ .action(mContext, SettingsEnums.ACTION_BATTERY_USAGE_APP_ITEM);
+ verify(mMetricsFeatureProvider, never())
+ .action(mContext, SettingsEnums.ACTION_BATTERY_USAGE_SYSTEM_ITEM);
+ }
+
+ @Test
+ public void testHandlePreferenceTreeClick_forAppEntry_returnTrue() {
+ doReturn(false).when(mBatteryHistEntry).isAppEntry();
+ doReturn(mBatteryDiffEntry).when(mPowerGaugePreference).getBatteryDiffEntry();
+
+ assertThat(mBatteryChartPreferenceController.handlePreferenceTreeClick(
+ mPowerGaugePreference)).isTrue();
+ verify(mMetricsFeatureProvider)
+ .action(
+ SettingsEnums.OPEN_BATTERY_USAGE,
+ SettingsEnums.ACTION_BATTERY_USAGE_SYSTEM_ITEM,
+ SettingsEnums.OPEN_BATTERY_USAGE,
+ /* package name */ "none",
+ /* percentage of total */ 0);
+ }
+
+ @Test
+ public void testHandlePreferenceTreeClick_forSystemEntry_returnTrue() {
+ mBatteryChartPreferenceController.mBatteryUtils = mBatteryUtils;
+ doReturn(true).when(mBatteryHistEntry).isAppEntry();
+ doReturn(mBatteryDiffEntry).when(mPowerGaugePreference).getBatteryDiffEntry();
+
+ assertThat(mBatteryChartPreferenceController.handlePreferenceTreeClick(
+ mPowerGaugePreference)).isTrue();
+ verify(mMetricsFeatureProvider)
+ .action(
+ SettingsEnums.OPEN_BATTERY_USAGE,
+ SettingsEnums.ACTION_BATTERY_USAGE_APP_ITEM,
+ SettingsEnums.OPEN_BATTERY_USAGE,
+ /* package name */ "none",
+ /* percentage of total */ 0);
+ }
+
+ @Test
+ public void testSetPreferenceSummary_setNullContentIfTotalUsageTimeIsZero() {
+ final PowerGaugePreference pref = new PowerGaugePreference(mContext);
+ pref.setSummary(PREF_SUMMARY);
+
+ mBatteryChartPreferenceController.setPreferenceSummary(
+ pref, createBatteryDiffEntry(
+ /*foregroundUsageTimeInMs=*/ 0,
+ /*backgroundUsageTimeInMs=*/ 0));
+ assertThat(pref.getSummary()).isNull();
+ }
+
+ @Test
+ public void testSetPreferenceSummary_setBackgroundUsageTimeOnly() {
+ final PowerGaugePreference pref = new PowerGaugePreference(mContext);
+ pref.setSummary(PREF_SUMMARY);
+
+ mBatteryChartPreferenceController.setPreferenceSummary(
+ pref, createBatteryDiffEntry(
+ /*foregroundUsageTimeInMs=*/ 0,
+ /*backgroundUsageTimeInMs=*/ DateUtils.MINUTE_IN_MILLIS));
+ assertThat(pref.getSummary()).isEqualTo("Background: 1 min");
+ }
+
+ @Test
+ public void testSetPreferenceSummary_setTotalUsageTimeLessThanAMinute() {
+ final PowerGaugePreference pref = new PowerGaugePreference(mContext);
+ pref.setSummary(PREF_SUMMARY);
+
+ mBatteryChartPreferenceController.setPreferenceSummary(
+ pref, createBatteryDiffEntry(
+ /*foregroundUsageTimeInMs=*/ 100,
+ /*backgroundUsageTimeInMs=*/ 200));
+ assertThat(pref.getSummary()).isEqualTo("Total: less than a min");
+ }
+
+ @Test
+ public void testSetPreferenceSummary_setTotalTimeIfBackgroundTimeLessThanAMinute() {
+ final PowerGaugePreference pref = new PowerGaugePreference(mContext);
+ pref.setSummary(PREF_SUMMARY);
+
+ mBatteryChartPreferenceController.setPreferenceSummary(
+ pref, createBatteryDiffEntry(
+ /*foregroundUsageTimeInMs=*/ DateUtils.MINUTE_IN_MILLIS,
+ /*backgroundUsageTimeInMs=*/ 200));
+ assertThat(pref.getSummary())
+ .isEqualTo("Total: 1 min\nBackground: less than a min");
+ }
+
+ @Test
+ public void testSetPreferenceSummary_setTotalAndBackgroundUsageTime() {
+ final PowerGaugePreference pref = new PowerGaugePreference(mContext);
+ pref.setSummary(PREF_SUMMARY);
+
+ mBatteryChartPreferenceController.setPreferenceSummary(
+ pref, createBatteryDiffEntry(
+ /*foregroundUsageTimeInMs=*/ DateUtils.MINUTE_IN_MILLIS,
+ /*backgroundUsageTimeInMs=*/ DateUtils.MINUTE_IN_MILLIS));
+ assertThat(pref.getSummary()).isEqualTo("Total: 2 min\nBackground: 1 min");
+ }
+
+ @Test
+ public void testSetPreferenceSummary_notAllowShownPackage_setSummayAsNull() {
+ final PowerGaugePreference pref = new PowerGaugePreference(mContext);
+ pref.setSummary(PREF_SUMMARY);
+ final BatteryDiffEntry batteryDiffEntry =
+ spy(createBatteryDiffEntry(
+ /*foregroundUsageTimeInMs=*/ DateUtils.MINUTE_IN_MILLIS,
+ /*backgroundUsageTimeInMs=*/ DateUtils.MINUTE_IN_MILLIS));
+ doReturn("com.android.googlequicksearchbox").when(batteryDiffEntry)
+ .getPackageName();
+
+ mBatteryChartPreferenceController.setPreferenceSummary(pref, batteryDiffEntry);
+ assertThat(pref.getSummary()).isNull();
+ }
+
+ @Test
+ public void testValidateUsageTime_returnTrueIfBatteryDiffEntryIsValid() {
+ assertThat(BatteryChartPreferenceControllerV2.validateUsageTime(
+ createBatteryDiffEntry(
+ /*foregroundUsageTimeInMs=*/ DateUtils.MINUTE_IN_MILLIS,
+ /*backgroundUsageTimeInMs=*/ DateUtils.MINUTE_IN_MILLIS)))
+ .isTrue();
+ }
+
+ @Test
+ public void testValidateUsageTime_foregroundTimeExceedThreshold_returnFalse() {
+ assertThat(BatteryChartPreferenceControllerV2.validateUsageTime(
+ createBatteryDiffEntry(
+ /*foregroundUsageTimeInMs=*/ DateUtils.HOUR_IN_MILLIS * 3,
+ /*backgroundUsageTimeInMs=*/ 0)))
+ .isFalse();
+ }
+
+ @Test
+ public void testValidateUsageTime_backgroundTimeExceedThreshold_returnFalse() {
+ assertThat(BatteryChartPreferenceControllerV2.validateUsageTime(
+ createBatteryDiffEntry(
+ /*foregroundUsageTimeInMs=*/ 0,
+ /*backgroundUsageTimeInMs=*/ DateUtils.HOUR_IN_MILLIS * 3)))
+ .isFalse();
+ }
+
+ @Test
+ public void testOnExpand_expandedIsTrue_addSystemEntriesToPreferenceGroup() {
+ doReturn(1).when(mAppListGroup).getPreferenceCount();
+ mBatteryChartPreferenceController.mSystemEntries.add(mBatteryDiffEntry);
+ doReturn("label").when(mBatteryDiffEntry).getAppLabel();
+ doReturn(mDrawable).when(mBatteryDiffEntry).getAppIcon();
+ doReturn(PREF_KEY).when(mBatteryHistEntry).getKey();
+
+ mBatteryChartPreferenceController.onExpand(/*isExpanded=*/ true);
+
+ final ArgumentCaptor<Preference> captor = ArgumentCaptor.forClass(Preference.class);
+ verify(mAppListGroup).addPreference(captor.capture());
+ // Verifies the added preference.
+ assertThat(captor.getValue().getKey()).isEqualTo(PREF_KEY);
+ verify(mMetricsFeatureProvider)
+ .action(
+ mContext,
+ SettingsEnums.ACTION_BATTERY_USAGE_EXPAND_ITEM,
+ true /*isExpanded*/);
+ }
+
+ @Test
+ public void testOnExpand_expandedIsFalse_removeSystemEntriesFromPreferenceGroup() {
+ doReturn(PREF_KEY).when(mBatteryHistEntry).getKey();
+ doReturn(mPowerGaugePreference).when(mAppListGroup).findPreference(PREF_KEY);
+ mBatteryChartPreferenceController.mSystemEntries.add(mBatteryDiffEntry);
+ // Verifies the cache is empty first.
+ assertThat(mBatteryChartPreferenceController.mPreferenceCache).isEmpty();
+
+ mBatteryChartPreferenceController.onExpand(/*isExpanded=*/ false);
+
+ verify(mAppListGroup).findPreference(PREF_KEY);
+ verify(mAppListGroup).removePreference(mPowerGaugePreference);
+ assertThat(mBatteryChartPreferenceController.mPreferenceCache).hasSize(1);
+ verify(mMetricsFeatureProvider)
+ .action(
+ mContext,
+ SettingsEnums.ACTION_BATTERY_USAGE_EXPAND_ITEM,
+ false /*isExpanded*/);
+ }
+
+ @Test
+ public void testOnSelect_selectSpecificTimeSlot_logMetric() {
+ mBatteryChartPreferenceController.onSelect(1 /*slot index*/);
+
+ verify(mMetricsFeatureProvider)
+ .action(mContext, SettingsEnums.ACTION_BATTERY_USAGE_TIME_SLOT);
+ }
+
+ @Test
+ public void testOnSelect_selectAll_logMetric() {
+ mBatteryChartPreferenceController.onSelect(
+ BatteryChartViewV2.SELECTED_INDEX_ALL /*slot index*/);
+
+ verify(mMetricsFeatureProvider)
+ .action(mContext, SettingsEnums.ACTION_BATTERY_USAGE_SHOW_ALL);
+ }
+
+ @Test
+ public void testRefreshCategoryTitle_setHourIntoBothTitleTextView() {
+ mBatteryChartPreferenceController = createController();
+ setUpBatteryHistoryKeys();
+ mBatteryChartPreferenceController.mAppListPrefGroup =
+ spy(new PreferenceCategory(mContext));
+ mBatteryChartPreferenceController.mExpandDividerPreference =
+ spy(new ExpandDividerPreference(mContext));
+ // Simulates select the first slot.
+ mBatteryChartPreferenceController.mTrapezoidIndex = 0;
+
+ mBatteryChartPreferenceController.refreshCategoryTitle();
+
+ ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
+ // Verifies the title in the preference group.
+ verify(mBatteryChartPreferenceController.mAppListPrefGroup)
+ .setTitle(captor.capture());
+ assertThat(captor.getValue()).isNotEqualTo("App usage for past 24 hr");
+ // Verifies the title in the expandable divider.
+ captor = ArgumentCaptor.forClass(String.class);
+ verify(mBatteryChartPreferenceController.mExpandDividerPreference)
+ .setTitle(captor.capture());
+ assertThat(captor.getValue()).isNotEqualTo("System usage for past 24 hr");
+ }
+
+ @Test
+ public void testRefreshCategoryTitle_setLast24HrIntoBothTitleTextView() {
+ mBatteryChartPreferenceController = createController();
+ mBatteryChartPreferenceController.mAppListPrefGroup =
+ spy(new PreferenceCategory(mContext));
+ mBatteryChartPreferenceController.mExpandDividerPreference =
+ spy(new ExpandDividerPreference(mContext));
+ // Simulates select all condition.
+ mBatteryChartPreferenceController.mTrapezoidIndex =
+ BatteryChartViewV2.SELECTED_INDEX_ALL;
+
+ mBatteryChartPreferenceController.refreshCategoryTitle();
+
+ ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
+ // Verifies the title in the preference group.
+ verify(mBatteryChartPreferenceController.mAppListPrefGroup)
+ .setTitle(captor.capture());
+ assertThat(captor.getValue())
+ .isEqualTo("App usage for past 24 hr");
+ // Verifies the title in the expandable divider.
+ captor = ArgumentCaptor.forClass(String.class);
+ verify(mBatteryChartPreferenceController.mExpandDividerPreference)
+ .setTitle(captor.capture());
+ assertThat(captor.getValue())
+ .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())
+ .setLatestTimestamp(anyLong());
+ }
+
+ @Test
+ public void testSetTimestampLabel_setExpectedTimestampData() {
+ mBatteryChartPreferenceController = createController();
+ mBatteryChartPreferenceController.mBatteryChartView =
+ spy(new BatteryChartViewV2(mContext));
+ setUpBatteryHistoryKeys();
+
+ mBatteryChartPreferenceController.setTimestampLabel();
+
+ verify(mBatteryChartPreferenceController.mBatteryChartView)
+ .setLatestTimestamp(1619247636826L);
+ }
+
+ @Test
+ public void testSetTimestampLabel_withoutValidTimestamp_setExpectedTimestampData() {
+ mBatteryChartPreferenceController = createController();
+ mBatteryChartPreferenceController.mBatteryChartView =
+ spy(new BatteryChartViewV2(mContext));
+ mBatteryChartPreferenceController.mBatteryHistoryKeys = new long[]{0L};
+
+ mBatteryChartPreferenceController.setTimestampLabel();
+
+ verify(mBatteryChartPreferenceController.mBatteryChartView)
+ .setLatestTimestamp(anyLong());
+ }
+
+ @Test
+ public void testOnSaveInstanceState_restoreSelectedIndexAndExpandState() {
+ final int expectedIndex = 1;
+ final boolean isExpanded = true;
+ final Bundle bundle = new Bundle();
+ mBatteryChartPreferenceController.mTrapezoidIndex = expectedIndex;
+ mBatteryChartPreferenceController.mIsExpanded = isExpanded;
+ mBatteryChartPreferenceController.onSaveInstanceState(bundle);
+ // Replaces the original controller with other values.
+ mBatteryChartPreferenceController.mTrapezoidIndex = -1;
+ mBatteryChartPreferenceController.mIsExpanded = false;
+
+ mBatteryChartPreferenceController.onCreate(bundle);
+ mBatteryChartPreferenceController.setBatteryHistoryMap(
+ createBatteryHistoryMap());
+
+ assertThat(mBatteryChartPreferenceController.mTrapezoidIndex)
+ .isEqualTo(expectedIndex);
+ assertThat(mBatteryChartPreferenceController.mIsExpanded).isTrue();
+ }
+
+ @Test
+ public void testIsValidToShowSummary_returnExpectedResult() {
+ assertThat(mBatteryChartPreferenceController
+ .isValidToShowSummary("com.google.android.apps.scone"))
+ .isTrue();
+
+ // Verifies the item which is defined in the array list.
+ assertThat(mBatteryChartPreferenceController
+ .isValidToShowSummary("com.android.googlequicksearchbox"))
+ .isFalse();
+ }
+
+ @Test
+ public void testIsValidToShowEntry_returnExpectedResult() {
+ assertThat(mBatteryChartPreferenceController
+ .isValidToShowEntry("com.google.android.apps.scone"))
+ .isTrue();
+
+ // Verifies the items which are defined in the array list.
+ assertThat(mBatteryChartPreferenceController
+ .isValidToShowEntry("com.android.gms.persistent"))
+ .isFalse();
+ }
+
+ private static Map<Long, Map<String, BatteryHistEntry>> createBatteryHistoryMap() {
+ final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap = new HashMap<>();
+ for (int index = 0; index < DESIRED_HISTORY_SIZE; index++) {
+ final ContentValues values = new ContentValues();
+ values.put("batteryLevel", Integer.valueOf(100 - index));
+ final BatteryHistEntry entry = new BatteryHistEntry(values);
+ final Map<String, BatteryHistEntry> entryMap = new HashMap<>();
+ entryMap.put("fake_entry_key" + index, entry);
+ batteryHistoryMap.put(Long.valueOf(index + 1), entryMap);
+ }
+ return batteryHistoryMap;
+ }
+
+ private BatteryDiffEntry createBatteryDiffEntry(
+ long foregroundUsageTimeInMs, long backgroundUsageTimeInMs) {
+ return new BatteryDiffEntry(
+ mContext, foregroundUsageTimeInMs, backgroundUsageTimeInMs,
+ /*consumePower=*/ 0, mBatteryHistEntry);
+ }
+
+ private void setUpBatteryHistoryKeys() {
+ mBatteryChartPreferenceController.mBatteryHistoryKeys =
+ new long[]{1619196786769L, 0L, 1619247636826L};
+ ConvertUtils.utcToLocalTimeHour(
+ mContext, /*timestamp=*/ 0, /*is24HourFormat=*/ false);
+ }
+
+ private BatteryChartPreferenceControllerV2 createController() {
+ final BatteryChartPreferenceControllerV2 controller =
+ new BatteryChartPreferenceControllerV2(
+ mContext, "app_list", /*lifecycle=*/ null,
+ mSettingsActivity, mFragment);
+ controller.mPrefContext = mContext;
+ return controller;
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryChartViewV2Test.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryChartViewV2Test.java
new file mode 100644
index 0000000..111019f
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryChartViewV2Test.java
@@ -0,0 +1,235 @@
+/*
+ * 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 static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.accessibilityservice.AccessibilityServiceInfo;
+import android.content.Context;
+import android.os.LocaleList;
+import android.view.accessibility.AccessibilityManager;
+
+import com.android.settings.fuelgauge.PowerUsageFeatureProvider;
+import com.android.settings.testutils.FakeFeatureFactory;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Locale;
+
+@RunWith(RobolectricTestRunner.class)
+public final class BatteryChartViewV2Test {
+
+ private Context mContext;
+ private BatteryChartViewV2 mBatteryChartView;
+ private FakeFeatureFactory mFeatureFactory;
+ private PowerUsageFeatureProvider mPowerUsageFeatureProvider;
+
+ @Mock
+ private AccessibilityServiceInfo mMockAccessibilityServiceInfo;
+ @Mock
+ private AccessibilityManager mMockAccessibilityManager;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mFeatureFactory = FakeFeatureFactory.setupForTest();
+ mPowerUsageFeatureProvider = mFeatureFactory.powerUsageFeatureProvider;
+ mContext = spy(RuntimeEnvironment.application);
+ mContext.getResources().getConfiguration().setLocales(
+ new LocaleList(new Locale("en_US")));
+ mBatteryChartView = new BatteryChartViewV2(mContext);
+ doReturn(mMockAccessibilityManager).when(mContext)
+ .getSystemService(AccessibilityManager.class);
+ doReturn("TalkBackService").when(mMockAccessibilityServiceInfo).getId();
+ doReturn(Arrays.asList(mMockAccessibilityServiceInfo))
+ .when(mMockAccessibilityManager)
+ .getEnabledAccessibilityServiceList(anyInt());
+ }
+
+ @Test
+ public void testIsAccessibilityEnabled_disable_returnFalse() {
+ doReturn(false).when(mMockAccessibilityManager).isEnabled();
+ assertThat(BatteryChartViewV2.isAccessibilityEnabled(mContext)).isFalse();
+ }
+
+ @Test
+ public void testIsAccessibilityEnabled_emptyInfo_returnFalse() {
+ doReturn(true).when(mMockAccessibilityManager).isEnabled();
+ doReturn(new ArrayList<AccessibilityServiceInfo>())
+ .when(mMockAccessibilityManager)
+ .getEnabledAccessibilityServiceList(anyInt());
+
+ assertThat(BatteryChartViewV2.isAccessibilityEnabled(mContext)).isFalse();
+ }
+
+ @Test
+ public void testIsAccessibilityEnabled_validServiceId_returnTrue() {
+ doReturn(true).when(mMockAccessibilityManager).isEnabled();
+ assertThat(BatteryChartViewV2.isAccessibilityEnabled(mContext)).isTrue();
+ }
+
+ @Test
+ public void testSetSelectedIndex_invokesCallback() {
+ final int[] selectedIndex = new int[1];
+ final int expectedIndex = 2;
+ mBatteryChartView.mSelectedIndex = 1;
+ mBatteryChartView.setOnSelectListener(
+ trapezoidIndex -> {
+ selectedIndex[0] = trapezoidIndex;
+ });
+
+ mBatteryChartView.setSelectedIndex(expectedIndex);
+
+ assertThat(mBatteryChartView.mSelectedIndex)
+ .isEqualTo(expectedIndex);
+ assertThat(selectedIndex[0]).isEqualTo(expectedIndex);
+ }
+
+ @Test
+ public void testSetSelectedIndex_sameIndex_notInvokesCallback() {
+ final int[] selectedIndex = new int[1];
+ final int expectedIndex = 1;
+ mBatteryChartView.mSelectedIndex = expectedIndex;
+ mBatteryChartView.setOnSelectListener(
+ trapezoidIndex -> {
+ selectedIndex[0] = trapezoidIndex;
+ });
+
+ mBatteryChartView.setSelectedIndex(expectedIndex);
+
+ assertThat(selectedIndex[0]).isNotEqualTo(expectedIndex);
+ }
+
+ @Test
+ public void testClickable_isChartGraphSlotsEnabledIsFalse_notClickable() {
+ mBatteryChartView.setClickableForce(true);
+ when(mPowerUsageFeatureProvider.isChartGraphSlotsEnabled(mContext))
+ .thenReturn(false);
+
+ mBatteryChartView.onAttachedToWindow();
+ assertThat(mBatteryChartView.isClickable()).isFalse();
+ assertThat(mBatteryChartView.mTrapezoidCurvePaint).isNotNull();
+ }
+
+ @Test
+ public void testClickable_accessibilityIsDisabled_clickable() {
+ mBatteryChartView.setClickableForce(true);
+ when(mPowerUsageFeatureProvider.isChartGraphSlotsEnabled(mContext))
+ .thenReturn(true);
+ doReturn(false).when(mMockAccessibilityManager).isEnabled();
+
+ mBatteryChartView.onAttachedToWindow();
+ assertThat(mBatteryChartView.isClickable()).isTrue();
+ assertThat(mBatteryChartView.mTrapezoidCurvePaint).isNull();
+ }
+
+ @Test
+ public void testClickable_accessibilityIsEnabledWithoutValidId_clickable() {
+ mBatteryChartView.setClickableForce(true);
+ when(mPowerUsageFeatureProvider.isChartGraphSlotsEnabled(mContext))
+ .thenReturn(true);
+ doReturn(true).when(mMockAccessibilityManager).isEnabled();
+ doReturn(new ArrayList<AccessibilityServiceInfo>())
+ .when(mMockAccessibilityManager)
+ .getEnabledAccessibilityServiceList(anyInt());
+
+ mBatteryChartView.onAttachedToWindow();
+ assertThat(mBatteryChartView.isClickable()).isTrue();
+ assertThat(mBatteryChartView.mTrapezoidCurvePaint).isNull();
+ }
+
+ @Test
+ public void testClickable_accessibilityIsEnabledWithValidId_notClickable() {
+ mBatteryChartView.setClickableForce(true);
+ when(mPowerUsageFeatureProvider.isChartGraphSlotsEnabled(mContext))
+ .thenReturn(true);
+ doReturn(true).when(mMockAccessibilityManager).isEnabled();
+
+ mBatteryChartView.onAttachedToWindow();
+ assertThat(mBatteryChartView.isClickable()).isFalse();
+ assertThat(mBatteryChartView.mTrapezoidCurvePaint).isNotNull();
+ }
+
+ @Test
+ public void testClickable_restoreFromNonClickableState() {
+ final int[] levels = new int[13];
+ for (int index = 0; index < levels.length; index++) {
+ levels[index] = index + 1;
+ }
+ mBatteryChartView.setTrapezoidCount(12);
+ mBatteryChartView.setLevels(levels);
+ mBatteryChartView.setClickableForce(true);
+ when(mPowerUsageFeatureProvider.isChartGraphSlotsEnabled(mContext))
+ .thenReturn(true);
+ doReturn(true).when(mMockAccessibilityManager).isEnabled();
+ mBatteryChartView.onAttachedToWindow();
+ // Ensures the testing environment is correct.
+ assertThat(mBatteryChartView.isClickable()).isFalse();
+ // Turns off accessibility service.
+ doReturn(false).when(mMockAccessibilityManager).isEnabled();
+
+ mBatteryChartView.onAttachedToWindow();
+
+ assertThat(mBatteryChartView.isClickable()).isTrue();
+ }
+
+ @Test
+ public void testOnAttachedToWindow_addAccessibilityStateChangeListener() {
+ mBatteryChartView.onAttachedToWindow();
+ verify(mMockAccessibilityManager)
+ .addAccessibilityStateChangeListener(mBatteryChartView);
+ }
+
+ @Test
+ public void testOnDetachedFromWindow_removeAccessibilityStateChangeListener() {
+ mBatteryChartView.onAttachedToWindow();
+ mBatteryChartView.mHandler.postDelayed(
+ mBatteryChartView.mUpdateClickableStateRun, 1000);
+
+ mBatteryChartView.onDetachedFromWindow();
+
+ verify(mMockAccessibilityManager)
+ .removeAccessibilityStateChangeListener(mBatteryChartView);
+ assertThat(mBatteryChartView.mHandler.hasCallbacks(
+ mBatteryChartView.mUpdateClickableStateRun))
+ .isFalse();
+ }
+
+ @Test
+ public void testOnAccessibilityStateChanged_postUpdateStateRunnable() {
+ mBatteryChartView.mHandler = spy(mBatteryChartView.mHandler);
+ mBatteryChartView.onAccessibilityStateChanged(/*enabled=*/ true);
+
+ verify(mBatteryChartView.mHandler)
+ .removeCallbacks(mBatteryChartView.mUpdateClickableStateRun);
+ verify(mBatteryChartView.mHandler)
+ .postDelayed(mBatteryChartView.mUpdateClickableStateRun, 500L);
+ }
+}