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);
+    }
+}