Merge "Show one SIM only for EAP-SIM when dual SIMs have the same carrier ID" into tm-qpr-dev
diff --git a/res/layout/battery_chart_graph.xml b/res/layout/battery_chart_graph.xml
index df48144..b95c660 100644
--- a/res/layout/battery_chart_graph.xml
+++ b/res/layout/battery_chart_graph.xml
@@ -29,14 +29,24 @@
         android:layout_marginVertical="16dp"
         android:textAppearance="?android:attr/textAppearanceSmall"
         android:textColor="?android:attr/textColorSecondary"
-        android:text="@string/battery_usage_chart_graph_hint" />
+        android:text="@string/battery_usage_chart_graph_hint_last_full_charge" />
 
     <com.android.settings.fuelgauge.batteryusage.BatteryChartView
-        android:id="@+id/battery_chart"
+        android:id="@+id/daily_battery_chart"
         android:layout_width="match_parent"
         android:layout_height="170dp"
-        android:layout_marginBottom="6dp"
-        android:visibility="invisible"
+        android:layout_marginBottom="16dp"
+        android:visibility="gone"
+        android:contentDescription="@string/battery_usage_chart"
+        android:textAppearance="?android:attr/textAppearanceSmall"
+        settings:textColor="?android:attr/textColorSecondary" />
+
+    <com.android.settings.fuelgauge.batteryusage.BatteryChartView
+        android:id="@+id/hourly_battery_chart"
+        android:layout_width="match_parent"
+        android:layout_height="170dp"
+        android:layout_marginBottom="16dp"
+        android:visibility="visible"
         android:contentDescription="@string/battery_usage_chart"
         android:textAppearance="?android:attr/textAppearanceSmall"
         settings:textColor="?android:attr/textColorSecondary" />
diff --git a/res/layout/card_preference.xml b/res/layout/card_preference.xml
index be49ca3..39523eb 100644
--- a/res/layout/card_preference.xml
+++ b/res/layout/card_preference.xml
@@ -57,6 +57,30 @@
             android:maxLines="10"
             style="@style/PreferenceSummaryTextStyle"/>
 
+        <RelativeLayout
+            android:id="@+id/card_preference_buttons"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="8dp"
+            android:layout_below="@android:id/summary"
+            android:visibility="gone">
+            <Button
+                android:id="@android:id/button1"
+                style="@style/CardPreferencePrimaryButton"
+                android:layout_height="wrap_content"
+                android:layout_width="wrap_content"
+                android:layout_marginHorizontal="4dp"
+                android:layout_toStartOf="@android:id/button2"
+                android:visibility="gone"/>
+            <Button
+                android:id="@android:id/button2"
+                style="@style/CardPreferenceBorderlessButton"
+                android:layout_height="wrap_content"
+                android:layout_width="wrap_content"
+                android:layout_marginHorizontal="4dp"
+                android:layout_alignParentEnd="true"
+                android:visibility="gone"/>
+        </RelativeLayout>
     </RelativeLayout>
 
     <!-- Preference should place its actual preference widget here. -->
diff --git a/res/values/config.xml b/res/values/config.xml
index 8b255e6..4180154 100755
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -622,6 +622,9 @@
     <!-- Whether to put the apps with system UID into system component bucket or not -->
     <bool name="config_battery_combine_system_components">false</bool>
 
+    <!-- The extra value for battery tip -->
+    <integer name="config_battery_extra_tip_value">12</integer>
+
     <!-- Whether to enable the advanced vpn feature. The default is not to. -->
     <bool name="config_advanced_vpn_enabled">false</bool>
 
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 9f8df7b..406c4a2 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -184,6 +184,11 @@
     <!-- Keywords for bluetooth pairing item [CHAR LIMIT=30] -->
     <string name="keywords_add_bt_device">bluetooth</string>
 
+
+    <!-- Button to help user to pair right ear of the hearing aid device. It will show when only one of the hearing aid device set is connected. [CHAR LIMIT=20] -->
+    <string name="bluetooth_pair_right_ear_button">Pair right ear</string>
+    <!-- Button to help user to pair left ear of the hearing aid device. It will show when only one of the hearing aid device set is connected. [CHAR LIMIT=20] -->
+    <string name="bluetooth_pair_left_ear_button">Pair left ear</string>
     <!-- Connected devices settings. Title of the dialog to hint user to pair other ear of the hearing aid device. Shows when only one of the hearing aid device set is connected. [CHAR LIMIT=25] -->
     <string name="bluetooth_pair_other_ear_dialog_title">Pair your other ear</string>
     <!-- Connected devices settings. Message of the dialog to hint user to pair right ear of the hearing aid device. Shows when only left side of hearing aid device set is connected. [CHAR LIMIT=NONE] -->
@@ -6276,6 +6281,8 @@
     <string name="battery_tip_limited_temporarily_title">Charging temporarily limited</string>
     <!-- Summary for the battery limited temporarily tip [CHAR LIMIT=NONE] -->
     <string name="battery_tip_limited_temporarily_summary">To preserve your battery. Learn more.</string>
+    <!-- Summary for the battery limited temporarily extra tip [CHAR LIMIT=NONE] -->
+    <string name="battery_tip_limited_temporarily_extra_summary"><xliff:g id="percent" example="10%">%1$s</xliff:g></string>
     <!-- Text of battery limited temporarily tip resume charge button. [CHAR LIMIT=NONE] -->
     <string name="battery_tip_limited_temporarily_dialog_resume_charge">Resume charging</string>
     <!-- Message of battery limited temporarily tip. [CHAR LIMIT=NONE] -->
@@ -6332,6 +6339,8 @@
     <string name="battery_tip_unrestrict_app_dialog_ok">Remove</string>
     <!-- CANCEL button for dialog to remove restriction for app [CHAR LIMIT=NONE] -->
     <string name="battery_tip_unrestrict_app_dialog_cancel">Cancel</string>
+    <!-- Charge to full button for battery defender tips [CHAR LIMIT=NONE] -->
+    <string name="battery_tip_charge_to_full_button">Charge to full</string>
 
     <!-- Message for battery tip dialog to show the battery summary -->
     <string name="battery_tip_dialog_summary_message" product="default">Your apps are using a normal amount of battery. If apps use too much battery, your phone will suggest actions you can take.\n\nYou can always turn on Battery Saver if you\u2019re running low on battery.</string>
@@ -6744,10 +6753,16 @@
     <!-- [CHAR_LIMIT=NONE] Battery percentage: Description for preference -->
     <string name="battery_percentage_description">Show battery percentage in status bar</string>
 
+    <!-- [CHAR_LIMIT=NONE] Battery usage main screen chart graph hint since last full charge -->
+    <string name="battery_usage_chart_graph_hint_last_full_charge">Battery level since last full charge</string>
     <!-- [CHAR_LIMIT=NONE] Battery usage main screen chart graph hint -->
     <string name="battery_usage_chart_graph_hint">Battery level for past 24 hr</string>
+    <!-- [CHAR_LIMIT=NONE] Battery app usage section header since last full charge -->
+    <string name="battery_app_usage">App usage since last full charge</string>
     <!-- [CHAR_LIMIT=NONE] Battery app usage section header for past 24 hour -->
     <string name="battery_app_usage_for_past_24">App usage for past 24 hr</string>
+    <!-- [CHAR_LIMIT=NONE] Battery system usage section header since last full charge -->
+    <string name="battery_system_usage">System usage since last full charge</string>
     <!-- [CHAR_LIMIT=NONE] Battery system usage section header for past 24 hour -->
     <string name="battery_system_usage_for_past_24">System usage for past 24 hr</string>
     <!-- [CHAR_LIMIT=NONE] Battery system usage section header -->
diff --git a/res/values/styles.xml b/res/values/styles.xml
index f147ce9..8dee082 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -969,4 +969,19 @@
         <item name="android:textColor">?android:attr/textColorPrimary</item>
         <item name="android:textDirection">locale</item>
     </style>
+
+    <style name="CardPreferencePrimaryButton" parent="@style/ActionPrimaryButton">
+        <item name="android:fontFamily">google-sans-medium</item>
+        <item name="android:textSize">14sp</item>
+        <item name="android:textAllCaps">false</item>
+        <item name="android:singleLine">true</item>
+    </style>
+
+    <style name="CardPreferenceBorderlessButton"
+        parent="@style/Widget.AppCompat.Button.Borderless.Colored">
+        <item name="android:fontFamily">google-sans-medium</item>
+        <item name="android:textSize">14sp</item>
+        <item name="android:textAllCaps">false</item>
+        <item name="android:singleLine">true</item>
+    </style>
 </resources>
diff --git a/res/xml/bluetooth_device_details_fragment.xml b/res/xml/bluetooth_device_details_fragment.xml
index f330b19..efb2bf7 100644
--- a/res/xml/bluetooth_device_details_fragment.xml
+++ b/res/xml/bluetooth_device_details_fragment.xml
@@ -42,6 +42,12 @@
         settings:searchable="false"
         settings:controller="com.android.settings.bluetooth.LeAudioBluetoothDetailsHeaderController"/>
 
+    <com.android.settingslib.widget.ButtonPreference
+        android:key="hearing_aid_pair_other_button"
+        android:gravity="center" />
+    <com.android.settings.applications.SpacePreference
+        android:layout_height="8dp" />
+
     <com.android.settingslib.widget.ActionButtonsPreference
         android:key="action_buttons"
         settings:allowDividerBelow="true"/>
diff --git a/src/com/android/settings/applications/appinfo/AppBatteryPreferenceController.java b/src/com/android/settings/applications/appinfo/AppBatteryPreferenceController.java
index 99c630d..732163b 100644
--- a/src/com/android/settings/applications/appinfo/AppBatteryPreferenceController.java
+++ b/src/com/android/settings/applications/appinfo/AppBatteryPreferenceController.java
@@ -179,7 +179,7 @@
                     return null;
                 }
                 final BatteryDiffEntry entry =
-                        BatteryChartPreferenceController.getBatteryLast24HrUsageData(
+                        BatteryChartPreferenceController.getAppBatteryUsageData(
                                 mContext, mPackageName, mUserId);
                 Log.d(TAG, "loadBatteryDiffEntries():\n" + entry);
                 return entry;
@@ -200,10 +200,10 @@
                 mBatteryPercent = Utils.formatPercentage(
                         mBatteryDiffEntry.getPercentOfTotal(), /* round */ true);
                 mPreference.setSummary(mContext.getString(
-                        R.string.battery_summary_24hr, mBatteryPercent));
+                        R.string.battery_summary, mBatteryPercent));
             } else {
                 mPreference.setSummary(
-                        mContext.getString(R.string.no_battery_summary_24hr));
+                        mContext.getString(R.string.no_battery_summary));
             }
         }
 
diff --git a/src/com/android/settings/applications/specialaccess/notificationaccess/BridgedAppsLinkPreferenceController.java b/src/com/android/settings/applications/specialaccess/notificationaccess/BridgedAppsLinkPreferenceController.java
index 1c787ba..6a641b3 100644
--- a/src/com/android/settings/applications/specialaccess/notificationaccess/BridgedAppsLinkPreferenceController.java
+++ b/src/com/android/settings/applications/specialaccess/notificationaccess/BridgedAppsLinkPreferenceController.java
@@ -18,10 +18,11 @@
 import android.os.Build;
 import android.service.notification.NotificationListenerFilter;
 
+import androidx.preference.Preference;
+
 import com.android.settings.core.BasePreferenceController;
 import com.android.settings.notification.NotificationBackend;
 
-
 public class BridgedAppsLinkPreferenceController extends BasePreferenceController {
 
     private ComponentName mCn;
@@ -61,7 +62,6 @@
             if (mTargetSdk > Build.VERSION_CODES.S) {
                 return AVAILABLE;
             }
-
             mNlf = mNm.getListenerFilter(mCn, mUserId);
             if (!mNlf.areAllTypesAllowed() || !mNlf.getDisallowedPackages().isEmpty()) {
                 return AVAILABLE;
@@ -69,4 +69,10 @@
         }
         return DISABLED_DEPENDENT_SETTING;
     }
+
+    @Override
+    public void updateState(Preference pref) {
+        pref.setEnabled(getAvailabilityStatus() == AVAILABLE);
+        super.updateState(pref);
+    }
 }
diff --git a/src/com/android/settings/applications/specialaccess/notificationaccess/NotificationAccessDetails.java b/src/com/android/settings/applications/specialaccess/notificationaccess/NotificationAccessDetails.java
index da25f17..e6feebb 100644
--- a/src/com/android/settings/applications/specialaccess/notificationaccess/NotificationAccessDetails.java
+++ b/src/com/android/settings/applications/specialaccess/notificationaccess/NotificationAccessDetails.java
@@ -233,11 +233,7 @@
         apc.updateState(screen.findPreference(apc.getPreferenceKey()));
         getPreferenceControllers().forEach(controllers -> {
             controllers.forEach(controller -> {
-                if (controller instanceof TypeFilterPreferenceController) {
-                    TypeFilterPreferenceController tfpc =
-                            (TypeFilterPreferenceController) controller;
-                    tfpc.updateState(screen.findPreference(tfpc.getPreferenceKey()));
-                }
+                controller.updateState(screen.findPreference(controller.getPreferenceKey()));
             });
         });
     }
@@ -249,11 +245,7 @@
         apc.updateState(screen.findPreference(apc.getPreferenceKey()));
         getPreferenceControllers().forEach(controllers -> {
             controllers.forEach(controller -> {
-                if (controller instanceof TypeFilterPreferenceController) {
-                    TypeFilterPreferenceController tfpc =
-                            (TypeFilterPreferenceController) controller;
-                    tfpc.updateState(screen.findPreference(tfpc.getPreferenceKey()));
-                }
+                controller.updateState(screen.findPreference(controller.getPreferenceKey()));
             });
         });
     }
diff --git a/src/com/android/settings/bluetooth/BluetoothDetailsPairOtherController.java b/src/com/android/settings/bluetooth/BluetoothDetailsPairOtherController.java
new file mode 100644
index 0000000..d14a9b1
--- /dev/null
+++ b/src/com/android/settings/bluetooth/BluetoothDetailsPairOtherController.java
@@ -0,0 +1,102 @@
+/*
+ * 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.bluetooth;
+
+import android.content.Context;
+
+import androidx.preference.PreferenceFragmentCompat;
+import androidx.preference.PreferenceScreen;
+
+import com.android.settings.R;
+import com.android.settings.core.SubSettingLauncher;
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.bluetooth.HearingAidProfile;
+import com.android.settingslib.core.lifecycle.Lifecycle;
+import com.android.settingslib.widget.ButtonPreference;
+
+/**
+ * This class handles button preference logic to display for hearing aid device.
+ */
+public class BluetoothDetailsPairOtherController extends BluetoothDetailsController {
+    private static final String KEY_PAIR_OTHER = "hearing_aid_pair_other_button";
+
+    private ButtonPreference mPreference;
+
+    public BluetoothDetailsPairOtherController(Context context,
+            PreferenceFragmentCompat fragment,
+            CachedBluetoothDevice device,
+            Lifecycle lifecycle) {
+        super(context, fragment, device, lifecycle);
+        lifecycle.addObserver(this);
+    }
+
+    @Override
+    public boolean isAvailable() {
+        return getButtonPreferenceVisibility(mCachedDevice);
+    }
+
+    @Override
+    public String getPreferenceKey() {
+        return KEY_PAIR_OTHER;
+    }
+
+    @Override
+    protected void init(PreferenceScreen screen) {
+        final int side = mCachedDevice.getDeviceSide();
+        final int stringRes = (side == HearingAidProfile.DeviceSide.SIDE_LEFT)
+                ? R.string.bluetooth_pair_right_ear_button
+                : R.string.bluetooth_pair_left_ear_button;
+
+        mPreference = screen.findPreference(getPreferenceKey());
+        mPreference.setTitle(stringRes);
+        mPreference.setOnClickListener(v -> launchPairingDetail());
+    }
+
+    @Override
+    protected void refresh() {
+        mPreference.setVisible(getButtonPreferenceVisibility(mCachedDevice));
+    }
+
+    private boolean getButtonPreferenceVisibility(CachedBluetoothDevice cachedDevice) {
+        return isBinauralMode(cachedDevice) && isOnlyOneSideConnected(cachedDevice);
+    }
+
+    private void launchPairingDetail() {
+        new SubSettingLauncher(mContext)
+                .setDestination(BluetoothPairingDetail.class.getName())
+                .setSourceMetricsCategory(
+                        ((BluetoothDeviceDetailsFragment) mFragment).getMetricsCategory())
+                .launch();
+    }
+
+    private boolean isBinauralMode(CachedBluetoothDevice cachedDevice) {
+        return cachedDevice.getDeviceMode() == HearingAidProfile.DeviceMode.MODE_BINAURAL;
+    }
+
+    private boolean isOnlyOneSideConnected(CachedBluetoothDevice cachedDevice) {
+        if (!cachedDevice.isConnectedHearingAidDevice()) {
+            return false;
+        }
+
+        final CachedBluetoothDevice subDevice = cachedDevice.getSubDevice();
+        if (subDevice != null && subDevice.isConnectedHearingAidDevice()) {
+            return false;
+        }
+
+        return true;
+    }
+}
diff --git a/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java b/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java
index c118a43..999e34d 100644
--- a/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java
+++ b/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java
@@ -251,6 +251,8 @@
                     lifecycle));
             controllers.add(new BluetoothDetailsRelatedToolsController(context, this, mCachedDevice,
                     lifecycle));
+            controllers.add(new BluetoothDetailsPairOtherController(context, this, mCachedDevice,
+                    lifecycle));
         }
         return controllers;
     }
diff --git a/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetail.java b/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetail.java
index d096d49..db98a4c 100644
--- a/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetail.java
+++ b/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetail.java
@@ -539,16 +539,13 @@
             return null;
         }
         if (totalTimeMs == 0) {
-            final int batteryWithoutUsageTime = consumedPower > 0
-                    ? R.string.battery_usage_without_time : R.string.battery_not_usage_24hr;
-            usageTimeSummary = getText(isChartGraphEnabled
-                    ? batteryWithoutUsageTime : R.string.battery_not_usage);
+            usageTimeSummary = getText(
+                    isChartGraphEnabled && consumedPower > 0 ? R.string.battery_usage_without_time
+                            : R.string.battery_not_usage);
         } else if (slotTime == null) {
-            // Shows summary text with past 24 hr or full charge if slot time is null.
-            usageTimeSummary = isChartGraphEnabled
-                    ? getAppPast24HrActiveSummary(foregroundTimeMs, backgroundTimeMs, totalTimeMs)
-                    : getAppFullChargeActiveSummary(
-                            foregroundTimeMs, backgroundTimeMs, totalTimeMs);
+            // Shows summary text with last full charge if slot time is null.
+            usageTimeSummary = getAppFullChargeActiveSummary(
+                    foregroundTimeMs, backgroundTimeMs, totalTimeMs);
         } else {
             // Shows summary text with slot time.
             usageTimeSummary = getAppActiveSummaryWithSlotTime(
diff --git a/src/com/android/settings/fuelgauge/PowerUsageFeatureProvider.java b/src/com/android/settings/fuelgauge/PowerUsageFeatureProvider.java
index 83d7a33..94a93b8 100644
--- a/src/com/android/settings/fuelgauge/PowerUsageFeatureProvider.java
+++ b/src/com/android/settings/fuelgauge/PowerUsageFeatureProvider.java
@@ -140,6 +140,11 @@
     boolean isAdaptiveChargingSupported();
 
     /**
+     * Returns {@code true} if current defender mode is extra defend
+     */
+    boolean isExtraDefend();
+
+    /**
      * Gets a intent for one time bypass charge limited to resume charging.
      */
     Intent getResumeChargeIntent();
diff --git a/src/com/android/settings/fuelgauge/PowerUsageFeatureProviderImpl.java b/src/com/android/settings/fuelgauge/PowerUsageFeatureProviderImpl.java
index 1262641..0adfc9d 100644
--- a/src/com/android/settings/fuelgauge/PowerUsageFeatureProviderImpl.java
+++ b/src/com/android/settings/fuelgauge/PowerUsageFeatureProviderImpl.java
@@ -161,6 +161,11 @@
     }
 
     @Override
+    public boolean isExtraDefend() {
+        return false;
+    }
+
+    @Override
     public Map<Long, Map<String, BatteryHistEntry>> getBatteryHistory(Context context) {
         return null;
     }
diff --git a/src/com/android/settings/fuelgauge/batterytip/BatteryTipLoader.java b/src/com/android/settings/fuelgauge/batterytip/BatteryTipLoader.java
index 4b98587..ea493a3 100644
--- a/src/com/android/settings/fuelgauge/batterytip/BatteryTipLoader.java
+++ b/src/com/android/settings/fuelgauge/batterytip/BatteryTipLoader.java
@@ -31,6 +31,7 @@
 import com.android.settings.fuelgauge.batterytip.tips.BatteryTip;
 import com.android.settings.fuelgauge.batterytip.tips.LowBatteryTip;
 import com.android.settings.fuelgauge.batterytip.tips.SummaryTip;
+import com.android.settings.overlay.FeatureFactory;
 import com.android.settingslib.fuelgauge.EstimateKt;
 import com.android.settingslib.utils.AsyncLoaderCompat;
 
@@ -66,13 +67,16 @@
         final BatteryTipPolicy policy = new BatteryTipPolicy(getContext());
         final BatteryInfo batteryInfo = mBatteryUtils.getBatteryInfo(TAG);
         final Context context = getContext();
+        final boolean extraDefend = FeatureFactory.getFactory(context)
+                .getPowerUsageFeatureProvider(context)
+                .isExtraDefend();
 
         tips.add(new LowBatteryDetector(context, policy, batteryInfo).detect());
         tips.add(new HighUsageDetector(context, policy, mBatteryUsageStats, batteryInfo).detect());
         tips.add(new SmartBatteryDetector(
                 context, policy, batteryInfo, context.getContentResolver()).detect());
         tips.add(new EarlyWarningDetector(policy, context).detect());
-        tips.add(new BatteryDefenderDetector(batteryInfo).detect());
+        tips.add(new BatteryDefenderDetector(batteryInfo, extraDefend).detect());
         Collections.sort(tips);
         return tips;
     }
diff --git a/src/com/android/settings/fuelgauge/batterytip/detectors/BatteryDefenderDetector.java b/src/com/android/settings/fuelgauge/batterytip/detectors/BatteryDefenderDetector.java
index 5befa33..367b2b1 100644
--- a/src/com/android/settings/fuelgauge/batterytip/detectors/BatteryDefenderDetector.java
+++ b/src/com/android/settings/fuelgauge/batterytip/detectors/BatteryDefenderDetector.java
@@ -24,10 +24,12 @@
  * Detect whether the battery is overheated
  */
 public class BatteryDefenderDetector implements BatteryTipDetector {
-    private BatteryInfo mBatteryInfo;
+    private final BatteryInfo mBatteryInfo;
+    private final boolean mExtraDefend;
 
-    public BatteryDefenderDetector(BatteryInfo batteryInfo) {
+    public BatteryDefenderDetector(BatteryInfo batteryInfo, boolean extraDefend) {
         mBatteryInfo = batteryInfo;
+        mExtraDefend = extraDefend;
     }
 
     @Override
@@ -36,6 +38,6 @@
                 mBatteryInfo.isOverheated
                     ? BatteryTip.StateType.NEW
                     : BatteryTip.StateType.INVISIBLE;
-        return new BatteryDefenderTip(state);
+        return new BatteryDefenderTip(state, mExtraDefend);
     }
 }
diff --git a/src/com/android/settings/fuelgauge/batterytip/tips/BatteryDefenderTip.java b/src/com/android/settings/fuelgauge/batterytip/tips/BatteryDefenderTip.java
index a2890ad..0a133bb 100644
--- a/src/com/android/settings/fuelgauge/batterytip/tips/BatteryDefenderTip.java
+++ b/src/com/android/settings/fuelgauge/batterytip/tips/BatteryDefenderTip.java
@@ -18,18 +18,36 @@
 
 import android.app.settings.SettingsEnums;
 import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.BatteryManager;
 import android.os.Parcel;
+import android.util.Log;
+
+import androidx.preference.Preference;
 
 import com.android.settings.R;
+import com.android.settings.overlay.FeatureFactory;
+import com.android.settings.widget.CardPreference;
 import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
 
+import java.text.NumberFormat;
+
 /**
  * Tip to show current battery is overheated
  */
 public class BatteryDefenderTip extends BatteryTip {
 
+    private static final String TAG = "BatteryDefenderTip";
+    private boolean mExtraDefend = false;
+
     public BatteryDefenderTip(@StateType int state) {
+        this(state, false);
+    }
+
+    public BatteryDefenderTip(@StateType int state, boolean extraDefend) {
         super(TipType.BATTERY_DEFENDER, state, true /* showDialog */);
+        mExtraDefend = extraDefend;
     }
 
     private BatteryDefenderTip(Parcel in) {
@@ -43,6 +61,14 @@
 
     @Override
     public CharSequence getSummary(Context context) {
+        if (mExtraDefend) {
+            final int extraValue = context.getResources()
+                    .getInteger(R.integer.config_battery_extra_tip_value);
+            final String extraPercentage = NumberFormat.getPercentInstance()
+                    .format(extraValue * 0.01f);
+            return context.getString(
+                    R.string.battery_tip_limited_temporarily_extra_summary, extraPercentage);
+        }
         return context.getString(R.string.battery_tip_limited_temporarily_summary);
     }
 
@@ -62,6 +88,55 @@
                 mState);
     }
 
+    @Override
+    public void updatePreference(Preference preference) {
+        super.updatePreference(preference);
+        final Context context = preference.getContext();
+
+        CardPreference cardPreference = castToCardPreferenceSafely(preference);
+        if (cardPreference == null) {
+            Log.e(TAG, "cast Preference to CardPreference failed");
+            return;
+        }
+
+        cardPreference.setPrimaryButtonText(
+                context.getString(R.string.battery_tip_charge_to_full_button));
+        cardPreference.setPrimaryButtonClickListener(
+                unused -> {
+                    resumeCharging(context);
+                    preference.setVisible(false);
+                });
+        cardPreference.setPrimaryButtonVisible(isPluggedIn(context));
+
+        cardPreference.setSecondaryButtonText(context.getString(R.string.see_more));
+        cardPreference.setSecondaryButtonClickListener(unused -> cardPreference.performClick());
+        cardPreference.setSecondaryButtonVisible(true);
+    }
+
+    private CardPreference castToCardPreferenceSafely(Preference preference) {
+        return preference instanceof CardPreference ? (CardPreference) preference : null;
+    }
+
+    private void resumeCharging(Context context) {
+        final Intent intent =
+                FeatureFactory.getFactory(context)
+                        .getPowerUsageFeatureProvider(context)
+                        .getResumeChargeIntent();
+        if (intent != null) {
+            context.sendBroadcast(intent);
+        }
+
+        Log.i(TAG, "send resume charging broadcast intent=" + intent);
+    }
+
+    private boolean isPluggedIn(Context context) {
+        final Intent batteryIntent =
+                context.registerReceiver(
+                        /* receiver= */ null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
+        return batteryIntent != null
+                && batteryIntent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0) != 0;
+    }
+
     public static final Creator CREATOR = new Creator() {
         public BatteryTip createFromParcel(Parcel in) {
             return new BatteryDefenderTip(in);
diff --git a/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceController.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceController.java
index d363308..88bec0d 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceController.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceController.java
@@ -20,7 +20,6 @@
 import android.content.Context;
 import android.content.res.Configuration;
 import android.graphics.drawable.Drawable;
-import android.os.AsyncTask;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.Looper;
@@ -28,7 +27,9 @@
 import android.text.format.DateFormat;
 import android.text.format.DateUtils;
 import android.util.Log;
+import android.view.View;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.VisibleForTesting;
 import androidx.preference.Preference;
 import androidx.preference.PreferenceGroup;
@@ -53,8 +54,6 @@
 import com.android.settingslib.widget.FooterPreference;
 
 import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -62,27 +61,23 @@
 /** Controls the update for chart graph and the list items. */
 public class BatteryChartPreferenceController extends AbstractPreferenceController
         implements PreferenceControllerMixin, LifecycleObserver, OnCreate, OnDestroy,
-        OnSaveInstanceState, BatteryChartView.OnSelectListener, OnResume,
-        ExpandDividerPreference.OnExpandListener {
+        OnSaveInstanceState, OnResume, ExpandDividerPreference.OnExpandListener {
     private static final String TAG = "BatteryChartPreferenceController";
     private static final String KEY_FOOTER_PREF = "battery_graph_footer";
     private static final String PACKAGE_NAME_NONE = "none";
 
-    /** Desired battery history size for timestamp slots. */
-    public static final int DESIRED_HISTORY_SIZE = 25;
-    private static final int CHART_LEVEL_ARRAY_SIZE = 13;
-    private static final int CHART_KEY_ARRAY_SIZE = DESIRED_HISTORY_SIZE;
     private static final long VALID_USAGE_TIME_DURATION = DateUtils.HOUR_IN_MILLIS * 2;
     private static final long VALID_DIFF_DURATION = DateUtils.MINUTE_IN_MILLIS * 3;
 
     // Keys for bundle instance to restore configurations.
     private static final String KEY_EXPAND_SYSTEM_INFO = "expand_system_info";
-    private static final String KEY_CURRENT_TIME_SLOT = "current_time_slot";
+    private static final String KEY_DAILY_CHART_INDEX = "daily_chart_index";
+    private static final String KEY_HOURLY_CHART_INDEX = "hourly_chart_index";
 
     private static int sUiMode = Configuration.UI_MODE_NIGHT_UNDEFINED;
 
     @VisibleForTesting
-    Map<Integer, List<BatteryDiffEntry>> mBatteryIndexedMap;
+    Map<Integer, Map<Integer, BatteryDiffData>> mBatteryUsageMap;
 
     @VisibleForTesting
     Context mPrefContext;
@@ -91,28 +86,34 @@
     @VisibleForTesting
     PreferenceGroup mAppListPrefGroup;
     @VisibleForTesting
-    BatteryChartView mBatteryChartView;
-    @VisibleForTesting
     ExpandDividerPreference mExpandDividerPreference;
-
     @VisibleForTesting
     boolean mIsExpanded = false;
-    @VisibleForTesting
-    int[] mBatteryHistoryLevels;
-    @VisibleForTesting
-    long[] mBatteryHistoryKeys;
-    @VisibleForTesting
-    int mTrapezoidIndex = BatteryChartView.SELECTED_INDEX_INVALID;
 
-    private boolean mIs24HourFormat = false;
+    @VisibleForTesting
+    BatteryChartView mDailyChartView;
+    @VisibleForTesting
+    BatteryChartView mHourlyChartView;
+
+    @VisibleForTesting
+    int mDailyChartIndex = BatteryChartViewModel.SELECTED_INDEX_ALL;
+    @VisibleForTesting
+    int mHourlyChartIndex = BatteryChartViewModel.SELECTED_INDEX_ALL;
+
+    private boolean mIs24HourFormat;
     private boolean mIsFooterPrefAdded = false;
     private PreferenceScreen mPreferenceScreen;
     private FooterPreference mFooterPreference;
+    // Daily view model only saves abbreviated day of week texts (e.g. MON). This field saves the
+    // full day of week texts (e.g. Monday), which is used in category title and battery detail
+    // page.
+    private List<String> mDailyTimestampFullTexts;
+    private BatteryChartViewModel mDailyViewModel;
+    private List<BatteryChartViewModel> mHourlyViewModels;
 
     private final String mPreferenceKey;
     private final SettingsActivity mActivity;
     private final InstrumentedPreferenceFragment mFragment;
-    private final CharSequence[] mNotAllowShowEntryPackages;
     private final CharSequence[] mNotAllowShowSummaryPackages;
     private final MetricsFeatureProvider mMetricsFeatureProvider;
     private final Handler mHandler = new Handler(Looper.getMainLooper());
@@ -120,8 +121,6 @@
     // Preference cache to avoid create new instance each time.
     @VisibleForTesting
     final Map<String, Preference> mPreferenceCache = new HashMap<>();
-    @VisibleForTesting
-    final List<BatteryDiffEntry> mSystemEntries = new ArrayList<>();
 
     public BatteryChartPreferenceController(
             Context context, String preferenceKey,
@@ -134,10 +133,6 @@
         mIs24HourFormat = DateFormat.is24HourFormat(context);
         mMetricsFeatureProvider =
                 FeatureFactory.getFactory(mContext).getMetricsFeatureProvider();
-        mNotAllowShowEntryPackages =
-                FeatureFactory.getFactory(context)
-                        .getPowerUsageFeatureProvider(context)
-                        .getHideApplicationEntries(context);
         mNotAllowShowSummaryPackages =
                 FeatureFactory.getFactory(context)
                         .getPowerUsageFeatureProvider(context)
@@ -152,12 +147,14 @@
         if (savedInstanceState == null) {
             return;
         }
-        mTrapezoidIndex =
-                savedInstanceState.getInt(KEY_CURRENT_TIME_SLOT, mTrapezoidIndex);
+        mDailyChartIndex =
+                savedInstanceState.getInt(KEY_DAILY_CHART_INDEX, mDailyChartIndex);
+        mHourlyChartIndex =
+                savedInstanceState.getInt(KEY_HOURLY_CHART_INDEX, mHourlyChartIndex);
         mIsExpanded =
                 savedInstanceState.getBoolean(KEY_EXPAND_SYSTEM_INFO, mIsExpanded);
-        Log.d(TAG, String.format("onCreate() slotIndex=%d isExpanded=%b",
-                mTrapezoidIndex, mIsExpanded));
+        Log.d(TAG, String.format("onCreate() dailyIndex=%d hourlyIndex=%d isExpanded=%b",
+                mDailyChartIndex, mHourlyChartIndex, mIsExpanded));
     }
 
     @Override
@@ -179,10 +176,11 @@
         if (savedInstance == null) {
             return;
         }
-        savedInstance.putInt(KEY_CURRENT_TIME_SLOT, mTrapezoidIndex);
+        savedInstance.putInt(KEY_DAILY_CHART_INDEX, mDailyChartIndex);
+        savedInstance.putInt(KEY_HOURLY_CHART_INDEX, mHourlyChartIndex);
         savedInstance.putBoolean(KEY_EXPAND_SYSTEM_INFO, mIsExpanded);
-        Log.d(TAG, String.format("onSaveInstanceState() slotIndex=%d isExpanded=%b",
-                mTrapezoidIndex, mIsExpanded));
+        Log.d(TAG, String.format("onSaveInstanceState() dailyIndex=%d hourlyIndex=%d isExpanded=%b",
+                mDailyChartIndex, mHourlyChartIndex, mIsExpanded));
     }
 
     @Override
@@ -204,8 +202,7 @@
         mPrefContext = screen.getContext();
         mAppListPrefGroup = screen.findPreference(mPreferenceKey);
         mAppListPrefGroup.setOrderingAsAdded(false);
-        mAppListPrefGroup.setTitle(
-                mPrefContext.getString(R.string.battery_app_usage_for_past_24));
+        mAppListPrefGroup.setTitle(mPrefContext.getString(R.string.battery_app_usage));
         mFooterPreference = screen.findPreference(KEY_FOOTER_PREF);
         // Removes footer first until usage data is loaded to avoid flashing.
         if (mFooterPreference != null) {
@@ -250,17 +247,6 @@
     }
 
     @Override
-    public void onSelect(int trapezoidIndex) {
-        Log.d(TAG, "onChartSelect:" + trapezoidIndex);
-        refreshUi(trapezoidIndex, /*isForce=*/ false);
-        mMetricsFeatureProvider.action(
-                mPrefContext,
-                trapezoidIndex == BatteryChartView.SELECTED_INDEX_ALL
-                        ? SettingsEnums.ACTION_BATTERY_USAGE_SHOW_ALL
-                        : SettingsEnums.ACTION_BATTERY_USAGE_TIME_SLOT);
-    }
-
-    @Override
     public void onExpand(boolean isExpanded) {
         mIsExpanded = isExpanded;
         mMetricsFeatureProvider.action(
@@ -272,81 +258,120 @@
 
     void setBatteryHistoryMap(
             final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap) {
-        // Resets all battery history data relative variables.
-        if (batteryHistoryMap == null || batteryHistoryMap.isEmpty()) {
-            mBatteryIndexedMap = null;
-            mBatteryHistoryKeys = null;
-            mBatteryHistoryLevels = null;
-            addFooterPreferenceIfNeeded(false);
+        Log.d(TAG, "setBatteryHistoryMap() " + (batteryHistoryMap == null ? "null"
+                : ("size=" + batteryHistoryMap.size())));
+        final BatteryLevelData batteryLevelData =
+                DataProcessor.getBatteryLevelData(mContext, mHandler, batteryHistoryMap,
+                        batteryUsageMap -> {
+                            mBatteryUsageMap = batteryUsageMap;
+                            refreshUi();
+                        });
+        Log.d(TAG, "getBatteryLevelData: " + batteryLevelData);
+        if (batteryLevelData == null) {
+            mDailyTimestampFullTexts = null;
+            mDailyViewModel = null;
+            mHourlyViewModels = null;
+            refreshUi();
             return;
         }
-        mBatteryHistoryKeys = getBatteryHistoryKeys(batteryHistoryMap);
-        mBatteryHistoryLevels = new int[CHART_LEVEL_ARRAY_SIZE];
-        for (int index = 0; index < CHART_LEVEL_ARRAY_SIZE; index++) {
-            final long timestamp = mBatteryHistoryKeys[index * 2];
-            final Map<String, BatteryHistEntry> entryMap = batteryHistoryMap.get(timestamp);
-            if (entryMap == null || entryMap.isEmpty()) {
-                Log.e(TAG, "abnormal entry list in the timestamp:"
-                        + ConvertUtils.utcToLocalTime(mPrefContext, timestamp));
-                continue;
+        mDailyTimestampFullTexts = generateTimestampDayOfWeekTexts(
+                mContext, batteryLevelData.getDailyBatteryLevels().getTimestamps(),
+                /* isAbbreviation= */ false);
+        mDailyViewModel = new BatteryChartViewModel(
+                batteryLevelData.getDailyBatteryLevels().getLevels(),
+                generateTimestampDayOfWeekTexts(
+                        mContext, batteryLevelData.getDailyBatteryLevels().getTimestamps(),
+                        /* isAbbreviation= */ true),
+                BatteryChartViewModel.AxisLabelPosition.CENTER_OF_TRAPEZOIDS);
+        mHourlyViewModels = new ArrayList<>();
+        for (BatteryLevelData.PeriodBatteryLevelData hourlyBatteryLevelsPerDay :
+                batteryLevelData.getHourlyBatteryLevelsPerDay()) {
+            mHourlyViewModels.add(new BatteryChartViewModel(
+                    hourlyBatteryLevelsPerDay.getLevels(),
+                    generateTimestampHourTexts(
+                            mContext, hourlyBatteryLevelsPerDay.getTimestamps()),
+                    BatteryChartViewModel.AxisLabelPosition.BETWEEN_TRAPEZOIDS));
+        }
+        refreshUi();
+    }
+
+    void setBatteryChartView(@NonNull final BatteryChartView dailyChartView,
+            @NonNull final BatteryChartView hourlyChartView) {
+        if (mDailyChartView != dailyChartView || mHourlyChartView != hourlyChartView) {
+            mHandler.post(() -> setBatteryChartViewInner(dailyChartView, hourlyChartView));
+        }
+    }
+
+    private void setBatteryChartViewInner(@NonNull final BatteryChartView dailyChartView,
+            @NonNull final BatteryChartView hourlyChartView) {
+        mDailyChartView = dailyChartView;
+        mDailyChartView.setOnSelectListener(trapezoidIndex -> {
+            if (mDailyChartIndex == trapezoidIndex) {
+                return;
             }
-            // Averages the battery level in each time slot to avoid corner conditions.
-            float batteryLevelCounter = 0;
-            for (BatteryHistEntry entry : entryMap.values()) {
-                batteryLevelCounter += entry.mBatteryLevel;
+            Log.d(TAG, "onDailyChartSelect:" + trapezoidIndex);
+            mDailyChartIndex = trapezoidIndex;
+            mHourlyChartIndex = BatteryChartViewModel.SELECTED_INDEX_ALL;
+            refreshUi();
+            // TODO: Change to log daily data.
+        });
+        mHourlyChartView = hourlyChartView;
+        mHourlyChartView.setOnSelectListener(trapezoidIndex -> {
+            if (mHourlyChartIndex == trapezoidIndex) {
+                return;
             }
-            mBatteryHistoryLevels[index] =
-                    Math.round(batteryLevelCounter / entryMap.size());
-        }
-        forceRefreshUi();
-        Log.d(TAG, String.format(
-                "setBatteryHistoryMap() size=%d key=%s\nlevels=%s",
-                batteryHistoryMap.size(),
-                ConvertUtils.utcToLocalTime(mPrefContext,
-                        mBatteryHistoryKeys[mBatteryHistoryKeys.length - 1]),
-                Arrays.toString(mBatteryHistoryLevels)));
-
-        // Loads item icon and label in the background.
-        new LoadAllItemsInfoTask(batteryHistoryMap).execute();
-    }
-
-    void setBatteryChartView(final BatteryChartView batteryChartView) {
-        if (mBatteryChartView != batteryChartView) {
-            mHandler.post(() -> setBatteryChartViewInner(batteryChartView));
-        }
-    }
-
-    private void setBatteryChartViewInner(final BatteryChartView batteryChartView) {
-        mBatteryChartView = batteryChartView;
-        mBatteryChartView.setOnSelectListener(this);
-        forceRefreshUi();
-    }
-
-    private void forceRefreshUi() {
-        final int refreshIndex =
-                mTrapezoidIndex == BatteryChartView.SELECTED_INDEX_INVALID
-                        ? BatteryChartView.SELECTED_INDEX_ALL
-                        : mTrapezoidIndex;
-        if (mBatteryChartView != null) {
-            mBatteryChartView.setLevels(mBatteryHistoryLevels);
-            mBatteryChartView.setSelectedIndex(refreshIndex);
-            setTimestampLabel();
-        }
-        refreshUi(refreshIndex, /*isForce=*/ true);
+            Log.d(TAG, "onHourlyChartSelect:" + trapezoidIndex);
+            mHourlyChartIndex = trapezoidIndex;
+            refreshUi();
+            mMetricsFeatureProvider.action(
+                    mPrefContext,
+                    trapezoidIndex == BatteryChartViewModel.SELECTED_INDEX_ALL
+                            ? SettingsEnums.ACTION_BATTERY_USAGE_SHOW_ALL
+                            : SettingsEnums.ACTION_BATTERY_USAGE_TIME_SLOT);
+        });
+        refreshUi();
     }
 
     @VisibleForTesting
-    boolean refreshUi(int trapezoidIndex, boolean isForce) {
-        // Invalid refresh condition.
-        if (mBatteryIndexedMap == null
-                || mBatteryChartView == null
-                || (mTrapezoidIndex == trapezoidIndex && !isForce)) {
+    boolean refreshUi() {
+        if (mDailyChartView == null || mHourlyChartView == null) {
+            // Chart views are not initialized.
             return false;
         }
-        Log.d(TAG, String.format("refreshUi: index=%d size=%d isForce:%b",
-                trapezoidIndex, mBatteryIndexedMap.size(), isForce));
+        if (mDailyViewModel == null || mHourlyViewModels == null) {
+            // Fail to get battery level data, show an empty hourly chart view.
+            mDailyChartView.setVisibility(View.GONE);
+            mHourlyChartView.setVisibility(View.VISIBLE);
+            mHourlyChartView.setViewModel(null);
+            removeAndCacheAllPrefs();
+            addFooterPreferenceIfNeeded(false);
+            return false;
+        }
+        if (mBatteryUsageMap == null) {
+            // Battery usage data is not ready, wait for data ready to refresh UI.
+            return false;
+        }
 
-        mTrapezoidIndex = trapezoidIndex;
+        if (isBatteryLevelDataInOneDay()) {
+            // Only 1 day data, hide the daily chart view.
+            mDailyChartView.setVisibility(View.GONE);
+            mDailyChartIndex = 0;
+        } else {
+            mDailyChartView.setVisibility(View.VISIBLE);
+            mDailyViewModel.setSelectedIndex(mDailyChartIndex);
+            mDailyChartView.setViewModel(mDailyViewModel);
+        }
+
+        if (mDailyChartIndex == BatteryChartViewModel.SELECTED_INDEX_ALL) {
+            // Multiple days are selected, hide the hourly chart view.
+            mHourlyChartView.setVisibility(View.GONE);
+        } else {
+            mHourlyChartView.setVisibility(View.VISIBLE);
+            final BatteryChartViewModel hourlyViewModel = mHourlyViewModels.get(mDailyChartIndex);
+            hourlyViewModel.setSelectedIndex(mHourlyChartIndex);
+            mHourlyChartView.setViewModel(hourlyViewModel);
+        }
+
         mHandler.post(() -> {
             final long start = System.currentTimeMillis();
             removeAndCacheAllPrefs();
@@ -359,43 +384,22 @@
     }
 
     private void addAllPreferences() {
-        final List<BatteryDiffEntry> entries =
-                mBatteryIndexedMap.get(Integer.valueOf(mTrapezoidIndex));
-        addFooterPreferenceIfNeeded(entries != null && !entries.isEmpty());
-        if (entries == null) {
-            Log.w(TAG, "cannot find BatteryDiffEntry for:" + mTrapezoidIndex);
+        final BatteryDiffData batteryDiffData =
+                mBatteryUsageMap.get(mDailyChartIndex).get(mHourlyChartIndex);
+        addFooterPreferenceIfNeeded(batteryDiffData != null
+                && (!batteryDiffData.getAppDiffEntryList().isEmpty()
+                || !batteryDiffData.getSystemDiffEntryList().isEmpty()));
+        if (batteryDiffData == null) {
+            Log.w(TAG, "cannot find BatteryDiffEntry for daily_index: " + mDailyChartIndex
+                    + " hourly_index: " + mHourlyChartIndex);
             return;
         }
-        // Separates data into two groups and sort them individually.
-        final List<BatteryDiffEntry> appEntries = new ArrayList<>();
-        mSystemEntries.clear();
-        entries.forEach(entry -> {
-            final String packageName = entry.getPackageName();
-            if (!isValidToShowEntry(packageName)) {
-                Log.w(TAG, "ignore showing item:" + packageName);
-                return;
-            }
-            if (entry.isSystemEntry()) {
-                mSystemEntries.add(entry);
-            } else {
-                appEntries.add(entry);
-            }
-            // Validates the usage time if users click a specific slot.
-            if (mTrapezoidIndex >= 0) {
-                validateUsageTime(entry);
-            }
-        });
-        Collections.sort(appEntries, BatteryDiffEntry.COMPARATOR);
-        Collections.sort(mSystemEntries, BatteryDiffEntry.COMPARATOR);
-        Log.d(TAG, String.format("addAllPreferences() app=%d system=%d",
-                appEntries.size(), mSystemEntries.size()));
-
         // Adds app entries to the list if it is not empty.
-        if (!appEntries.isEmpty()) {
-            addPreferenceToScreen(appEntries);
+        if (!batteryDiffData.getAppDiffEntryList().isEmpty()) {
+            addPreferenceToScreen(batteryDiffData.getAppDiffEntryList());
         }
         // Adds the expabable divider if we have system entries data.
-        if (!mSystemEntries.isEmpty()) {
+        if (!batteryDiffData.getSystemDiffEntryList().isEmpty()) {
             if (mExpandDividerPreference == null) {
                 mExpandDividerPreference = new ExpandDividerPreference(mPrefContext);
                 mExpandDividerPreference.setOnExpandListener(this);
@@ -469,11 +473,13 @@
     }
 
     private void refreshExpandUi() {
+        final List<BatteryDiffEntry> systemEntries = mBatteryUsageMap.get(mDailyChartIndex).get(
+                mHourlyChartIndex).getSystemDiffEntryList();
         if (mIsExpanded) {
-            addPreferenceToScreen(mSystemEntries);
+            addPreferenceToScreen(systemEntries);
         } else {
             // Removes and recycles all system entries to hide all of them.
-            for (BatteryDiffEntry entry : mSystemEntries) {
+            for (BatteryDiffEntry entry : systemEntries) {
                 final String prefKey = entry.mBatteryHistEntry.getKey();
                 final Preference pref = mAppListPrefGroup.findPreference(prefKey);
                 if (pref != null) {
@@ -499,11 +505,12 @@
     }
 
     private String getSlotInformation(boolean isApp, String slotInformation) {
+        // TODO: Updates the right slot information from daily and hourly chart selection.
         // Null means we show all information without a specific time slot.
         if (slotInformation == null) {
             return isApp
-                    ? mPrefContext.getString(R.string.battery_app_usage_for_past_24)
-                    : mPrefContext.getString(R.string.battery_system_usage_for_past_24);
+                    ? mPrefContext.getString(R.string.battery_app_usage)
+                    : mPrefContext.getString(R.string.battery_system_usage);
         } else {
             return isApp
                     ? mPrefContext.getString(R.string.battery_app_usage_for, slotInformation)
@@ -511,17 +518,33 @@
         }
     }
 
-    private String getSlotInformation() {
-        if (mTrapezoidIndex < 0) {
+    @VisibleForTesting
+    String getSlotInformation() {
+        if (mDailyTimestampFullTexts == null || mDailyViewModel == null
+                || mHourlyViewModels == null) {
+            // No data
             return null;
         }
-        final String fromHour = ConvertUtils.utcToLocalTimeHour(mPrefContext,
-                mBatteryHistoryKeys[mTrapezoidIndex * 2], mIs24HourFormat);
-        final String toHour = ConvertUtils.utcToLocalTimeHour(mPrefContext,
-                mBatteryHistoryKeys[(mTrapezoidIndex + 1) * 2], mIs24HourFormat);
-        return mIs24HourFormat
-                ? String.format("%s–%s", fromHour, toHour)
-                : String.format("%s – %s", fromHour, toHour);
+        if (isAllSelected()) {
+            return null;
+        }
+
+        final String selectedDayText = mDailyTimestampFullTexts.get(mDailyChartIndex);
+        if (mHourlyChartIndex == BatteryChartViewModel.SELECTED_INDEX_ALL) {
+            return selectedDayText;
+        }
+
+        final String fromHourText = mHourlyViewModels.get(mDailyChartIndex).texts().get(
+                mHourlyChartIndex);
+        final String toHourText = mHourlyViewModels.get(mDailyChartIndex).texts().get(
+                mHourlyChartIndex + 1);
+        final String selectedHourText =
+                String.format("%s%s%s", fromHourText, mIs24HourFormat ? "-" : " - ", toHourText);
+        if (isBatteryLevelDataInOneDay()) {
+            return selectedHourText;
+        }
+
+        return String.format("%s %s", selectedDayText, selectedHourText);
     }
 
     @VisibleForTesting
@@ -575,22 +598,7 @@
 
     @VisibleForTesting
     boolean isValidToShowSummary(String packageName) {
-        return !contains(packageName, mNotAllowShowSummaryPackages);
-    }
-
-    @VisibleForTesting
-    boolean isValidToShowEntry(String packageName) {
-        return !contains(packageName, mNotAllowShowEntryPackages);
-    }
-
-    @VisibleForTesting
-    void setTimestampLabel() {
-        if (mBatteryChartView == null || mBatteryHistoryKeys == null) {
-            return;
-        }
-        final long latestTimestamp =
-                mBatteryHistoryKeys[mBatteryHistoryKeys.length - 1];
-        mBatteryChartView.setLatestTimestamp(latestTimestamp);
+        return !DataProcessor.contains(packageName, mNotAllowShowSummaryPackages);
     }
 
     private void addFooterPreferenceIfNeeded(boolean containAppItems) {
@@ -605,60 +613,65 @@
         mHandler.post(() -> mPreferenceScreen.addPreference(mFooterPreference));
     }
 
-    private static boolean contains(String target, CharSequence[] packageNames) {
-        if (target != null && packageNames != null) {
-            for (CharSequence packageName : packageNames) {
-                if (TextUtils.equals(target, packageName)) {
-                    return true;
-                }
-            }
-        }
-        return false;
+    private boolean isBatteryLevelDataInOneDay() {
+        return mHourlyViewModels != null && mHourlyViewModels.size() == 1;
     }
 
-    @VisibleForTesting
-    static boolean validateUsageTime(BatteryDiffEntry entry) {
-        final long foregroundUsageTimeInMs = entry.mForegroundUsageTimeInMs;
-        final long backgroundUsageTimeInMs = entry.mBackgroundUsageTimeInMs;
-        final long totalUsageTimeInMs = foregroundUsageTimeInMs + backgroundUsageTimeInMs;
-        if (foregroundUsageTimeInMs > VALID_USAGE_TIME_DURATION
-                || backgroundUsageTimeInMs > VALID_USAGE_TIME_DURATION
-                || totalUsageTimeInMs > VALID_USAGE_TIME_DURATION) {
-            Log.e(TAG, "validateUsageTime() fail for\n" + entry);
-            return false;
+    private boolean isAllSelected() {
+        return (isBatteryLevelDataInOneDay()
+                || mDailyChartIndex == BatteryChartViewModel.SELECTED_INDEX_ALL)
+                && mHourlyChartIndex == BatteryChartViewModel.SELECTED_INDEX_ALL;
+    }
+
+    private static List<String> generateTimestampDayOfWeekTexts(@NonNull final Context context,
+            @NonNull final List<Long> timestamps, final boolean isAbbreviation) {
+        final ArrayList<String> texts = new ArrayList<>();
+        for (Long timestamp : timestamps) {
+            texts.add(ConvertUtils.utcToLocalTimeDayOfWeek(context, timestamp, isAbbreviation));
         }
-        return true;
+        return texts;
+    }
+
+    private static List<String> generateTimestampHourTexts(
+            @NonNull final Context context, @NonNull final List<Long> timestamps) {
+        final boolean is24HourFormat = DateFormat.is24HourFormat(context);
+        final ArrayList<String> texts = new ArrayList<>();
+        for (Long timestamp : timestamps) {
+            texts.add(ConvertUtils.utcToLocalTimeHour(context, timestamp, is24HourFormat));
+        }
+        return texts;
     }
 
     /** Used for {@link AppBatteryPreferenceController}. */
-    public static List<BatteryDiffEntry> getBatteryLast24HrUsageData(Context context) {
+    public static List<BatteryDiffEntry> getAppBatteryUsageData(Context context) {
         final long start = System.currentTimeMillis();
         final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap =
                 FeatureFactory.getFactory(context)
                         .getPowerUsageFeatureProvider(context)
-                        .getBatteryHistory(context);
+                        .getBatteryHistorySinceLastFullCharge(context);
         if (batteryHistoryMap == null || batteryHistoryMap.isEmpty()) {
             return null;
         }
-        Log.d(TAG, String.format("getBatteryLast24HrData() size=%d time=&d/ms",
+        Log.d(TAG, String.format("getBatterySinceLastFullChargeUsageData() size=%d time=%d/ms",
                 batteryHistoryMap.size(), (System.currentTimeMillis() - start)));
-        final Map<Integer, List<BatteryDiffEntry>> batteryIndexedMap =
-                ConvertUtils.getIndexedUsageMap(
-                        context,
-                        /*timeSlotSize=*/ CHART_LEVEL_ARRAY_SIZE - 1,
-                        getBatteryHistoryKeys(batteryHistoryMap),
-                        batteryHistoryMap,
-                        /*purgeLowPercentageAndFakeData=*/ true);
-        return batteryIndexedMap.get(BatteryChartView.SELECTED_INDEX_ALL);
+
+        final Map<Integer, Map<Integer, BatteryDiffData>> batteryUsageData =
+                DataProcessor.getBatteryUsageData(context, batteryHistoryMap);
+        return batteryUsageData == null
+                ? null
+                : batteryUsageData
+                        .get(BatteryChartViewModel.SELECTED_INDEX_ALL)
+                        .get(BatteryChartViewModel.SELECTED_INDEX_ALL)
+                        .getAppDiffEntryList();
     }
 
     /** Used for {@link AppBatteryPreferenceController}. */
-    public static BatteryDiffEntry getBatteryLast24HrUsageData(
+    public static BatteryDiffEntry getAppBatteryUsageData(
             Context context, String packageName, int userId) {
         if (packageName == null) {
             return null;
         }
-        final List<BatteryDiffEntry> entries = getBatteryLast24HrUsageData(context);
+        final List<BatteryDiffEntry> entries = getAppBatteryUsageData(context);
         if (entries == null) {
             return null;
         }
@@ -673,65 +686,4 @@
         }
         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/BatteryChartView.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryChartView.java
index 427388b..e668b37 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/BatteryChartView.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryChartView.java
@@ -18,6 +18,7 @@
 import static com.android.settings.Utils.formatPercentage;
 
 import static java.lang.Math.round;
+import static java.util.Objects.requireNonNull;
 
 import android.accessibilityservice.AccessibilityServiceInfo;
 import android.content.Context;
@@ -29,8 +30,6 @@
 import android.graphics.Path;
 import android.graphics.Rect;
 import android.os.Handler;
-import android.text.format.DateFormat;
-import android.text.format.DateUtils;
 import android.util.AttributeSet;
 import android.util.Log;
 import android.view.HapticFeedbackConstants;
@@ -39,6 +38,7 @@
 import android.view.accessibility.AccessibilityManager;
 import android.widget.TextView;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.VisibleForTesting;
 import androidx.appcompat.widget.AppCompatImageView;
 
@@ -46,7 +46,7 @@
 import com.android.settings.overlay.FeatureFactory;
 import com.android.settingslib.Utils;
 
-import java.time.Clock;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Locale;
@@ -58,36 +58,26 @@
     private static final List<String> ACCESSIBILITY_SERVICE_NAMES =
             Arrays.asList("SwitchAccessService", "TalkBackService", "JustSpeakService");
 
-    private static final int DEFAULT_TRAPEZOID_COUNT = 12;
-    private static final int DEFAULT_TIMESTAMP_COUNT = 4;
-    private static final int TIMESTAMP_GAPS_COUNT = DEFAULT_TIMESTAMP_COUNT - 1;
     private static final int DIVIDER_COLOR = Color.parseColor("#CDCCC5");
     private static final long UPDATE_STATE_DELAYED_TIME = 500L;
 
-    /** Selects all trapezoid shapes. */
-    public static final int SELECTED_INDEX_ALL = -1;
-    public static final int SELECTED_INDEX_INVALID = -2;
-
     /** A callback listener for selected group index is updated. */
     public interface OnSelectListener {
         /** The callback function for selected group index is updated. */
         void onSelect(int trapezoidIndex);
     }
 
+    private BatteryChartViewModel mViewModel;
+
     private int mDividerWidth;
     private int mDividerHeight;
-    private int mTrapezoidCount;
     private float mTrapezoidVOffset;
     private float mTrapezoidHOffset;
     private boolean mIsSlotsClickabled;
     private String[] mPercentages = getPercentages();
 
     @VisibleForTesting
-    int mHoveredIndex = SELECTED_INDEX_INVALID;
-    @VisibleForTesting
-    int mSelectedIndex = SELECTED_INDEX_INVALID;
-    @VisibleForTesting
-    String[] mTimestamps;
+    int mHoveredIndex = BatteryChartViewModel.SELECTED_INDEX_INVALID;
 
     // Colors for drawing the trapezoid shape and dividers.
     private int mTrapezoidColor;
@@ -98,25 +88,26 @@
     private final Rect mIndent = new Rect();
     private final Rect[] mPercentageBounds =
             new Rect[]{new Rect(), new Rect(), new Rect()};
-    // For drawing the timestamp information.
-    private final Rect[] mTimestampsBounds =
-            new Rect[]{new Rect(), new Rect(), new Rect(), new Rect()};
+    // For drawing the axis label information.
+    private final List<Rect> mAxisLabelsBounds = new ArrayList<>();
+
 
     @VisibleForTesting
     Handler mHandler = new Handler();
     @VisibleForTesting
     final Runnable mUpdateClickableStateRun = () -> updateClickableState();
 
-    private int[] mLevels;
     private Paint mTextPaint;
     private Paint mDividerPaint;
     private Paint mTrapezoidPaint;
 
     @VisibleForTesting
     Paint mTrapezoidCurvePaint = null;
-    private TrapezoidSlot[] mTrapezoidSlots;
+    @VisibleForTesting
+    TrapezoidSlot[] mTrapezoidSlots;
     // Records the location to calculate selected index.
-    private float mTouchUpEventX = Float.MIN_VALUE;
+    @VisibleForTesting
+    float mTouchUpEventX = Float.MIN_VALUE;
     private BatteryChartView.OnSelectListener mOnSelectListener;
 
     public BatteryChartView(Context context) {
@@ -128,57 +119,25 @@
         initializeColors(context);
         // Registers the click event listener.
         setOnClickListener(this);
-        setSelectedIndex(SELECTED_INDEX_ALL);
-        setTrapezoidCount(DEFAULT_TRAPEZOID_COUNT);
         setClickable(false);
-        setLatestTimestamp(0);
+        requestLayout();
     }
 
-    /** Sets the total trapezoid count for drawing. */
-    public void setTrapezoidCount(int trapezoidCount) {
-        Log.i(TAG, "trapezoidCount:" + trapezoidCount);
-        mTrapezoidCount = trapezoidCount;
-        mTrapezoidSlots = new TrapezoidSlot[trapezoidCount];
-        // Allocates the trapezoid slot array.
-        for (int index = 0; index < trapezoidCount; index++) {
-            mTrapezoidSlots[index] = new TrapezoidSlot();
-        }
-        invalidate();
-    }
-
-    /** Sets all levels value to draw the trapezoid shape */
-    public void setLevels(int[] levels) {
-        Log.d(TAG, "setLevels() " + (levels == null ? "null" : levels.length));
-        if (levels == null) {
-            mLevels = null;
-            return;
-        }
-        // We should provide trapezoid count + 1 data to draw all trapezoids.
-        mLevels = levels.length == mTrapezoidCount + 1 ? levels : null;
-        setClickable(false);
-        invalidate();
-        if (mLevels == null) {
-            return;
-        }
-        // Sets the chart is clickable if there is at least one valid item in it.
-        for (int index = 0; index < mLevels.length - 1; index++) {
-            if (mLevels[index] != 0 && mLevels[index + 1] != 0) {
-                setClickable(true);
-                break;
-            }
-        }
-    }
-
-    /** Sets the selected group index to draw highlight effect. */
-    public void setSelectedIndex(int index) {
-        if (mSelectedIndex != index) {
-            mSelectedIndex = index;
+    /** Sets the data model of this view. */
+    public void setViewModel(BatteryChartViewModel viewModel) {
+        if (viewModel == null) {
+            mViewModel = null;
             invalidate();
-            // Callbacks to the listener if we have.
-            if (mOnSelectListener != null) {
-                mOnSelectListener.onSelect(mSelectedIndex);
-            }
+            return;
         }
+
+        Log.d(TAG, String.format("setViewModel(): size: %d, selectedIndex: %d.",
+                viewModel.size(), viewModel.selectedIndex()));
+        mViewModel = viewModel;
+        initializeAxisLabelsBounds();
+        initializeTrapezoidSlots(viewModel.size() - 1);
+        setClickable(hasAnyValidTrapezoid(viewModel));
+        requestLayout();
     }
 
     /** Sets the callback to monitor the selected group index. */
@@ -195,29 +154,6 @@
         } else {
             mTextPaint = null;
         }
-        setVisibility(View.VISIBLE);
-        requestLayout();
-    }
-
-    /** Sets the latest timestamp for drawing into x-axis information. */
-    public void setLatestTimestamp(long latestTimestamp) {
-        if (latestTimestamp == 0) {
-            latestTimestamp = Clock.systemUTC().millis();
-        }
-        if (mTimestamps == null) {
-            mTimestamps = new String[DEFAULT_TIMESTAMP_COUNT];
-        }
-        final long timeSlotOffset =
-                DateUtils.HOUR_IN_MILLIS * (/*total 24 hours*/ 24 / TIMESTAMP_GAPS_COUNT);
-        final boolean is24HourFormat = DateFormat.is24HourFormat(getContext());
-        for (int index = 0; index < DEFAULT_TIMESTAMP_COUNT; index++) {
-            mTimestamps[index] =
-                    ConvertUtils.utcToLocalTimeHour(
-                            getContext(),
-                            latestTimestamp - (TIMESTAMP_GAPS_COUNT - index)
-                                    * timeSlotOffset,
-                            is24HourFormat);
-        }
         requestLayout();
     }
 
@@ -226,6 +162,7 @@
         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
         // Measures text bounds and updates indent configuration.
         if (mTextPaint != null) {
+            mTextPaint.setTextAlign(Paint.Align.LEFT);
             for (int index = 0; index < mPercentages.length; index++) {
                 mTextPaint.getTextBounds(
                         mPercentages[index], 0, mPercentages[index].length(),
@@ -235,15 +172,14 @@
             mIndent.top = mPercentageBounds[0].height();
             mIndent.right = mPercentageBounds[0].width() + mTextPadding;
 
-            if (mTimestamps != null) {
-                int maxHeight = 0;
-                for (int index = 0; index < DEFAULT_TIMESTAMP_COUNT; index++) {
-                    mTextPaint.getTextBounds(
-                            mTimestamps[index], 0, mTimestamps[index].length(),
-                            mTimestampsBounds[index]);
-                    maxHeight = Math.max(maxHeight, mTimestampsBounds[index].height());
+            if (mViewModel != null) {
+                int maxTop = 0;
+                for (int index = 0; index < mViewModel.size(); index++) {
+                    final String text = mViewModel.texts().get(index);
+                    mTextPaint.getTextBounds(text, 0, text.length(), mAxisLabelsBounds.get(index));
+                    maxTop = Math.max(maxTop, -mAxisLabelsBounds.get(index).top);
                 }
-                mIndent.bottom = maxHeight + round(mTextPadding * 1.5f);
+                mIndent.bottom = maxTop + round(mTextPadding * 2f);
             }
             Log.d(TAG, "setIndent:" + mPercentageBounds[0]);
         } else {
@@ -254,7 +190,12 @@
     @Override
     public void draw(Canvas canvas) {
         super.draw(canvas);
+        // Before mLevels initialized, the count of trapezoids is unknown. Only draws the
+        // horizontal percentages and dividers.
         drawHorizontalDividers(canvas);
+        if (mViewModel == null) {
+            return;
+        }
         drawVerticalDividers(canvas);
         drawTrapezoids(canvas);
     }
@@ -294,7 +235,7 @@
     public void onHoverChanged(boolean hovered) {
         super.onHoverChanged(hovered);
         if (!hovered) {
-            mHoveredIndex = SELECTED_INDEX_INVALID; // reset
+            mHoveredIndex = BatteryChartViewModel.SELECTED_INDEX_INVALID; // reset
             invalidate();
         }
     }
@@ -307,15 +248,15 @@
         }
         final int trapezoidIndex = getTrapezoidIndex(mTouchUpEventX);
         // Ignores the click event if the level is zero.
-        if (trapezoidIndex == SELECTED_INDEX_INVALID
-                || !isValidToDraw(trapezoidIndex)) {
+        if (trapezoidIndex == BatteryChartViewModel.SELECTED_INDEX_INVALID
+                || !isValidToDraw(mViewModel, trapezoidIndex)) {
             return;
         }
-        // Selects all if users click the same trapezoid item two times.
-        if (trapezoidIndex == mSelectedIndex) {
-            setSelectedIndex(SELECTED_INDEX_ALL);
-        } else {
-            setSelectedIndex(trapezoidIndex);
+        if (mOnSelectListener != null) {
+            // Selects all if users click the same trapezoid item two times.
+            mOnSelectListener.onSelect(
+                    trapezoidIndex == mViewModel.selectedIndex()
+                            ? BatteryChartViewModel.SELECTED_INDEX_ALL : trapezoidIndex);
         }
         view.performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK);
     }
@@ -364,8 +305,8 @@
             mTrapezoidCurvePaint.setStrokeWidth(mDividerWidth * 2);
         } else if (mIsSlotsClickabled) {
             mTrapezoidCurvePaint = null;
-            // Sets levels again to force update the click state.
-            setLevels(mLevels);
+            // Sets view model again to force update the click state.
+            setViewModel(mViewModel);
         }
         invalidate();
     }
@@ -380,6 +321,13 @@
         super.setClickable(clickable);
     }
 
+    private void initializeTrapezoidSlots(int count) {
+        mTrapezoidSlots = new TrapezoidSlot[count];
+        for (int index = 0; index < mTrapezoidSlots.length; index++) {
+            mTrapezoidSlots[index] = new TrapezoidSlot();
+        }
+    }
+
     private void initializeColors(Context context) {
         setBackgroundColor(Color.TRANSPARENT);
         mTrapezoidSolidColor = Utils.getColorAccentDefaultColor(context);
@@ -434,10 +382,10 @@
 
     private void drawPercentage(Canvas canvas, int index, float offsetY) {
         if (mTextPaint != null) {
+            mTextPaint.setTextAlign(Paint.Align.RIGHT);
             canvas.drawText(
                     mPercentages[index],
-                    getWidth() - mPercentageBounds[index].width()
-                            - mPercentageBounds[index].left,
+                    getWidth(),
                     offsetY + mPercentageBounds[index].height() * .5f,
                     mTextPaint);
         }
@@ -445,9 +393,9 @@
 
     private void drawVerticalDividers(Canvas canvas) {
         final int width = getWidth() - mIndent.right;
-        final int dividerCount = mTrapezoidCount + 1;
+        final int dividerCount = mTrapezoidSlots.length + 1;
         final float dividerSpace = dividerCount * mDividerWidth;
-        final float unitWidth = (width - dividerSpace) / (float) mTrapezoidCount;
+        final float unitWidth = (width - dividerSpace) / (float) mTrapezoidSlots.length;
         final float bottomY = getHeight() - mIndent.bottom;
         final float startY = bottomY - mDividerHeight;
         final float trapezoidSlotOffset = mTrapezoidHOffset + mDividerWidth * .5f;
@@ -463,66 +411,140 @@
             }
             startX = nextX;
         }
-        // Draws the timestamp slot information.
-        if (mTimestamps != null) {
-            final float[] xOffsets = new float[DEFAULT_TIMESTAMP_COUNT];
-            final float baselineX = mDividerWidth * .5f;
-            final float offsetX = mDividerWidth + unitWidth;
-            final int slotBarOffset = (/*total 12 bars*/ 12) / TIMESTAMP_GAPS_COUNT;
-            for (int index = 0; index < DEFAULT_TIMESTAMP_COUNT; index++) {
-                xOffsets[index] = baselineX + index * offsetX * slotBarOffset;
+        // Draws the axis label slot information.
+        if (mViewModel != null) {
+            final float baselineY = getHeight() - mTextPadding * 1.5f;
+            Rect[] axisLabelDisplayAreas;
+            switch (mViewModel.axisLabelPosition()) {
+                case CENTER_OF_TRAPEZOIDS:
+                    axisLabelDisplayAreas = getAxisLabelDisplayAreas(
+                            /* size= */ mViewModel.size() - 1,
+                            /* baselineX= */ mDividerWidth + unitWidth * .5f,
+                            /* offsetX= */ mDividerWidth + unitWidth,
+                            baselineY,
+                            /* shiftFirstAndLast= */ false);
+                    break;
+                case BETWEEN_TRAPEZOIDS:
+                default:
+                    axisLabelDisplayAreas = getAxisLabelDisplayAreas(
+                            /* size= */ mViewModel.size(),
+                            /* baselineX= */ mDividerWidth * .5f,
+                            /* offsetX= */ mDividerWidth + unitWidth,
+                            baselineY,
+                            /* shiftFirstAndLast= */ true);
+                    break;
             }
-            drawTimestamp(canvas, xOffsets);
+            drawAxisLabels(canvas, axisLabelDisplayAreas, baselineY);
         }
     }
 
-    private void drawTimestamp(Canvas canvas, float[] xOffsets) {
-        // Draws the 1st timestamp info.
-        canvas.drawText(
-                mTimestamps[0],
-                xOffsets[0] - mTimestampsBounds[0].left,
-                getTimestampY(0), mTextPaint);
-        final int latestIndex = DEFAULT_TIMESTAMP_COUNT - 1;
-        // Draws the last timestamp info.
-        canvas.drawText(
-                mTimestamps[latestIndex],
-                xOffsets[latestIndex] - mTimestampsBounds[latestIndex].width()
-                        - mTimestampsBounds[latestIndex].left,
-                getTimestampY(latestIndex), mTextPaint);
-        // Draws the rest of timestamp info since it is located in the center.
-        for (int index = 1; index <= DEFAULT_TIMESTAMP_COUNT - 2; index++) {
-            canvas.drawText(
-                    mTimestamps[index],
-                    xOffsets[index]
-                            - (mTimestampsBounds[index].width() - mTimestampsBounds[index].left)
-                            * .5f,
-                    getTimestampY(index), mTextPaint);
+    /** Gets all the axis label texts displaying area positions if they are shown. */
+    private Rect[] getAxisLabelDisplayAreas(final int size, final float baselineX,
+            final float offsetX, final float baselineY, final boolean shiftFirstAndLast) {
+        final Rect[] result = new Rect[size];
+        for (int index = 0; index < result.length; index++) {
+            final float width = mAxisLabelsBounds.get(index).width();
+            float middle = baselineX + index * offsetX;
+            if (shiftFirstAndLast) {
+                if (index == 0) {
+                    middle += width * .5f;
+                }
+                if (index == size - 1) {
+                    middle -= width * .5f;
+                }
+            }
+            final float left = middle - width * .5f;
+            final float right = left + width;
+            final float top = baselineY + mAxisLabelsBounds.get(index).top;
+            final float bottom = top + mAxisLabelsBounds.get(index).height();
+            result[index] = new Rect(round(left), round(top), round(right), round(bottom));
+        }
+        return result;
+    }
 
+    private void drawAxisLabels(Canvas canvas, final Rect[] displayAreas, final float baselineY) {
+        final int lastIndex = displayAreas.length - 1;
+        // Suppose first and last labels are always able to draw.
+        drawAxisLabelText(canvas, 0, displayAreas[0], baselineY);
+        drawAxisLabelText(canvas, lastIndex, displayAreas[lastIndex], baselineY);
+        drawAxisLabelsBetweenStartIndexAndEndIndex(canvas, displayAreas, 0, lastIndex, baselineY);
+    }
+
+    /**
+     * Recursively draws axis labels between the start index and the end index. If the inner number
+     * can be exactly divided into 2 parts, check and draw the middle index label and then
+     * recursively draw the 2 parts. Otherwise, divide into 3 parts. Check and draw the middle two
+     * labels and then recursively draw the 3 parts. If there are any overlaps, skip drawing and go
+     * back to the uplevel of the recursion.
+     */
+    private void drawAxisLabelsBetweenStartIndexAndEndIndex(Canvas canvas,
+            final Rect[] displayAreas, final int startIndex, final int endIndex,
+            final float baselineY) {
+        if (endIndex - startIndex <= 1) {
+            return;
+        }
+        if ((endIndex - startIndex) % 2 == 0) {
+            int middleIndex = (startIndex + endIndex) / 2;
+            if (hasOverlap(displayAreas, startIndex, middleIndex)
+                    || hasOverlap(displayAreas, middleIndex, endIndex)) {
+                return;
+            }
+            drawAxisLabelText(canvas, middleIndex, displayAreas[middleIndex], baselineY);
+            drawAxisLabelsBetweenStartIndexAndEndIndex(
+                    canvas, displayAreas, startIndex, middleIndex, baselineY);
+            drawAxisLabelsBetweenStartIndexAndEndIndex(
+                    canvas, displayAreas, middleIndex, endIndex, baselineY);
+        } else {
+            int middleIndex1 = startIndex + round((endIndex - startIndex) / 3f);
+            int middleIndex2 = startIndex + round((endIndex - startIndex) * 2 / 3f);
+            if (hasOverlap(displayAreas, startIndex, middleIndex1)
+                    || hasOverlap(displayAreas, middleIndex1, middleIndex2)
+                    || hasOverlap(displayAreas, middleIndex2, endIndex)) {
+                return;
+            }
+            drawAxisLabelText(canvas, middleIndex1, displayAreas[middleIndex1], baselineY);
+            drawAxisLabelText(canvas, middleIndex2, displayAreas[middleIndex2], baselineY);
+            drawAxisLabelsBetweenStartIndexAndEndIndex(
+                    canvas, displayAreas, startIndex, middleIndex1, baselineY);
+            drawAxisLabelsBetweenStartIndexAndEndIndex(
+                    canvas, displayAreas, middleIndex1, middleIndex2, baselineY);
+            drawAxisLabelsBetweenStartIndexAndEndIndex(
+                    canvas, displayAreas, middleIndex2, endIndex, baselineY);
         }
     }
 
-    private int getTimestampY(int index) {
-        return getHeight() - mTimestampsBounds[index].height()
-                + (mTimestampsBounds[index].height() + mTimestampsBounds[index].top)
-                + round(mTextPadding * 1.5f);
+    private boolean hasOverlap(
+            final Rect[] displayAreas, final int leftIndex, final int rightIndex) {
+        return displayAreas[leftIndex].right + mTextPadding * 2f > displayAreas[rightIndex].left;
+    }
+
+    private void drawAxisLabelText(
+            Canvas canvas, final int index, final Rect displayArea, final float baselineY) {
+        mTextPaint.setTextAlign(Paint.Align.CENTER);
+        canvas.drawText(
+                mViewModel.texts().get(index),
+                displayArea.centerX(),
+                baselineY,
+                mTextPaint);
     }
 
     private void drawTrapezoids(Canvas canvas) {
         // Ignores invalid trapezoid data.
-        if (mLevels == null) {
+        if (mViewModel == null) {
             return;
         }
         final float trapezoidBottom =
                 getHeight() - mIndent.bottom - mDividerHeight - mDividerWidth
                         - mTrapezoidVOffset;
-        final float availableSpace = trapezoidBottom - mDividerWidth * .5f - mIndent.top;
+        final float availableSpace =
+                trapezoidBottom - mDividerWidth * .5f - mIndent.top - mTrapezoidVOffset;
         final float unitHeight = availableSpace / 100f;
         // Draws all trapezoid shapes into the canvas.
         final Path trapezoidPath = new Path();
         Path trapezoidCurvePath = null;
-        for (int index = 0; index < mTrapezoidCount; index++) {
+        for (int index = 0; index < mTrapezoidSlots.length; index++) {
             // Not draws the trapezoid for corner or not initialization cases.
-            if (!isValidToDraw(index)) {
+            if (!isValidToDraw(mViewModel, index)) {
                 if (mTrapezoidCurvePaint != null && trapezoidCurvePath != null) {
                     canvas.drawPath(trapezoidCurvePath, mTrapezoidCurvePaint);
                     trapezoidCurvePath = null;
@@ -530,17 +552,18 @@
                 continue;
             }
             // Configures the trapezoid paint color.
-            final int trapezoidColor =
-                    !mIsSlotsClickabled
-                            ? mTrapezoidColor
-                            : mSelectedIndex == index || mSelectedIndex == SELECTED_INDEX_ALL
-                                    ? mTrapezoidSolidColor : mTrapezoidColor;
+            final int trapezoidColor = mIsSlotsClickabled && (mViewModel.selectedIndex() == index
+                    || mViewModel.selectedIndex() == BatteryChartViewModel.SELECTED_INDEX_ALL)
+                    ? mTrapezoidSolidColor : mTrapezoidColor;
             final boolean isHoverState =
-                    mIsSlotsClickabled && mHoveredIndex == index && isValidToDraw(mHoveredIndex);
+                    mIsSlotsClickabled && mHoveredIndex == index
+                            && isValidToDraw(mViewModel, mHoveredIndex);
             mTrapezoidPaint.setColor(isHoverState ? mTrapezoidHoverColor : trapezoidColor);
 
-            final float leftTop = round(trapezoidBottom - mLevels[index] * unitHeight);
-            final float rightTop = round(trapezoidBottom - mLevels[index + 1] * unitHeight);
+            final float leftTop = round(
+                    trapezoidBottom - requireNonNull(mViewModel.levels().get(index)) * unitHeight);
+            final float rightTop = round(trapezoidBottom
+                    - requireNonNull(mViewModel.levels().get(index + 1)) * unitHeight);
             trapezoidPath.reset();
             trapezoidPath.moveTo(mTrapezoidSlots[index].mLeft, trapezoidBottom);
             trapezoidPath.lineTo(mTrapezoidSlots[index].mLeft, leftTop);
@@ -579,15 +602,37 @@
                 return index;
             }
         }
-        return SELECTED_INDEX_INVALID;
+        return BatteryChartViewModel.SELECTED_INDEX_INVALID;
     }
 
-    private boolean isValidToDraw(int trapezoidIndex) {
-        return mLevels != null
+    private void initializeAxisLabelsBounds() {
+        mAxisLabelsBounds.clear();
+        for (int i = 0; i < mViewModel.size(); i++) {
+            mAxisLabelsBounds.add(new Rect());
+        }
+    }
+
+    private static boolean isTrapezoidValid(
+            @NonNull BatteryChartViewModel viewModel, int trapezoidIndex) {
+        return viewModel.levels().get(trapezoidIndex) != null
+                && viewModel.levels().get(trapezoidIndex + 1) != null;
+    }
+
+    private static boolean isValidToDraw(BatteryChartViewModel viewModel, int trapezoidIndex) {
+        return viewModel != null
                 && trapezoidIndex >= 0
-                && trapezoidIndex < mLevels.length - 1
-                && mLevels[trapezoidIndex] != 0
-                && mLevels[trapezoidIndex + 1] != 0;
+                && trapezoidIndex < viewModel.size() - 1
+                && isTrapezoidValid(viewModel, trapezoidIndex);
+    }
+
+    private static boolean hasAnyValidTrapezoid(@NonNull BatteryChartViewModel viewModel) {
+        // Sets the chart is clickable if there is at least one valid item in it.
+        for (int trapezoidIndex = 0; trapezoidIndex < viewModel.size() - 1; trapezoidIndex++) {
+            if (isTrapezoidValid(viewModel, trapezoidIndex)) {
+                return true;
+            }
+        }
+        return false;
     }
 
     private static String[] getPercentages() {
@@ -621,7 +666,8 @@
     }
 
     // A container class for each trapezoid left and right location.
-    private static final class TrapezoidSlot {
+    @VisibleForTesting
+    static final class TrapezoidSlot {
         public float mLeft;
         public float mRight;
 
diff --git a/src/com/android/settings/fuelgauge/batteryusage/BatteryChartViewModel.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryChartViewModel.java
new file mode 100644
index 0000000..ac01bfd
--- /dev/null
+++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryChartViewModel.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.fuelgauge.batteryusage;
+
+import androidx.annotation.NonNull;
+import androidx.core.util.Preconditions;
+
+import java.util.List;
+import java.util.Locale;
+import java.util.Objects;
+
+/** The view model of {@code BatteryChartView} */
+class BatteryChartViewModel {
+    private static final String TAG = "BatteryChartViewModel";
+
+    public static final int SELECTED_INDEX_ALL = -1;
+    public static final int SELECTED_INDEX_INVALID = -2;
+
+    // We need at least 2 levels to draw a trapezoid.
+    private static final int MIN_LEVELS_DATA_SIZE = 2;
+
+    enum AxisLabelPosition {
+        BETWEEN_TRAPEZOIDS,
+        CENTER_OF_TRAPEZOIDS,
+    }
+
+    private final List<Integer> mLevels;
+    private final List<String> mTexts;
+    private final AxisLabelPosition mAxisLabelPosition;
+    private int mSelectedIndex = SELECTED_INDEX_ALL;
+
+    BatteryChartViewModel(
+            @NonNull List<Integer> levels, @NonNull List<String> texts,
+            @NonNull AxisLabelPosition axisLabelPosition) {
+        Preconditions.checkArgument(
+                levels.size() == texts.size() && levels.size() >= MIN_LEVELS_DATA_SIZE,
+                String.format(Locale.ENGLISH,
+                        "Invalid BatteryChartViewModel levels.size: %d, texts.size: %d.",
+                        levels.size(), texts.size()));
+        mLevels = levels;
+        mTexts = texts;
+        mAxisLabelPosition = axisLabelPosition;
+    }
+
+    public int size() {
+        return mLevels.size();
+    }
+
+    public List<Integer> levels() {
+        return mLevels;
+    }
+
+    public List<String> texts() {
+        return mTexts;
+    }
+
+    public AxisLabelPosition axisLabelPosition() {
+        return mAxisLabelPosition;
+    }
+
+    public int selectedIndex() {
+        return mSelectedIndex;
+    }
+
+    public void setSelectedIndex(int index) {
+        mSelectedIndex = index;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mLevels, mTexts, mSelectedIndex, mAxisLabelPosition);
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (this == other) {
+            return true;
+        } else if (!(other instanceof BatteryChartViewModel)) {
+            return false;
+        }
+        final BatteryChartViewModel batteryChartViewModel = (BatteryChartViewModel) other;
+        return Objects.equals(mLevels, batteryChartViewModel.mLevels)
+                && Objects.equals(mTexts, batteryChartViewModel.mTexts)
+                && mAxisLabelPosition == batteryChartViewModel.mAxisLabelPosition
+                && mSelectedIndex == batteryChartViewModel.mSelectedIndex;
+    }
+
+    @Override
+    public String toString() {
+        return String.format(Locale.ENGLISH,
+                "levels: %s,\ntexts: %s,\naxisLabelPosition: %s, selectedIndex: %d",
+                Objects.toString(mLevels), Objects.toString(mTexts), mAxisLabelPosition,
+                mSelectedIndex);
+    }
+}
diff --git a/src/com/android/settings/fuelgauge/batteryusage/BatteryDiffData.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryDiffData.java
new file mode 100644
index 0000000..b5d4dde
--- /dev/null
+++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryDiffData.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.fuelgauge.batteryusage;
+
+import androidx.annotation.NonNull;
+
+import java.util.Collections;
+import java.util.List;
+
+/** Wraps the battery usage diff data for each entry used for battery usage app list. */
+public class BatteryDiffData {
+    private final List<BatteryDiffEntry> mAppEntries;
+    private final List<BatteryDiffEntry> mSystemEntries;
+
+    /** Constructor for the diff entries which already have totalConsumePower value. */
+    public BatteryDiffData(
+            @NonNull List<BatteryDiffEntry> appDiffEntries,
+            @NonNull List<BatteryDiffEntry> systemDiffEntries) {
+        mAppEntries = appDiffEntries;
+        mSystemEntries = systemDiffEntries;
+        sortEntries();
+    }
+
+    /** Constructor for the diff entries which have not set totalConsumePower value. */
+    public BatteryDiffData(
+            @NonNull List<BatteryDiffEntry> appDiffEntries,
+            @NonNull List<BatteryDiffEntry> systemDiffEntries,
+            final double totalConsumePower) {
+        mAppEntries = appDiffEntries;
+        mSystemEntries = systemDiffEntries;
+        setTotalConsumePowerForAllEntries(totalConsumePower);
+        sortEntries();
+    }
+
+    public List<BatteryDiffEntry> getAppDiffEntryList() {
+        return mAppEntries;
+    }
+
+    public List<BatteryDiffEntry> getSystemDiffEntryList() {
+        return mSystemEntries;
+    }
+
+    // Sets total consume power for each entry.
+    private void setTotalConsumePowerForAllEntries(final double totalConsumePower) {
+        mAppEntries.forEach(diffEntry -> diffEntry.setTotalConsumePower(totalConsumePower));
+        mSystemEntries.forEach(diffEntry -> diffEntry.setTotalConsumePower(totalConsumePower));
+    }
+
+    // Sorts entries based on consumed percentage.
+    private void sortEntries() {
+        Collections.sort(mAppEntries, BatteryDiffEntry.COMPARATOR);
+        Collections.sort(mSystemEntries, BatteryDiffEntry.COMPARATOR);
+    }
+}
diff --git a/src/com/android/settings/fuelgauge/batteryusage/BatteryHistoryLoader.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryHistoryLoader.java
index 34606a5..83b2615 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/BatteryHistoryLoader.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryHistoryLoader.java
@@ -43,6 +43,6 @@
     public Map<Long, Map<String, BatteryHistEntry>> loadInBackground() {
         final PowerUsageFeatureProvider powerUsageFeatureProvider =
                 FeatureFactory.getFactory(mContext).getPowerUsageFeatureProvider(mContext);
-        return powerUsageFeatureProvider.getBatteryHistory(mContext);
+        return powerUsageFeatureProvider.getBatteryHistorySinceLastFullCharge(mContext);
     }
 }
diff --git a/src/com/android/settings/fuelgauge/batteryusage/BatteryHistoryPreference.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryHistoryPreference.java
index e125d17..71fd26c 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/BatteryHistoryPreference.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryHistoryPreference.java
@@ -49,7 +49,8 @@
 
     private TextView mSummaryView;
     private CharSequence mSummaryContent;
-    private BatteryChartView mBatteryChartView;
+    private BatteryChartView mDailyChartView;
+    private BatteryChartView mHourlyChartView;
     private BatteryChartPreferenceController mChartPreferenceController;
 
     public BatteryHistoryPreference(Context context, AttributeSet attrs) {
@@ -92,8 +93,8 @@
 
     void setChartPreferenceController(BatteryChartPreferenceController controller) {
         mChartPreferenceController = controller;
-        if (mBatteryChartView != null) {
-            mChartPreferenceController.setBatteryChartView(mBatteryChartView);
+        if (mDailyChartView != null && mHourlyChartView != null) {
+            mChartPreferenceController.setBatteryChartView(mDailyChartView, mHourlyChartView);
         }
     }
 
@@ -105,11 +106,14 @@
             return;
         }
         if (mIsChartGraphEnabled) {
-            mBatteryChartView = (BatteryChartView) view.findViewById(R.id.battery_chart);
-            mBatteryChartView.setCompanionTextView(
+            mDailyChartView = (BatteryChartView) view.findViewById(R.id.daily_battery_chart);
+            mDailyChartView.setCompanionTextView(
+                    (TextView) view.findViewById(R.id.companion_text));
+            mHourlyChartView = (BatteryChartView) view.findViewById(R.id.hourly_battery_chart);
+            mHourlyChartView.setCompanionTextView(
                     (TextView) view.findViewById(R.id.companion_text));
             if (mChartPreferenceController != null) {
-                mChartPreferenceController.setBatteryChartView(mBatteryChartView);
+                mChartPreferenceController.setBatteryChartView(mDailyChartView, mHourlyChartView);
             }
         } else {
             final TextView chargeView = (TextView) view.findViewById(R.id.charge);
diff --git a/src/com/android/settings/fuelgauge/batteryusage/BatteryLevelData.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryLevelData.java
new file mode 100644
index 0000000..4ff9eeb
--- /dev/null
+++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryLevelData.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.fuelgauge.batteryusage;
+
+import androidx.annotation.NonNull;
+import androidx.core.util.Preconditions;
+
+import java.util.List;
+import java.util.Locale;
+import java.util.Objects;
+
+/** Wraps the battery timestamp and level data used for battery usage chart. */
+public final class BatteryLevelData {
+    /** A container for the battery timestamp and level data. */
+    public static final class PeriodBatteryLevelData {
+        // The length of mTimestamps and mLevels must be the same. mLevels[index] might be null when
+        // there is no level data for the corresponding timestamp.
+        private final List<Long> mTimestamps;
+        private final List<Integer> mLevels;
+
+        public PeriodBatteryLevelData(
+                @NonNull List<Long> timestamps, @NonNull List<Integer> levels) {
+            Preconditions.checkArgument(timestamps.size() == levels.size(),
+                    /* errorMessage= */ "Timestamp: " + timestamps.size() + ", Level: "
+                            + levels.size());
+            mTimestamps = timestamps;
+            mLevels = levels;
+        }
+
+        public List<Long> getTimestamps() {
+            return mTimestamps;
+        }
+
+        public List<Integer> getLevels() {
+            return mLevels;
+        }
+
+        @Override
+        public String toString() {
+            return String.format(Locale.ENGLISH, "timestamps: %s; levels: %s",
+                    Objects.toString(mTimestamps), Objects.toString(mLevels));
+        }
+    }
+
+    /**
+     * There could be 2 cases for the daily battery levels:
+     * 1) length is 2: The usage data is within 1 day. Only contains start and end data, such as
+     *    data of 2022-01-01 06:00 and 2022-01-01 16:00.
+     * 2) length > 2: The usage data is more than 1 days. The data should be the start, end and 0am
+     *    data of every day between the start and end, such as data of 2022-01-01 06:00,
+     *    2022-01-02 00:00, 2022-01-03 00:00 and 2022-01-03 08:00.
+     */
+    private final PeriodBatteryLevelData mDailyBatteryLevels;
+    // The size of hourly data must be the size of daily data - 1.
+    private final List<PeriodBatteryLevelData> mHourlyBatteryLevelsPerDay;
+
+    public BatteryLevelData(
+            @NonNull PeriodBatteryLevelData dailyBatteryLevels,
+            @NonNull List<PeriodBatteryLevelData> hourlyBatteryLevelsPerDay) {
+        final long dailySize = dailyBatteryLevels.getTimestamps().size();
+        final long hourlySize = hourlyBatteryLevelsPerDay.size();
+        Preconditions.checkArgument(hourlySize == dailySize - 1,
+                /* errorMessage= */ "DailySize: " + dailySize + ", HourlySize: " + hourlySize);
+        mDailyBatteryLevels = dailyBatteryLevels;
+        mHourlyBatteryLevelsPerDay = hourlyBatteryLevelsPerDay;
+    }
+
+    public PeriodBatteryLevelData getDailyBatteryLevels() {
+        return mDailyBatteryLevels;
+    }
+
+    public List<PeriodBatteryLevelData> getHourlyBatteryLevelsPerDay() {
+        return mHourlyBatteryLevelsPerDay;
+    }
+
+    @Override
+    public String toString() {
+        return String.format(Locale.ENGLISH,
+                "dailyBatteryLevels: %s; hourlyBatteryLevelsPerDay: %s",
+                Objects.toString(mDailyBatteryLevels),
+                Objects.toString(mHourlyBatteryLevelsPerDay));
+    }
+}
+
diff --git a/src/com/android/settings/fuelgauge/batteryusage/ConvertUtils.java b/src/com/android/settings/fuelgauge/batteryusage/ConvertUtils.java
index 168fe0f..f04658d 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/ConvertUtils.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/ConvertUtils.java
@@ -140,7 +140,7 @@
 
     /** Converts UTC timestamp to local time hour data. */
     public static String utcToLocalTimeHour(
-            Context context, long timestamp, boolean is24HourFormat) {
+            final Context context, final long timestamp, final boolean is24HourFormat) {
         final Locale locale = getLocale(context);
         // e.g. for 12-hour format: 9 pm
         // e.g. for 24-hour format: 09:00
@@ -149,6 +149,15 @@
         return DateFormat.format(pattern, timestamp).toString().toLowerCase(locale);
     }
 
+    /** Converts UTC timestamp to local time day of week data. */
+    public static String utcToLocalTimeDayOfWeek(
+            final Context context, final long timestamp, final boolean isAbbreviation) {
+        final Locale locale = getLocale(context);
+        final String pattern = DateFormat.getBestDateTimePattern(locale,
+                isAbbreviation ? "E" : "EEEE");
+        return DateFormat.format(pattern, timestamp).toString();
+    }
+
     /** Gets indexed battery usage data for each corresponding time slot. */
     public static Map<Integer, List<BatteryDiffEntry>> getIndexedUsageMap(
             final Context context,
@@ -267,7 +276,7 @@
                 diffEntry.setTotalConsumePower(totalConsumePower);
             }
         }
-        insert24HoursData(BatteryChartView.SELECTED_INDEX_ALL, resultMap);
+        insert24HoursData(BatteryChartViewModel.SELECTED_INDEX_ALL, resultMap);
         resolveMultiUsersData(context, resultMap);
         if (purgeLowPercentageAndFakeData) {
             purgeLowPercentageAndFakeData(context, resultMap);
diff --git a/src/com/android/settings/fuelgauge/batteryusage/DataProcessor.java b/src/com/android/settings/fuelgauge/batteryusage/DataProcessor.java
new file mode 100644
index 0000000..125f879
--- /dev/null
+++ b/src/com/android/settings/fuelgauge/batteryusage/DataProcessor.java
@@ -0,0 +1,1058 @@
+/*
+ * 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.fuelgauge.batteryusage.ConvertUtils.utcToLocalTime;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.os.AsyncTask;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.text.TextUtils;
+import android.text.format.DateUtils;
+import android.util.ArraySet;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.settings.Utils;
+import com.android.settings.fuelgauge.BatteryUtils;
+import com.android.settings.overlay.FeatureFactory;
+import com.android.settingslib.fuelgauge.BatteryStatus;
+
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A utility class to process data loaded from database and make the data easy to use for battery
+ * usage UI.
+ */
+public final class DataProcessor {
+    private static final boolean DEBUG = false;
+    private static final String TAG = "DataProcessor";
+    private static final int MIN_DAILY_DATA_SIZE = 2;
+    private static final int MIN_TIMESTAMP_DATA_SIZE = 2;
+    private static final int MAX_DIFF_SECONDS_OF_UPPER_TIMESTAMP = 5;
+    // Maximum total time value for each hourly slot cumulative data at most 2 hours.
+    private static final float TOTAL_HOURLY_TIME_THRESHOLD = DateUtils.HOUR_IN_MILLIS * 2;
+    private static final Map<String, BatteryHistEntry> EMPTY_BATTERY_MAP = new HashMap<>();
+    private static final BatteryHistEntry EMPTY_BATTERY_HIST_ENTRY =
+            new BatteryHistEntry(new ContentValues());
+
+    @VisibleForTesting
+    static final double PERCENTAGE_OF_TOTAL_THRESHOLD = 1f;
+    @VisibleForTesting
+    static final int SELECTED_INDEX_ALL = BatteryChartViewModel.SELECTED_INDEX_ALL;
+
+    /** A fake package name to represent no BatteryEntry data. */
+    public static final String FAKE_PACKAGE_NAME = "fake_package";
+
+    /** A callback listener when battery usage loading async task is executed. */
+    public interface UsageMapAsyncResponse {
+        /** The callback function when batteryUsageMap is loaded. */
+        void onBatteryUsageMapLoaded(
+                Map<Integer, Map<Integer, BatteryDiffData>> batteryUsageMap);
+    }
+
+    private DataProcessor() {
+    }
+
+    /**
+     * @return Returns battery level data and start async task to compute battery diff usage data
+     * and load app labels + icons.
+     * Returns null if the input is invalid or not having at least 2 hours data.
+     */
+    @Nullable
+    public static BatteryLevelData getBatteryLevelData(
+            Context context,
+            @Nullable Handler handler,
+            @Nullable final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap,
+            final UsageMapAsyncResponse asyncResponseDelegate) {
+        if (batteryHistoryMap == null || batteryHistoryMap.isEmpty()) {
+            Log.d(TAG, "getBatteryLevelData() returns null");
+            return null;
+        }
+        handler = handler != null ? handler : new Handler(Looper.getMainLooper());
+        // Process raw history map data into hourly timestamps.
+        final Map<Long, Map<String, BatteryHistEntry>> processedBatteryHistoryMap =
+                getHistoryMapWithExpectedTimestamps(context, batteryHistoryMap);
+        // Wrap and processed history map into easy-to-use format for UI rendering.
+        final BatteryLevelData batteryLevelData =
+                getLevelDataThroughProcessedHistoryMap(context, processedBatteryHistoryMap);
+
+        // Start the async task to compute diff usage data and load labels and icons.
+        if (batteryLevelData != null) {
+            new ComputeUsageMapAndLoadItemsTask(
+                    context,
+                    handler,
+                    asyncResponseDelegate,
+                    batteryLevelData.getHourlyBatteryLevelsPerDay(),
+                    processedBatteryHistoryMap).execute();
+        }
+
+        return batteryLevelData;
+    }
+
+    /**
+     * @return Returns battery usage data of different entries.
+     * Returns null if the input is invalid or there is no enough data.
+     */
+    @Nullable
+    public static Map<Integer, Map<Integer, BatteryDiffData>> getBatteryUsageData(
+            Context context,
+            @Nullable final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap) {
+        if (batteryHistoryMap == null || batteryHistoryMap.isEmpty()) {
+            Log.d(TAG, "getBatteryLevelData() returns null");
+            return null;
+        }
+        // Process raw history map data into hourly timestamps.
+        final Map<Long, Map<String, BatteryHistEntry>> processedBatteryHistoryMap =
+                getHistoryMapWithExpectedTimestamps(context, batteryHistoryMap);
+        // Wrap and processed history map into easy-to-use format for UI rendering.
+        final BatteryLevelData batteryLevelData =
+                getLevelDataThroughProcessedHistoryMap(context, processedBatteryHistoryMap);
+        return batteryLevelData == null
+                ? null
+                : getBatteryUsageMap(
+                        context,
+                        batteryLevelData.getHourlyBatteryLevelsPerDay(),
+                        processedBatteryHistoryMap);
+    }
+
+    /**
+     * @return Returns whether the target is in the CharSequence array.
+     */
+    public 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;
+    }
+
+    /**
+     * @return Returns the processed history map which has interpolated to every hour data.
+     * The start and end timestamp must be the even hours.
+     * The keys of processed history map should contain every hour between the start and end
+     * timestamp. If there's no data in some key, the value will be the empty hashmap.
+     */
+    @VisibleForTesting
+    static Map<Long, Map<String, BatteryHistEntry>> getHistoryMapWithExpectedTimestamps(
+            Context context,
+            final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap) {
+        final long startTime = System.currentTimeMillis();
+        final List<Long> rawTimestampList = new ArrayList<>(batteryHistoryMap.keySet());
+        final Map<Long, Map<String, BatteryHistEntry>> resultMap = new HashMap();
+        if (rawTimestampList.isEmpty()) {
+            Log.d(TAG, "empty batteryHistoryMap in getHistoryMapWithExpectedTimestamps()");
+            return resultMap;
+        }
+        Collections.sort(rawTimestampList);
+        final List<Long> expectedTimestampList = getTimestampSlots(rawTimestampList);
+        final boolean isFromFullCharge =
+                isFromFullCharge(batteryHistoryMap.get(rawTimestampList.get(0)));
+        interpolateHistory(
+                context, rawTimestampList, expectedTimestampList, isFromFullCharge,
+                batteryHistoryMap, resultMap);
+        Log.d(TAG, String.format("getHistoryMapWithExpectedTimestamps() size=%d in %d/ms",
+                resultMap.size(), (System.currentTimeMillis() - startTime)));
+        return resultMap;
+    }
+
+    @VisibleForTesting
+    @Nullable
+    static BatteryLevelData getLevelDataThroughProcessedHistoryMap(
+            Context context,
+            final Map<Long, Map<String, BatteryHistEntry>> processedBatteryHistoryMap) {
+        final List<Long> timestampList = new ArrayList<>(processedBatteryHistoryMap.keySet());
+        Collections.sort(timestampList);
+        final List<Long> dailyTimestamps = getDailyTimestamps(timestampList);
+        // There should be at least the start and end timestamps. Otherwise, return null to not show
+        // data in usage chart.
+        if (dailyTimestamps.size() < MIN_DAILY_DATA_SIZE) {
+            return null;
+        }
+
+        final List<List<Long>> hourlyTimestamps = getHourlyTimestamps(dailyTimestamps);
+        final BatteryLevelData.PeriodBatteryLevelData dailyLevelData =
+                getPeriodBatteryLevelData(context, processedBatteryHistoryMap, dailyTimestamps);
+        final List<BatteryLevelData.PeriodBatteryLevelData> hourlyLevelData =
+                getHourlyPeriodBatteryLevelData(
+                        context, processedBatteryHistoryMap, hourlyTimestamps);
+        return new BatteryLevelData(dailyLevelData, hourlyLevelData);
+    }
+
+    /**
+     * Computes expected timestamp slots for last full charge, which will return hourly timestamps
+     * between start and end two even hour values.
+     */
+    @VisibleForTesting
+    static List<Long> getTimestampSlots(final List<Long> rawTimestampList) {
+        final List<Long> timestampSlots = new ArrayList<>();
+        final int rawTimestampListSize = rawTimestampList.size();
+        // If timestamp number is smaller than 2, the following computation is not necessary.
+        if (rawTimestampListSize < MIN_TIMESTAMP_DATA_SIZE) {
+            return timestampSlots;
+        }
+        final long rawStartTimestamp = rawTimestampList.get(0);
+        final long rawEndTimestamp = rawTimestampList.get(rawTimestampListSize - 1);
+        // No matter the start is from last full charge or 6 days ago, use the nearest even hour.
+        final long startTimestamp = getNearestEvenHourTimestamp(rawStartTimestamp);
+        // Use the even hour before the raw end timestamp as the end.
+        final long endTimestamp = getLastEvenHourBeforeTimestamp(rawEndTimestamp);
+        // If the start timestamp is later or equal the end one, return the empty list.
+        if (startTimestamp >= endTimestamp) {
+            return timestampSlots;
+        }
+        for (long timestamp = startTimestamp; timestamp <= endTimestamp;
+                timestamp += DateUtils.HOUR_IN_MILLIS) {
+            timestampSlots.add(timestamp);
+        }
+        return timestampSlots;
+    }
+
+    /**
+     * Computes expected daily timestamp slots.
+     *
+     * The valid result should be composed of 3 parts:
+     * 1) start timestamp
+     * 2) every 00:00 timestamp (default timezone) between the start and end
+     * 3) end timestamp
+     * Otherwise, returns an empty list.
+     */
+    @VisibleForTesting
+    static List<Long> getDailyTimestamps(final List<Long> timestampList) {
+        final List<Long> dailyTimestampList = new ArrayList<>();
+        // If timestamp number is smaller than 2, the following computation is not necessary.
+        if (timestampList.size() < MIN_TIMESTAMP_DATA_SIZE) {
+            return dailyTimestampList;
+        }
+        final long startTime = timestampList.get(0);
+        final long endTime = timestampList.get(timestampList.size() - 1);
+        long nextDay = getTimestampOfNextDay(startTime);
+        dailyTimestampList.add(startTime);
+        while (nextDay < endTime) {
+            dailyTimestampList.add(nextDay);
+            nextDay += DateUtils.DAY_IN_MILLIS;
+        }
+        dailyTimestampList.add(endTime);
+        return dailyTimestampList;
+    }
+
+    @VisibleForTesting
+    static boolean isFromFullCharge(@Nullable final Map<String, BatteryHistEntry> entryList) {
+        if (entryList == null) {
+            Log.d(TAG, "entryList is null in isFromFullCharge()");
+            return false;
+        }
+        final List<String> entryKeys = new ArrayList<>(entryList.keySet());
+        if (entryKeys.isEmpty()) {
+            Log.d(TAG, "empty entryList in isFromFullCharge()");
+            return false;
+        }
+        // The hist entries in the same timestamp should have same battery status and level.
+        // Checking the first one should be enough.
+        final BatteryHistEntry firstHistEntry = entryList.get(entryKeys.get(0));
+        return BatteryStatus.isCharged(firstHistEntry.mBatteryStatus, firstHistEntry.mBatteryLevel);
+    }
+
+    @VisibleForTesting
+    static long[] findNearestTimestamp(final List<Long> timestamps, final long target) {
+        final long[] results = new long[] {Long.MIN_VALUE, Long.MAX_VALUE};
+        // Searches the nearest lower and upper timestamp value.
+        timestamps.forEach(timestamp -> {
+            if (timestamp <= target && timestamp > results[0]) {
+                results[0] = timestamp;
+            }
+            if (timestamp >= target && timestamp < results[1]) {
+                results[1] = timestamp;
+            }
+        });
+        // Uses zero value to represent invalid searching result.
+        results[0] = results[0] == Long.MIN_VALUE ? 0 : results[0];
+        results[1] = results[1] == Long.MAX_VALUE ? 0 : results[1];
+        return results;
+    }
+
+    /**
+     * @return Returns the timestamp for 00:00 1 day after the given timestamp based on local
+     * timezone.
+     */
+    @VisibleForTesting
+    static long getTimestampOfNextDay(long timestamp) {
+        return getTimestampWithDayDiff(timestamp, /*dayDiff=*/ 1);
+    }
+
+    /**
+     *  Returns whether currentSlot will be used in daily chart.
+     */
+    @VisibleForTesting
+    static boolean isForDailyChart(final boolean isStartOrEnd, final long currentSlot) {
+        // The start and end timestamps will always be used in daily chart.
+        if (isStartOrEnd) {
+            return true;
+        }
+
+        // The timestamps for 00:00 will be used in daily chart.
+        final long startOfTheDay = getTimestampWithDayDiff(currentSlot, /*dayDiff=*/ 0);
+        return currentSlot == startOfTheDay;
+    }
+
+    /**
+     * @return Returns the indexed battery usage data for each corresponding time slot.
+     *
+     * There could be 2 cases of the returned value:
+     * 1) null: empty or invalid data.
+     * 2) non-null: must be a 2d map and composed by 3 parts:
+     *    1 - [SELECTED_INDEX_ALL][SELECTED_INDEX_ALL]
+     *    2 - [0][SELECTED_INDEX_ALL] ~ [maxDailyIndex][SELECTED_INDEX_ALL]
+     *    3 - [0][0] ~ [maxDailyIndex][maxHourlyIndex]
+     */
+    @VisibleForTesting
+    @Nullable
+    static Map<Integer, Map<Integer, BatteryDiffData>> getBatteryUsageMap(
+            final Context context,
+            final List<BatteryLevelData.PeriodBatteryLevelData> hourlyBatteryLevelsPerDay,
+            final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap) {
+        if (batteryHistoryMap.isEmpty()) {
+            return null;
+        }
+        final Map<Integer, Map<Integer, BatteryDiffData>> resultMap = new HashMap<>();
+        // Insert diff data from [0][0] to [maxDailyIndex][maxHourlyIndex].
+        insertHourlyUsageDiffData(
+                context, hourlyBatteryLevelsPerDay, batteryHistoryMap, resultMap);
+        // Insert diff data from [0][SELECTED_INDEX_ALL] to [maxDailyIndex][SELECTED_INDEX_ALL].
+        insertDailyUsageDiffData(hourlyBatteryLevelsPerDay, resultMap);
+        // Insert diff data [SELECTED_INDEX_ALL][SELECTED_INDEX_ALL].
+        insertAllUsageDiffData(resultMap);
+        purgeLowPercentageAndFakeData(context, resultMap);
+        if (!isUsageMapValid(resultMap, hourlyBatteryLevelsPerDay)) {
+            return null;
+        }
+        return resultMap;
+    }
+
+    /**
+     * Interpolates history map based on expected timestamp slots and processes the corner case when
+     * the expected start timestamp is earlier than what we have.
+     */
+    private static void interpolateHistory(
+            Context context,
+            final List<Long> rawTimestampList,
+            final List<Long> expectedTimestampSlots,
+            final boolean isFromFullCharge,
+            final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap,
+            final Map<Long, Map<String, BatteryHistEntry>> resultMap) {
+        if (rawTimestampList.isEmpty() || expectedTimestampSlots.isEmpty()) {
+            return;
+        }
+        final long expectedStartTimestamp = expectedTimestampSlots.get(0);
+        final long rawStartTimestamp = rawTimestampList.get(0);
+        int startIndex = 0;
+        // If the expected start timestamp is full charge or earlier than what we have, use the
+        // first data of what we have directly. This should be OK because the expected start
+        // timestamp is the nearest even hour of the raw start timestamp, their time diff is no
+        // more than 1 hour.
+        if (isFromFullCharge || expectedStartTimestamp < rawStartTimestamp) {
+            startIndex = 1;
+            resultMap.put(expectedStartTimestamp, batteryHistoryMap.get(rawStartTimestamp));
+        }
+        final int expectedTimestampSlotsSize = expectedTimestampSlots.size();
+        for (int index = startIndex; index < expectedTimestampSlotsSize; index++) {
+            final long currentSlot = expectedTimestampSlots.get(index);
+            final boolean isStartOrEnd = index == 0 || index == expectedTimestampSlotsSize - 1;
+            interpolateHistoryForSlot(
+                    context, currentSlot, rawTimestampList, batteryHistoryMap, resultMap,
+                    isStartOrEnd);
+        }
+    }
+
+    private static void interpolateHistoryForSlot(
+            Context context,
+            final long currentSlot,
+            final List<Long> rawTimestampList,
+            final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap,
+            final Map<Long, Map<String, BatteryHistEntry>> resultMap,
+            final boolean isStartOrEnd) {
+        final long[] nearestTimestamps = findNearestTimestamp(rawTimestampList, currentSlot);
+        final long lowerTimestamp = nearestTimestamps[0];
+        final long upperTimestamp = nearestTimestamps[1];
+        // Case 1: upper timestamp is zero since scheduler is delayed!
+        if (upperTimestamp == 0) {
+            log(context, "job scheduler is delayed", currentSlot, null);
+            resultMap.put(currentSlot, new HashMap<>());
+            return;
+        }
+        // Case 2: upper timestamp is closed to the current timestamp.
+        if ((upperTimestamp - currentSlot)
+                < MAX_DIFF_SECONDS_OF_UPPER_TIMESTAMP * DateUtils.SECOND_IN_MILLIS) {
+            log(context, "force align into the nearest slot", currentSlot, null);
+            resultMap.put(currentSlot, batteryHistoryMap.get(upperTimestamp));
+            return;
+        }
+        // Case 3: lower timestamp is zero before starting to collect data.
+        if (lowerTimestamp == 0) {
+            log(context, "no lower timestamp slot data", currentSlot, null);
+            resultMap.put(currentSlot, new HashMap<>());
+            return;
+        }
+        interpolateHistoryForSlot(context,
+                currentSlot, lowerTimestamp, upperTimestamp, batteryHistoryMap, resultMap,
+                isStartOrEnd);
+    }
+
+    private static void interpolateHistoryForSlot(
+            Context context,
+            final long currentSlot,
+            final long lowerTimestamp,
+            final long upperTimestamp,
+            final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap,
+            final Map<Long, Map<String, BatteryHistEntry>> resultMap,
+            final boolean isStartOrEnd) {
+        final Map<String, BatteryHistEntry> lowerEntryDataMap =
+                batteryHistoryMap.get(lowerTimestamp);
+        final Map<String, BatteryHistEntry> upperEntryDataMap =
+                batteryHistoryMap.get(upperTimestamp);
+        // Verifies whether the lower data is valid to use or not by checking boot time.
+        final BatteryHistEntry upperEntryDataFirstEntry =
+                upperEntryDataMap.values().stream().findFirst().get();
+        final long upperEntryDataBootTimestamp =
+                upperEntryDataFirstEntry.mTimestamp - upperEntryDataFirstEntry.mBootTimestamp;
+        // Lower data is captured before upper data corresponding device is booting.
+        // Skips the booting-specific logics and always does interpolation for daily chart level
+        // data.
+        if (lowerTimestamp < upperEntryDataBootTimestamp
+                && !isForDailyChart(isStartOrEnd, currentSlot)) {
+            // Provides an opportunity to force align the slot directly.
+            if ((upperTimestamp - currentSlot) < 10 * DateUtils.MINUTE_IN_MILLIS) {
+                log(context, "force align into the nearest slot", currentSlot, null);
+                resultMap.put(currentSlot, upperEntryDataMap);
+            } else {
+                log(context, "in the different booting section", currentSlot, null);
+                resultMap.put(currentSlot, new HashMap<>());
+            }
+            return;
+        }
+        log(context, "apply interpolation arithmetic", currentSlot, null);
+        final Map<String, BatteryHistEntry> newHistEntryMap = new HashMap<>();
+        final double timestampLength = upperTimestamp - lowerTimestamp;
+        final double timestampDiff = currentSlot - lowerTimestamp;
+        // Applies interpolation arithmetic for each BatteryHistEntry.
+        for (String entryKey : upperEntryDataMap.keySet()) {
+            final BatteryHistEntry lowerEntry = lowerEntryDataMap.get(entryKey);
+            final BatteryHistEntry upperEntry = upperEntryDataMap.get(entryKey);
+            // Checks whether there is any abnormal battery reset conditions.
+            if (lowerEntry != null) {
+                final boolean invalidForegroundUsageTime =
+                        lowerEntry.mForegroundUsageTimeInMs > upperEntry.mForegroundUsageTimeInMs;
+                final boolean invalidBackgroundUsageTime =
+                        lowerEntry.mBackgroundUsageTimeInMs > upperEntry.mBackgroundUsageTimeInMs;
+                if (invalidForegroundUsageTime || invalidBackgroundUsageTime) {
+                    newHistEntryMap.put(entryKey, upperEntry);
+                    log(context, "abnormal reset condition is found", currentSlot, upperEntry);
+                    continue;
+                }
+            }
+            final BatteryHistEntry newEntry =
+                    BatteryHistEntry.interpolate(
+                            currentSlot,
+                            upperTimestamp,
+                            /*ratio=*/ timestampDiff / timestampLength,
+                            lowerEntry,
+                            upperEntry);
+            newHistEntryMap.put(entryKey, newEntry);
+            if (lowerEntry == null) {
+                log(context, "cannot find lower entry data", currentSlot, upperEntry);
+                continue;
+            }
+        }
+        resultMap.put(currentSlot, newHistEntryMap);
+    }
+
+    /**
+     * @return Returns the nearest even hour timestamp of the given timestamp.
+     */
+    private static long getNearestEvenHourTimestamp(long rawTimestamp) {
+        // If raw hour is even, the nearest even hour should be the even hour before raw
+        // start. The hour doesn't need to change and just set the minutes and seconds to 0.
+        // Otherwise, the nearest even hour should be raw hour + 1.
+        // For example, the nearest hour of 14:30:50 should be 14:00:00. While the nearest
+        // hour of 15:30:50 should be 16:00:00.
+        return getEvenHourTimestamp(rawTimestamp, /*addHourOfDay*/ 1);
+    }
+
+    /**
+     * @return Returns the last even hour timestamp before the given timestamp.
+     */
+    private static long getLastEvenHourBeforeTimestamp(long rawTimestamp) {
+        // If raw hour is even, the hour doesn't need to change as well.
+        // Otherwise, the even hour before raw end should be raw hour - 1.
+        // For example, the even hour before 14:30:50 should be 14:00:00. While the even
+        // hour before 15:30:50 should be 14:00:00.
+        return getEvenHourTimestamp(rawTimestamp, /*addHourOfDay*/ -1);
+    }
+
+    private static long getEvenHourTimestamp(long rawTimestamp, int addHourOfDay) {
+        final Calendar evenHourCalendar = Calendar.getInstance();
+        evenHourCalendar.setTimeInMillis(rawTimestamp);
+        // Before computing the evenHourCalendar, record raw hour based on local timezone.
+        final int rawHour = evenHourCalendar.get(Calendar.HOUR_OF_DAY);
+        if (rawHour % 2 != 0) {
+            evenHourCalendar.add(Calendar.HOUR_OF_DAY, addHourOfDay);
+        }
+        evenHourCalendar.set(Calendar.MINUTE, 0);
+        evenHourCalendar.set(Calendar.SECOND, 0);
+        evenHourCalendar.set(Calendar.MILLISECOND, 0);
+        return evenHourCalendar.getTimeInMillis();
+    }
+
+    private static List<List<Long>> getHourlyTimestamps(final List<Long> dailyTimestamps) {
+        final List<List<Long>> hourlyTimestamps = new ArrayList<>();
+        if (dailyTimestamps.size() < MIN_DAILY_DATA_SIZE) {
+            return hourlyTimestamps;
+        }
+
+        for (int dailyStartIndex = 0; dailyStartIndex < dailyTimestamps.size() - 1;
+                dailyStartIndex++) {
+            long currentTimestamp = dailyTimestamps.get(dailyStartIndex);
+            final long dailyEndTimestamp = dailyTimestamps.get(dailyStartIndex + 1);
+            final List<Long> hourlyTimestampsPerDay = new ArrayList<>();
+            while (currentTimestamp <= dailyEndTimestamp) {
+                hourlyTimestampsPerDay.add(currentTimestamp);
+                currentTimestamp += 2 * DateUtils.HOUR_IN_MILLIS;
+            }
+            hourlyTimestamps.add(hourlyTimestampsPerDay);
+        }
+        return hourlyTimestamps;
+    }
+
+    private static List<BatteryLevelData.PeriodBatteryLevelData> getHourlyPeriodBatteryLevelData(
+            Context context,
+            final Map<Long, Map<String, BatteryHistEntry>> processedBatteryHistoryMap,
+            final List<List<Long>> timestamps) {
+        final List<BatteryLevelData.PeriodBatteryLevelData> levelData = new ArrayList<>();
+        timestamps.forEach(
+                timestampList -> levelData.add(
+                        getPeriodBatteryLevelData(
+                                context, processedBatteryHistoryMap, timestampList)));
+        return levelData;
+    }
+
+    private static BatteryLevelData.PeriodBatteryLevelData getPeriodBatteryLevelData(
+            Context context,
+            final Map<Long, Map<String, BatteryHistEntry>> processedBatteryHistoryMap,
+            final List<Long> timestamps) {
+        final List<Integer> levels = new ArrayList<>();
+        timestamps.forEach(
+                timestamp -> levels.add(getLevel(context, processedBatteryHistoryMap, timestamp)));
+        return new BatteryLevelData.PeriodBatteryLevelData(timestamps, levels);
+    }
+
+    private static Integer getLevel(
+            Context context,
+            final Map<Long, Map<String, BatteryHistEntry>> processedBatteryHistoryMap,
+            final long timestamp) {
+        final Map<String, BatteryHistEntry> entryMap = processedBatteryHistoryMap.get(timestamp);
+        if (entryMap == null || entryMap.isEmpty()) {
+            Log.e(TAG, "abnormal entry list in the timestamp:"
+                    + utcToLocalTime(context, timestamp));
+            return null;
+        }
+        // Averages the battery level in each time slot to avoid corner conditions.
+        float batteryLevelCounter = 0;
+        for (BatteryHistEntry entry : entryMap.values()) {
+            batteryLevelCounter += entry.mBatteryLevel;
+        }
+        return Math.round(batteryLevelCounter / entryMap.size());
+    }
+
+    private static void insertHourlyUsageDiffData(
+            Context context,
+            final List<BatteryLevelData.PeriodBatteryLevelData> hourlyBatteryLevelsPerDay,
+            final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap,
+            final Map<Integer, Map<Integer, BatteryDiffData>> resultMap) {
+        final int currentUserId = context.getUserId();
+        final UserHandle userHandle =
+                Utils.getManagedProfile(context.getSystemService(UserManager.class));
+        final int workProfileUserId =
+                userHandle != null ? userHandle.getIdentifier() : Integer.MIN_VALUE;
+        // Each time slot usage diff data =
+        //     Math.abs(timestamp[i+2] data - timestamp[i+1] data) +
+        //     Math.abs(timestamp[i+1] data - timestamp[i] data);
+        // since we want to aggregate every two hours data into a single time slot.
+        for (int dailyIndex = 0; dailyIndex < hourlyBatteryLevelsPerDay.size(); dailyIndex++) {
+            final Map<Integer, BatteryDiffData> dailyDiffMap = new HashMap<>();
+            resultMap.put(dailyIndex, dailyDiffMap);
+            if (hourlyBatteryLevelsPerDay.get(dailyIndex) == null) {
+                continue;
+            }
+            final List<Long> timestamps = hourlyBatteryLevelsPerDay.get(dailyIndex).getTimestamps();
+            for (int hourlyIndex = 0; hourlyIndex < timestamps.size() - 1; hourlyIndex++) {
+                final BatteryDiffData hourlyBatteryDiffData =
+                        insertHourlyUsageDiffDataPerSlot(
+                                context,
+                                currentUserId,
+                                workProfileUserId,
+                                hourlyIndex,
+                                timestamps,
+                                batteryHistoryMap);
+                dailyDiffMap.put(hourlyIndex, hourlyBatteryDiffData);
+            }
+        }
+    }
+
+    private static void insertDailyUsageDiffData(
+            final List<BatteryLevelData.PeriodBatteryLevelData> hourlyBatteryLevelsPerDay,
+            final Map<Integer, Map<Integer, BatteryDiffData>> resultMap) {
+        for (int index = 0; index < hourlyBatteryLevelsPerDay.size(); index++) {
+            Map<Integer, BatteryDiffData> dailyUsageMap = resultMap.get(index);
+            if (dailyUsageMap == null) {
+                dailyUsageMap = new HashMap<>();
+                resultMap.put(index, dailyUsageMap);
+            }
+            dailyUsageMap.put(
+                    SELECTED_INDEX_ALL,
+                    getAccumulatedUsageDiffData(dailyUsageMap.values()));
+        }
+    }
+
+    private static void insertAllUsageDiffData(
+            final Map<Integer, Map<Integer, BatteryDiffData>> resultMap) {
+        final List<BatteryDiffData> diffDataList = new ArrayList<>();
+        resultMap.keySet().forEach(
+                key -> diffDataList.add(resultMap.get(key).get(SELECTED_INDEX_ALL)));
+        final Map<Integer, BatteryDiffData> allUsageMap = new HashMap<>();
+        allUsageMap.put(SELECTED_INDEX_ALL, getAccumulatedUsageDiffData(diffDataList));
+        resultMap.put(SELECTED_INDEX_ALL, allUsageMap);
+    }
+
+    @Nullable
+    private static BatteryDiffData insertHourlyUsageDiffDataPerSlot(
+            Context context,
+            final int currentUserId,
+            final int workProfileUserId,
+            final int currentIndex,
+            final List<Long> timestamps,
+            final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap) {
+        final List<BatteryDiffEntry> appEntries = new ArrayList<>();
+        final List<BatteryDiffEntry> systemEntries = new ArrayList<>();
+
+        final Long currentTimestamp = timestamps.get(currentIndex);
+        final Long nextTimestamp = currentTimestamp + DateUtils.HOUR_IN_MILLIS;
+        final Long nextTwoTimestamp = nextTimestamp + DateUtils.HOUR_IN_MILLIS;
+        // Fetches BatteryHistEntry data from corresponding time slot.
+        final Map<String, BatteryHistEntry> currentBatteryHistMap =
+                batteryHistoryMap.getOrDefault(currentTimestamp, EMPTY_BATTERY_MAP);
+        final Map<String, BatteryHistEntry> nextBatteryHistMap =
+                batteryHistoryMap.getOrDefault(nextTimestamp, EMPTY_BATTERY_MAP);
+        final Map<String, BatteryHistEntry> nextTwoBatteryHistMap =
+                batteryHistoryMap.getOrDefault(nextTwoTimestamp, EMPTY_BATTERY_MAP);
+        // We should not get the empty list since we have at least one fake data to record
+        // the battery level and status in each time slot, the empty list is used to
+        // represent there is no enough data to apply interpolation arithmetic.
+        if (currentBatteryHistMap.isEmpty()
+                || nextBatteryHistMap.isEmpty()
+                || nextTwoBatteryHistMap.isEmpty()) {
+            return null;
+        }
+
+        // Collects all keys in these three time slot records as all populations.
+        final Set<String> allBatteryHistEntryKeys = new ArraySet<>();
+        allBatteryHistEntryKeys.addAll(currentBatteryHistMap.keySet());
+        allBatteryHistEntryKeys.addAll(nextBatteryHistMap.keySet());
+        allBatteryHistEntryKeys.addAll(nextTwoBatteryHistMap.keySet());
+
+        double totalConsumePower = 0.0;
+        double consumePowerFromOtherUsers = 0f;
+        // Calculates all packages diff usage data in a specific time slot.
+        for (String key : allBatteryHistEntryKeys) {
+            final BatteryHistEntry currentEntry =
+                    currentBatteryHistMap.getOrDefault(key, EMPTY_BATTERY_HIST_ENTRY);
+            final BatteryHistEntry nextEntry =
+                    nextBatteryHistMap.getOrDefault(key, EMPTY_BATTERY_HIST_ENTRY);
+            final BatteryHistEntry nextTwoEntry =
+                    nextTwoBatteryHistMap.getOrDefault(key, EMPTY_BATTERY_HIST_ENTRY);
+            // Cumulative values is a specific time slot for a specific app.
+            long foregroundUsageTimeInMs =
+                    getDiffValue(
+                            currentEntry.mForegroundUsageTimeInMs,
+                            nextEntry.mForegroundUsageTimeInMs,
+                            nextTwoEntry.mForegroundUsageTimeInMs);
+            long backgroundUsageTimeInMs =
+                    getDiffValue(
+                            currentEntry.mBackgroundUsageTimeInMs,
+                            nextEntry.mBackgroundUsageTimeInMs,
+                            nextTwoEntry.mBackgroundUsageTimeInMs);
+            double consumePower =
+                    getDiffValue(
+                            currentEntry.mConsumePower,
+                            nextEntry.mConsumePower,
+                            nextTwoEntry.mConsumePower);
+            // Excludes entry since we don't have enough data to calculate.
+            if (foregroundUsageTimeInMs == 0
+                    && backgroundUsageTimeInMs == 0
+                    && consumePower == 0) {
+                continue;
+            }
+            final BatteryHistEntry selectedBatteryEntry =
+                    selectBatteryHistEntry(currentEntry, nextEntry, nextTwoEntry);
+            if (selectedBatteryEntry == null) {
+                continue;
+            }
+            // Forces refine the cumulative value since it may introduce deviation error since we
+            // will apply the interpolation arithmetic.
+            final float totalUsageTimeInMs =
+                    foregroundUsageTimeInMs + backgroundUsageTimeInMs;
+            if (totalUsageTimeInMs > TOTAL_HOURLY_TIME_THRESHOLD) {
+                final float ratio = TOTAL_HOURLY_TIME_THRESHOLD / totalUsageTimeInMs;
+                if (DEBUG) {
+                    Log.w(TAG, String.format("abnormal usage time %d|%d for:\n%s",
+                            Duration.ofMillis(foregroundUsageTimeInMs).getSeconds(),
+                            Duration.ofMillis(backgroundUsageTimeInMs).getSeconds(),
+                            currentEntry));
+                }
+                foregroundUsageTimeInMs =
+                        Math.round(foregroundUsageTimeInMs * ratio);
+                backgroundUsageTimeInMs =
+                        Math.round(backgroundUsageTimeInMs * ratio);
+                consumePower = consumePower * ratio;
+            }
+            totalConsumePower += consumePower;
+
+            final boolean isFromOtherUsers = isConsumedFromOtherUsers(
+                    currentUserId, workProfileUserId, selectedBatteryEntry);
+            if (isFromOtherUsers) {
+                consumePowerFromOtherUsers += consumePower;
+            } else {
+                final BatteryDiffEntry currentBatteryDiffEntry = new BatteryDiffEntry(
+                        context,
+                        foregroundUsageTimeInMs,
+                        backgroundUsageTimeInMs,
+                        consumePower,
+                        selectedBatteryEntry);
+                if (currentBatteryDiffEntry.isSystemEntry()) {
+                    systemEntries.add(currentBatteryDiffEntry);
+                } else {
+                    appEntries.add(currentBatteryDiffEntry);
+                }
+            }
+        }
+        if (consumePowerFromOtherUsers != 0) {
+            systemEntries.add(createOtherUsersEntry(context, consumePowerFromOtherUsers));
+        }
+
+        // If there is no data, return null instead of empty item.
+        if (appEntries.isEmpty() && systemEntries.isEmpty()) {
+            return null;
+        }
+
+        final BatteryDiffData resultDiffData =
+                new BatteryDiffData(appEntries, systemEntries, totalConsumePower);
+        return resultDiffData;
+    }
+
+    private static boolean isConsumedFromOtherUsers(
+            final int currentUserId,
+            final int workProfileUserId,
+            final BatteryHistEntry batteryHistEntry) {
+        return batteryHistEntry.mConsumerType == ConvertUtils.CONSUMER_TYPE_UID_BATTERY
+                && batteryHistEntry.mUserId != currentUserId
+                && batteryHistEntry.mUserId != workProfileUserId;
+    }
+
+    @Nullable
+    private static BatteryDiffData getAccumulatedUsageDiffData(
+            final Collection<BatteryDiffData> diffEntryListData) {
+        double totalConsumePower = 0f;
+        final Map<String, BatteryDiffEntry> diffEntryMap = new HashMap<>();
+        final List<BatteryDiffEntry> appEntries = new ArrayList<>();
+        final List<BatteryDiffEntry> systemEntries = new ArrayList<>();
+
+        for (BatteryDiffData diffEntryList : diffEntryListData) {
+            if (diffEntryList == null) {
+                continue;
+            }
+            for (BatteryDiffEntry entry : diffEntryList.getAppDiffEntryList()) {
+                computeUsageDiffDataPerEntry(entry, diffEntryMap);
+                totalConsumePower += entry.mConsumePower;
+            }
+            for (BatteryDiffEntry entry : diffEntryList.getSystemDiffEntryList()) {
+                computeUsageDiffDataPerEntry(entry, diffEntryMap);
+                totalConsumePower += entry.mConsumePower;
+            }
+        }
+
+        final Collection<BatteryDiffEntry> diffEntryList = diffEntryMap.values();
+        for (BatteryDiffEntry entry : diffEntryList) {
+            // Sets total daily consume power data into all BatteryDiffEntry.
+            entry.setTotalConsumePower(totalConsumePower);
+            if (entry.isSystemEntry()) {
+                systemEntries.add(entry);
+            } else {
+                appEntries.add(entry);
+            }
+        }
+
+        return diffEntryList.isEmpty() ? null : new BatteryDiffData(appEntries, systemEntries);
+    }
+
+    private static void computeUsageDiffDataPerEntry(
+            final BatteryDiffEntry entry,
+            final Map<String, BatteryDiffEntry> diffEntryMap) {
+        final String key = entry.mBatteryHistEntry.getKey();
+        final BatteryDiffEntry oldBatteryDiffEntry = diffEntryMap.get(key);
+        // Creates new BatteryDiffEntry if we don't have it.
+        if (oldBatteryDiffEntry == null) {
+            diffEntryMap.put(key, entry.clone());
+        } else {
+            // Sums up some field data into the existing one.
+            oldBatteryDiffEntry.mForegroundUsageTimeInMs +=
+                    entry.mForegroundUsageTimeInMs;
+            oldBatteryDiffEntry.mBackgroundUsageTimeInMs +=
+                    entry.mBackgroundUsageTimeInMs;
+            oldBatteryDiffEntry.mConsumePower += entry.mConsumePower;
+        }
+    }
+
+    // Removes low percentage data and fake usage data, which will be zero value.
+    private static void purgeLowPercentageAndFakeData(
+            final Context context,
+            final Map<Integer, Map<Integer, BatteryDiffData>> resultMap) {
+        final Set<CharSequence> backgroundUsageTimeHideList =
+                FeatureFactory.getFactory(context)
+                        .getPowerUsageFeatureProvider(context)
+                        .getHideBackgroundUsageTimeSet(context);
+        final CharSequence[] notAllowShowEntryPackages =
+                FeatureFactory.getFactory(context)
+                        .getPowerUsageFeatureProvider(context)
+                        .getHideApplicationEntries(context);
+        resultMap.keySet().forEach(dailyKey -> {
+            final Map<Integer, BatteryDiffData> dailyUsageMap = resultMap.get(dailyKey);
+            dailyUsageMap.values().forEach(diffEntryLists -> {
+                if (diffEntryLists == null) {
+                    return;
+                }
+                purgeLowPercentageAndFakeData(
+                        diffEntryLists.getAppDiffEntryList(), backgroundUsageTimeHideList,
+                        notAllowShowEntryPackages);
+                purgeLowPercentageAndFakeData(
+                        diffEntryLists.getSystemDiffEntryList(), backgroundUsageTimeHideList,
+                        notAllowShowEntryPackages);
+            });
+        });
+    }
+
+    private static void purgeLowPercentageAndFakeData(
+            final List<BatteryDiffEntry> entries,
+            final Set<CharSequence> backgroundUsageTimeHideList,
+            final CharSequence[] notAllowShowEntryPackages) {
+        final Iterator<BatteryDiffEntry> iterator = entries.iterator();
+        while (iterator.hasNext()) {
+            final BatteryDiffEntry entry = iterator.next();
+            final String packageName = entry.getPackageName();
+            if (entry.getPercentOfTotal() < PERCENTAGE_OF_TOTAL_THRESHOLD
+                    || FAKE_PACKAGE_NAME.equals(packageName)
+                    || contains(packageName, notAllowShowEntryPackages)) {
+                iterator.remove();
+            }
+            if (packageName != null
+                    && !backgroundUsageTimeHideList.isEmpty()
+                    && contains(packageName, backgroundUsageTimeHideList)) {
+                entry.mBackgroundUsageTimeInMs = 0;
+            }
+        }
+    }
+
+    private static boolean isUsageMapValid(
+            final Map<Integer, Map<Integer, BatteryDiffData>> batteryUsageMap,
+            final List<BatteryLevelData.PeriodBatteryLevelData> hourlyBatteryLevelsPerDay) {
+        if (batteryUsageMap.get(SELECTED_INDEX_ALL) == null
+                || !batteryUsageMap.get(SELECTED_INDEX_ALL).containsKey(SELECTED_INDEX_ALL)) {
+            Log.e(TAG, "no [SELECTED_INDEX_ALL][SELECTED_INDEX_ALL] in batteryUsageMap");
+            return false;
+        }
+        for (int dailyIndex = 0; dailyIndex < hourlyBatteryLevelsPerDay.size(); dailyIndex++) {
+            if (batteryUsageMap.get(dailyIndex) == null
+                    || !batteryUsageMap.get(dailyIndex).containsKey(SELECTED_INDEX_ALL)) {
+                Log.e(TAG, "no [" + dailyIndex + "][SELECTED_INDEX_ALL] in batteryUsageMap, "
+                        + "daily size is: " + hourlyBatteryLevelsPerDay.size());
+                return false;
+            }
+            if (hourlyBatteryLevelsPerDay.get(dailyIndex) == null) {
+                continue;
+            }
+            final List<Long> timestamps = hourlyBatteryLevelsPerDay.get(dailyIndex).getTimestamps();
+            // Length of hourly usage map should be the length of hourly level data - 1.
+            for (int hourlyIndex = 0; hourlyIndex < timestamps.size() - 1; hourlyIndex++) {
+                if (!batteryUsageMap.get(dailyIndex).containsKey(hourlyIndex)) {
+                    Log.e(TAG, "no [" + dailyIndex + "][" + hourlyIndex + "] in batteryUsageMap, "
+                            + "hourly size is: " + (timestamps.size() - 1));
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+
+    private static long getTimestampWithDayDiff(final long timestamp, final int dayDiff) {
+        final Calendar calendar = Calendar.getInstance();
+        calendar.setTimeInMillis(timestamp);
+        calendar.add(Calendar.DAY_OF_YEAR, dayDiff);
+        calendar.set(Calendar.HOUR_OF_DAY, 0);
+        calendar.set(Calendar.MINUTE, 0);
+        calendar.set(Calendar.SECOND, 0);
+        return calendar.getTimeInMillis();
+    }
+
+    private static boolean contains(String target, Set<CharSequence> packageNames) {
+        if (target != null && packageNames != null) {
+            for (CharSequence packageName : packageNames) {
+                if (TextUtils.equals(target, packageName)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    private static long getDiffValue(long v1, long v2, long v3) {
+        return (v2 > v1 ? v2 - v1 : 0) + (v3 > v2 ? v3 - v2 : 0);
+    }
+
+    private static double getDiffValue(double v1, double v2, double v3) {
+        return (v2 > v1 ? v2 - v1 : 0) + (v3 > v2 ? v3 - v2 : 0);
+    }
+
+    @Nullable
+    private static BatteryHistEntry selectBatteryHistEntry(
+            final BatteryHistEntry... batteryHistEntries) {
+        for (BatteryHistEntry entry : batteryHistEntries) {
+            if (entry != null && entry != EMPTY_BATTERY_HIST_ENTRY) {
+                return entry;
+            }
+        }
+        return null;
+    }
+
+    private static BatteryDiffEntry createOtherUsersEntry(
+            Context context, final double consumePower) {
+        final ContentValues values = new ContentValues();
+        values.put(BatteryHistEntry.KEY_UID, BatteryUtils.UID_OTHER_USERS);
+        values.put(BatteryHistEntry.KEY_USER_ID, BatteryUtils.UID_OTHER_USERS);
+        values.put(BatteryHistEntry.KEY_CONSUMER_TYPE, ConvertUtils.CONSUMER_TYPE_UID_BATTERY);
+        // We will show the percentage for the "other users" item only, the aggregated
+        // running time information is useless for users to identify individual apps.
+        final BatteryDiffEntry batteryDiffEntry = new BatteryDiffEntry(
+                context,
+                /*foregroundUsageTimeInMs=*/ 0,
+                /*backgroundUsageTimeInMs=*/ 0,
+                consumePower,
+                new BatteryHistEntry(values));
+        return batteryDiffEntry;
+    }
+
+    private static void log(Context context, final String content, final long timestamp,
+            final BatteryHistEntry entry) {
+        if (DEBUG) {
+            Log.d(TAG, String.format(entry != null ? "%s %s:\n%s" : "%s %s:%s",
+                    utcToLocalTime(context, timestamp), content, entry));
+        }
+    }
+
+    // Compute diff map and loads all items (icon and label) in the background.
+    private static final class ComputeUsageMapAndLoadItemsTask
+            extends AsyncTask<Void, Void, Map<Integer, Map<Integer, BatteryDiffData>>> {
+
+        private Context mApplicationContext;
+        private Handler mHandler;
+        private UsageMapAsyncResponse mAsyncResponseDelegate;
+        private List<BatteryLevelData.PeriodBatteryLevelData> mHourlyBatteryLevelsPerDay;
+        private Map<Long, Map<String, BatteryHistEntry>> mBatteryHistoryMap;
+
+        private ComputeUsageMapAndLoadItemsTask(
+                Context context,
+                Handler handler,
+                final UsageMapAsyncResponse asyncResponseDelegate,
+                final List<BatteryLevelData.PeriodBatteryLevelData> hourlyBatteryLevelsPerDay,
+                final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap) {
+            mApplicationContext = context.getApplicationContext();
+            mHandler = handler;
+            mAsyncResponseDelegate = asyncResponseDelegate;
+            mHourlyBatteryLevelsPerDay = hourlyBatteryLevelsPerDay;
+            mBatteryHistoryMap = batteryHistoryMap;
+        }
+
+        @Override
+        protected Map<Integer, Map<Integer, BatteryDiffData>> doInBackground(Void... voids) {
+            if (mApplicationContext == null
+                    || mHandler == null
+                    || mAsyncResponseDelegate == null
+                    || mBatteryHistoryMap == null
+                    || mHourlyBatteryLevelsPerDay == null) {
+                Log.e(TAG, "invalid input for ComputeUsageMapAndLoadItemsTask()");
+                return null;
+            }
+            final long startTime = System.currentTimeMillis();
+            final Map<Integer, Map<Integer, BatteryDiffData>> batteryUsageMap =
+                    getBatteryUsageMap(
+                            mApplicationContext, mHourlyBatteryLevelsPerDay, mBatteryHistoryMap);
+            if (batteryUsageMap != null) {
+                // Pre-loads each BatteryDiffEntry relative icon and label for all slots.
+                final BatteryDiffData batteryUsageMapForAll =
+                        batteryUsageMap.get(SELECTED_INDEX_ALL).get(SELECTED_INDEX_ALL);
+                if (batteryUsageMapForAll != null) {
+                    batteryUsageMapForAll.getAppDiffEntryList().forEach(
+                            entry -> entry.loadLabelAndIcon());
+                    batteryUsageMapForAll.getSystemDiffEntryList().forEach(
+                            entry -> entry.loadLabelAndIcon());
+                }
+            }
+            Log.d(TAG, String.format("execute ComputeUsageMapAndLoadItemsTask in %d/ms",
+                    (System.currentTimeMillis() - startTime)));
+            return batteryUsageMap;
+        }
+
+        @Override
+        protected void onPostExecute(
+                final Map<Integer, Map<Integer, BatteryDiffData>> batteryUsageMap) {
+            mApplicationContext = null;
+            mHourlyBatteryLevelsPerDay = null;
+            mBatteryHistoryMap = null;
+            // Post results back to main thread to refresh UI.
+            if (mHandler != null && mAsyncResponseDelegate != null) {
+                mHandler.post(() -> {
+                    mAsyncResponseDelegate.onBatteryUsageMapLoaded(batteryUsageMap);
+                });
+            }
+        }
+    }
+}
diff --git a/src/com/android/settings/fuelgauge/batteryusage/PowerUsageSummary.java b/src/com/android/settings/fuelgauge/batteryusage/PowerUsageSummary.java
index 405d855..bca32a7 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/PowerUsageSummary.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/PowerUsageSummary.java
@@ -259,10 +259,7 @@
     @VisibleForTesting
     void initPreference() {
         mBatteryUsagePreference = findPreference(KEY_BATTERY_USAGE);
-        mBatteryUsagePreference.setSummary(
-                mPowerFeatureProvider.isChartGraphEnabled(getContext())
-                        ? getString(R.string.advanced_battery_preference_summary_with_hours)
-                        : getString(R.string.advanced_battery_preference_summary));
+        mBatteryUsagePreference.setSummary(getString(R.string.advanced_battery_preference_summary));
 
         mHelpPreference = findPreference(KEY_BATTERY_ERROR);
         mHelpPreference.setVisible(false);
diff --git a/src/com/android/settings/notification/ConfigureNotificationSettings.java b/src/com/android/settings/notification/ConfigureNotificationSettings.java
index f888ea7..1922261 100644
--- a/src/com/android/settings/notification/ConfigureNotificationSettings.java
+++ b/src/com/android/settings/notification/ConfigureNotificationSettings.java
@@ -107,7 +107,6 @@
         mNotificationAssistantPreferenceController =
                 use(NotificationAssistantPreferenceController.class);
         mNotificationAssistantPreferenceController.setFragment(this);
-        mNotificationAssistantPreferenceController.setBackend(new NotificationBackend());
     }
 
     private static List<AbstractPreferenceController> buildPreferenceControllers(Context context,
diff --git a/src/com/android/settings/notification/NotificationAssistantPreferenceController.java b/src/com/android/settings/notification/NotificationAssistantPreferenceController.java
index 91031c8..a6179e5 100644
--- a/src/com/android/settings/notification/NotificationAssistantPreferenceController.java
+++ b/src/com/android/settings/notification/NotificationAssistantPreferenceController.java
@@ -44,6 +44,7 @@
     public NotificationAssistantPreferenceController(Context context) {
         super(context, KEY_NAS);
         mUserManager = UserManager.get(context);
+        mNotificationBackend = new NotificationBackend();
     }
 
     @Override
@@ -101,4 +102,9 @@
     void setBackend(NotificationBackend backend) {
         mNotificationBackend = backend;
     }
-}
\ No newline at end of file
+
+    @Override
+    public boolean isSliceable() {
+        return (mFragment != null && mFragment instanceof ConfigureNotificationSettings);
+    }
+}
diff --git a/src/com/android/settings/vpn2/AdvancedVpnFeatureProvider.java b/src/com/android/settings/vpn2/AdvancedVpnFeatureProvider.java
index cb56c35..962b6c2 100644
--- a/src/com/android/settings/vpn2/AdvancedVpnFeatureProvider.java
+++ b/src/com/android/settings/vpn2/AdvancedVpnFeatureProvider.java
@@ -47,4 +47,9 @@
      * Returns {@code true} advanced vpn is removable.
      */
     boolean isAdvancedVpnRemovable();
+
+    /**
+     * Returns {@code true} if the disconnect dialog is enabled when advanced vpn is connected.
+     */
+    boolean isDisconnectDialogEnabled();
 }
diff --git a/src/com/android/settings/vpn2/AdvancedVpnFeatureProviderImpl.java b/src/com/android/settings/vpn2/AdvancedVpnFeatureProviderImpl.java
index c5bc69c..b8f58a9 100644
--- a/src/com/android/settings/vpn2/AdvancedVpnFeatureProviderImpl.java
+++ b/src/com/android/settings/vpn2/AdvancedVpnFeatureProviderImpl.java
@@ -46,4 +46,9 @@
     public boolean isAdvancedVpnRemovable() {
         return true;
     }
+
+    @Override
+    public boolean isDisconnectDialogEnabled() {
+        return true;
+    }
 }
diff --git a/src/com/android/settings/vpn2/VpnSettings.java b/src/com/android/settings/vpn2/VpnSettings.java
index 4380595..a91bb6c 100644
--- a/src/com/android/settings/vpn2/VpnSettings.java
+++ b/src/com/android/settings/vpn2/VpnSettings.java
@@ -26,6 +26,7 @@
 import android.app.settings.SettingsEnums;
 import android.content.Context;
 import android.content.Intent;
+import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.net.ConnectivityManager;
@@ -365,7 +366,7 @@
     public void setShownPreferences(final Collection<Preference> updates) {
         retainAllPreference(updates);
 
-        final PreferenceGroup vpnGroup = getPreferenceScreen();
+        final PreferenceGroup vpnGroup = mPreferenceScreen;
         updatePreferenceGroup(vpnGroup, updates);
 
         // Show all new preferences on the screen
@@ -447,14 +448,16 @@
         } else if (preference instanceof AppPreference) {
             AppPreference pref = (AppPreference) preference;
             boolean connected = (pref.getState() == AppPreference.STATE_CONNECTED);
+            String vpnPackageName = pref.getPackageName();
 
-            if (!connected) {
+            if ((!connected) || (isAdvancedVpn(mFeatureProvider, vpnPackageName, getContext())
+                    && !mFeatureProvider.isDisconnectDialogEnabled())) {
                 try {
                     UserHandle user = UserHandle.of(pref.getUserId());
-                    Context userContext = getActivity().createPackageContextAsUser(
-                            getActivity().getPackageName(), 0 /* flags */, user);
+                    Context userContext = getContext().createPackageContextAsUser(
+                            getContext().getPackageName(), 0 /* flags */, user);
                     PackageManager pm = userContext.getPackageManager();
-                    Intent appIntent = pm.getLaunchIntentForPackage(pref.getPackageName());
+                    Intent appIntent = pm.getLaunchIntentForPackage(vpnPackageName);
                     if (appIntent != null) {
                         userContext.startActivityAsUser(appIntent, user);
                         return true;
@@ -534,9 +537,32 @@
             pref.setOnPreferenceClickListener(this);
             mAppPreferences.put(app, pref);
         }
+        enableAdvancedVpnGearIconIfNecessary(pref);
         return pref;
     }
 
+    private void enableAdvancedVpnGearIconIfNecessary(AppPreference pref) {
+        Context context = getContext();
+        if (!isAdvancedVpn(mFeatureProvider, pref.getPackageName(), context)) {
+            return;
+        }
+
+        boolean isEnabled = false;
+        AppOpsManager appOpsManager = getContext().getSystemService(AppOpsManager.class);
+        List<AppOpsManager.PackageOps> apps =
+                appOpsManager.getPackagesForOps(
+                        new int[] {OP_ACTIVATE_VPN, OP_ACTIVATE_PLATFORM_VPN});
+        if (apps != null) {
+            for (AppOpsManager.PackageOps pkg : apps) {
+                if (isAdvancedVpn(mFeatureProvider, pkg.getPackageName(), context)) {
+                    isEnabled = true;
+                    break;
+                }
+            }
+        }
+        pref.setOnGearClickListener(isEnabled ? mGearListener : null);
+    }
+
     @WorkerThread
     private Map<String, LegacyVpnInfo> getConnectedLegacyVpns() {
         mConnectedLegacyVpn = mVpnManager.getLegacyVpnInfo(UserHandle.myUserId());
@@ -593,6 +619,19 @@
             profileIds = Collections.singleton(UserHandle.myUserId());
         }
 
+        if (featureProvider.isAdvancedVpnSupported(context)) {
+            PackageManager pm = context.getPackageManager();
+            try {
+                ApplicationInfo appInfo =
+                        pm.getApplicationInfo(
+                                featureProvider.getAdvancedVpnPackageName(), /* flags= */ 0);
+                int userId = UserHandle.getUserId(appInfo.uid);
+                result.add(new AppVpnInfo(userId, featureProvider.getAdvancedVpnPackageName()));
+            } catch (PackageManager.NameNotFoundException e) {
+                Log.e(LOG_TAG, "Advanced VPN package name not found.", e);
+            }
+        }
+
         List<AppOpsManager.PackageOps> apps =
                 aom.getPackagesForOps(new int[] {OP_ACTIVATE_VPN, OP_ACTIVATE_PLATFORM_VPN});
         if (apps != null) {
@@ -602,6 +641,9 @@
                     // Skip packages for users outside of our profile group.
                     continue;
                 }
+                if (isAdvancedVpn(featureProvider, pkg.getPackageName(), context)) {
+                    continue;
+                }
                 // Look for a MODE_ALLOWED permission to activate VPN.
                 boolean allowed = false;
                 for (AppOpsManager.OpEntry op : pkg.getOps()) {
@@ -610,7 +652,7 @@
                         allowed = true;
                     }
                 }
-                if (allowed || isAdvancedVpn(featureProvider, pkg.getPackageName(), context)) {
+                if (allowed) {
                     result.add(new AppVpnInfo(userId, pkg.getPackageName()));
                 }
             }
diff --git a/src/com/android/settings/widget/CardPreference.java b/src/com/android/settings/widget/CardPreference.java
index c041552..afccfac 100644
--- a/src/com/android/settings/widget/CardPreference.java
+++ b/src/com/android/settings/widget/CardPreference.java
@@ -18,18 +18,36 @@
 
 import android.content.Context;
 import android.util.AttributeSet;
+import android.view.View;
+import android.widget.Button;
 
 import androidx.preference.Preference;
+import androidx.preference.PreferenceViewHolder;
 
 import com.android.settings.R;
 
 import com.google.android.material.card.MaterialCardView;
 
+import java.util.Optional;
+
 /**
  * Preference that wrapped by {@link MaterialCardView}, only support to set icon, title and summary
  */
 public class CardPreference extends Preference {
 
+    private View.OnClickListener mPrimaryBtnClickListener = null;
+    private View.OnClickListener mSecondaryBtnClickListener = null;
+
+    private String mPrimaryButtonText = null;
+    private String mSecondaryButtonText = null;
+
+    private Optional<Button> mPrimaryButton = Optional.empty();
+    private Optional<Button> mSecondaryButton = Optional.empty();
+    private Optional<View> mButtonsGroup = Optional.empty();
+
+    private boolean mPrimaryButtonVisible = false;
+    private boolean mSecondaryButtonVisible = false;
+
     public CardPreference(Context context) {
         this(context, null /* attrs */);
     }
@@ -37,4 +55,94 @@
     public CardPreference(Context context, AttributeSet attrs) {
         super(context, attrs, R.attr.cardPreferenceStyle);
     }
+
+    @Override
+    public void onBindViewHolder(PreferenceViewHolder holder) {
+        super.onBindViewHolder(holder);
+
+        initButtonsAndLayout(holder);
+    }
+
+    private void initButtonsAndLayout(PreferenceViewHolder holder) {
+        mPrimaryButton = Optional.ofNullable((Button) holder.findViewById(android.R.id.button1));
+        mSecondaryButton = Optional.ofNullable((Button) holder.findViewById(android.R.id.button2));
+        mButtonsGroup = Optional.ofNullable(holder.findViewById(R.id.card_preference_buttons));
+
+        setPrimaryButtonText(mPrimaryButtonText);
+        setPrimaryButtonClickListener(mPrimaryBtnClickListener);
+        setPrimaryButtonVisible(mPrimaryButtonVisible);
+        setSecondaryButtonText(mSecondaryButtonText);
+        setSecondaryButtonClickListener(mSecondaryBtnClickListener);
+        setSecondaryButtonVisible(mSecondaryButtonVisible);
+    }
+
+    /**
+     * Register a callback to be invoked when the primary button is clicked.
+     *
+     * @param l the callback that will run
+     */
+    public void setPrimaryButtonClickListener(View.OnClickListener l) {
+        mPrimaryButton.ifPresent(button -> button.setOnClickListener(l));
+        mPrimaryBtnClickListener = l;
+    }
+
+    /**
+     * Register a callback to be invoked when the secondary button is clicked.
+     *
+     * @param l the callback that will run
+     */
+    public void setSecondaryButtonClickListener(View.OnClickListener l) {
+        mSecondaryButton.ifPresent(button -> button.setOnClickListener(l));
+        mSecondaryBtnClickListener = l;
+    }
+
+    /**
+     * Sets the text to be displayed on primary button.
+     *
+     * @param text text to be displayed
+     */
+    public void setPrimaryButtonText(String text) {
+        mPrimaryButton.ifPresent(button -> button.setText(text));
+        mPrimaryButtonText = text;
+    }
+
+    /**
+     * Sets the text to be displayed on secondary button.
+     *
+     * @param text text to be displayed
+     */
+    public void setSecondaryButtonText(String text) {
+        mSecondaryButton.ifPresent(button -> button.setText(text));
+        mSecondaryButtonText = text;
+    }
+
+    /**
+     * Set the visible on the primary button.
+     *
+     * @param visible {@code true} for visible
+     */
+    public void setPrimaryButtonVisible(boolean visible) {
+        mPrimaryButton.ifPresent(
+                button -> button.setVisibility(visible ? View.VISIBLE : View.GONE));
+        mPrimaryButtonVisible = visible;
+        updateButtonGroupsVisibility();
+    }
+
+    /**
+     * Set the visible on the secondary button.
+     *
+     * @param visible {@code true} for visible
+     */
+    public void setSecondaryButtonVisible(boolean visible) {
+        mSecondaryButton.ifPresent(
+                button -> button.setVisibility(visible ? View.VISIBLE : View.GONE));
+        mSecondaryButtonVisible = visible;
+        updateButtonGroupsVisibility();
+    }
+
+    private void updateButtonGroupsVisibility() {
+        int visibility =
+                (mPrimaryButtonVisible || mSecondaryButtonVisible) ? View.VISIBLE : View.GONE;
+        mButtonsGroup.ifPresent(group -> group.setVisibility(visibility));
+    }
 }
diff --git a/src/com/android/settings/wifi/details2/WifiDetailPreferenceController2.java b/src/com/android/settings/wifi/details2/WifiDetailPreferenceController2.java
index ba7628e..f7fc07a 100644
--- a/src/com/android/settings/wifi/details2/WifiDetailPreferenceController2.java
+++ b/src/com/android/settings/wifi/details2/WifiDetailPreferenceController2.java
@@ -19,6 +19,7 @@
 import static android.net.NetworkCapabilities.NET_CAPABILITY_PARTIAL_CONNECTIVITY;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED;
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
+import static android.telephony.TelephonyManager.UNKNOWN_CARRIER_ID;
 
 import android.app.Activity;
 import android.app.AlertDialog;
@@ -49,7 +50,6 @@
 import android.provider.Telephony.CarrierId;
 import android.telephony.SubscriptionInfo;
 import android.telephony.SubscriptionManager;
-import android.telephony.TelephonyManager;
 import android.text.TextUtils;
 import android.util.FeatureFlagUtils;
 import android.util.Log;
@@ -711,27 +711,17 @@
         // Checks if the SIM subscription is active.
         final List<SubscriptionInfo> activeSubscriptionInfos = mContext
                 .getSystemService(SubscriptionManager.class).getActiveSubscriptionInfoList();
-        final int defaultDataSubscriptionId = SubscriptionManager.getDefaultDataSubscriptionId();
         if (activeSubscriptionInfos != null) {
-            for (SubscriptionInfo subscriptionInfo : activeSubscriptionInfos) {
-                final CharSequence displayName = SubscriptionUtil.getUniqueSubscriptionDisplayName(
-                        subscriptionInfo, mContext);
-                if (config.carrierId == subscriptionInfo.getCarrierId()) {
-                    mEapSimSubscriptionPref.setSummary(displayName);
-                    return;
-                }
-
-                // When it's UNKNOWN_CARRIER_ID, devices connects it with the SIM subscription of
-                // defaultDataSubscriptionId.
-                if (config.carrierId == TelephonyManager.UNKNOWN_CARRIER_ID
-                        && defaultDataSubscriptionId == subscriptionInfo.getSubscriptionId()) {
-                    mEapSimSubscriptionPref.setSummary(displayName);
-                    return;
-                }
+            SubscriptionInfo info = fineSubscriptionInfo(config.carrierId, activeSubscriptionInfos,
+                    SubscriptionManager.getDefaultDataSubscriptionId());
+            if (info != null) {
+                mEapSimSubscriptionPref.setSummary(
+                        SubscriptionUtil.getUniqueSubscriptionDisplayName(info, mContext));
+                return;
             }
         }
 
-        if (config.carrierId == TelephonyManager.UNKNOWN_CARRIER_ID) {
+        if (config.carrierId == UNKNOWN_CARRIER_ID) {
             mEapSimSubscriptionPref.setSummary(R.string.wifi_no_related_sim_card);
             return;
         }
@@ -750,6 +740,25 @@
                 null /* orderBy */);
     }
 
+    @VisibleForTesting
+    SubscriptionInfo fineSubscriptionInfo(int carrierId,
+            List<SubscriptionInfo> activeSubscriptionInfos, int defaultDataSubscriptionId) {
+        SubscriptionInfo firstMatchedInfo = null;
+        for (SubscriptionInfo info : activeSubscriptionInfos) {
+            // When it's UNKNOWN_CARRIER_ID or matched with configured CarrierId,
+            // devices connects it with the SIM subscription of defaultDataSubscriptionId.
+            if (defaultDataSubscriptionId == info.getSubscriptionId()
+                    && (carrierId == info.getCarrierId() || carrierId == UNKNOWN_CARRIER_ID)) {
+                return info;
+            }
+
+            if (firstMatchedInfo == null && carrierId == info.getCarrierId()) {
+                firstMatchedInfo = info;
+            }
+        }
+        return firstMatchedInfo;
+    }
+
     private void refreshMacAddress() {
         final String macAddress = mWifiEntry.getMacAddress();
         if (TextUtils.isEmpty(macAddress)) {
diff --git a/tests/robotests/src/com/android/settings/applications/appinfo/AppBatteryPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/applications/appinfo/AppBatteryPreferenceControllerTest.java
index a75663b..c95a509 100644
--- a/tests/robotests/src/com/android/settings/applications/appinfo/AppBatteryPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/applications/appinfo/AppBatteryPreferenceControllerTest.java
@@ -162,7 +162,8 @@
 
         mController.updateBatteryWithDiffEntry();
 
-        assertThat(mBatteryPreference.getSummary()).isEqualTo("No battery use for past 24 hours");
+        assertThat(mBatteryPreference.getSummary().toString()).isEqualTo(
+                "No battery use since last full charge");
     }
 
     @Test
@@ -175,7 +176,8 @@
 
         mController.updateBatteryWithDiffEntry();
 
-        assertThat(mBatteryPreference.getSummary()).isEqualTo("60% use for past 24 hours");
+        assertThat(mBatteryPreference.getSummary().toString()).isEqualTo(
+                "60% use since last full charge");
     }
 
     @Test
diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsPairOtherControllerTest.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsPairOtherControllerTest.java
new file mode 100644
index 0000000..cfa6d41
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsPairOtherControllerTest.java
@@ -0,0 +1,121 @@
+/*
+ * 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.bluetooth;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+
+import com.android.settings.R;
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.bluetooth.HearingAidProfile;
+import com.android.settingslib.widget.ButtonPreference;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.robolectric.RobolectricTestRunner;
+
+/** Tests for {@link BluetoothDetailsPairOtherController}. */
+@RunWith(RobolectricTestRunner.class)
+public class BluetoothDetailsPairOtherControllerTest extends BluetoothDetailsControllerTestBase  {
+    @Rule
+    public final MockitoRule mockito = MockitoJUnit.rule();
+
+    @Mock
+    private CachedBluetoothDevice mSubCachedDevice;
+    private BluetoothDetailsPairOtherController mController;
+    private ButtonPreference mPreference;
+
+    @Override
+    public void setUp() {
+        super.setUp();
+
+        mController = new BluetoothDetailsPairOtherController(mContext, mFragment, mCachedDevice,
+                mLifecycle);
+        mPreference = new ButtonPreference(mContext);
+        mPreference.setKey(mController.getPreferenceKey());
+        mScreen.addPreference(mPreference);
+    }
+
+    @Test
+    public void init_leftSideDevice_expectedTitle() {
+        when(mCachedDevice.getDeviceSide()).thenReturn(HearingAidProfile.DeviceSide.SIDE_LEFT);
+
+        mController.init(mScreen);
+
+        assertThat(mPreference.getTitle().toString()).isEqualTo(
+                mContext.getString(R.string.bluetooth_pair_right_ear_button));
+    }
+
+    @Test
+    public void init_rightSideDevice_expectedTitle() {
+        when(mCachedDevice.getDeviceSide()).thenReturn(HearingAidProfile.DeviceSide.SIDE_RIGHT);
+
+        mController.init(mScreen);
+
+        assertThat(mPreference.getTitle().toString()).isEqualTo(
+                mContext.getString(R.string.bluetooth_pair_left_ear_button));
+    }
+
+    @Test
+    public void isAvailable_isConnectedHearingAidDevice_available() {
+        when(mCachedDevice.isConnectedHearingAidDevice()).thenReturn(false);
+
+        assertThat(mController.isAvailable()).isFalse();
+    }
+
+    @Test
+    public void isAvailable_notConnectedHearingAidDevice_notAvailable() {
+        when(mCachedDevice.isConnectedHearingAidDevice()).thenReturn(true);
+        when(mCachedDevice.getDeviceMode()).thenReturn(HearingAidProfile.DeviceMode.MODE_MONAURAL);
+
+        assertThat(mController.isAvailable()).isFalse();
+    }
+
+    @Test
+    public void isAvailable_subDeviceIsConnectedHearingAidDevice_notAvailable() {
+        when(mCachedDevice.isConnectedHearingAidDevice()).thenReturn(true);
+        when(mCachedDevice.getDeviceMode()).thenReturn(HearingAidProfile.DeviceMode.MODE_BINAURAL);
+        when(mSubCachedDevice.isConnectedHearingAidDevice()).thenReturn(true);
+        when(mCachedDevice.getSubDevice()).thenReturn(mSubCachedDevice);
+
+        assertThat(mController.isAvailable()).isFalse();
+    }
+
+    @Test
+    public void isAvailable_subDeviceNotConnectedHearingAidDevice_available() {
+        when(mCachedDevice.isConnectedHearingAidDevice()).thenReturn(true);
+        when(mCachedDevice.getDeviceMode()).thenReturn(HearingAidProfile.DeviceMode.MODE_BINAURAL);
+        when(mSubCachedDevice.isConnectedHearingAidDevice()).thenReturn(false);
+        when(mCachedDevice.getSubDevice()).thenReturn(mSubCachedDevice);
+
+        assertThat(mController.isAvailable()).isTrue();
+    }
+
+    @Test
+    public void isAvailable_subDeviceNotExist_available() {
+        when(mCachedDevice.isConnectedHearingAidDevice()).thenReturn(true);
+        when(mCachedDevice.getDeviceMode()).thenReturn(HearingAidProfile.DeviceMode.MODE_BINAURAL);
+        when(mCachedDevice.getSubDevice()).thenReturn(null);
+
+        assertThat(mController.isAvailable()).isTrue();
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetailTest.java b/tests/robotests/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetailTest.java
index b06932a..7c56ab3 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetailTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetailTest.java
@@ -232,7 +232,7 @@
     }
 
     @Test
-    public void testGetPreferenceScreenResId_returnNewLayout() {
+    public void setPreferenceScreenResId_returnNewLayout() {
         assertThat(mFragment.getPreferenceScreenResId()).isEqualTo(R.xml.power_usage_detail);
     }
 
@@ -252,7 +252,7 @@
     }
 
     @Test
-    public void testInitHeader_HasAppEntry_BuildByAppEntry() {
+    public void initHeader_HasAppEntry_BuildByAppEntry() {
         ReflectionHelpers.setStaticField(AppUtils.class, "sInstantAppDataProvider",
                 new InstantAppDataProvider() {
                     @Override
@@ -269,7 +269,7 @@
     }
 
     @Test
-    public void testInitHeader_HasAppEntry_InstantApp() {
+    public void initHeader_HasAppEntry_InstantApp() {
         ReflectionHelpers.setStaticField(AppUtils.class, "sInstantAppDataProvider",
                 new InstantAppDataProvider() {
                     @Override
@@ -286,7 +286,7 @@
     }
 
     @Test
-    public void testInitHeader_noUsageTimeAndGraphDisabled_hasCorrectSummary() {
+    public void initHeader_noUsageTimeAndGraphDisabled_hasCorrectSummary() {
         when(mFeatureFactory.powerUsageFeatureProvider.isChartGraphEnabled(mContext))
                 .thenReturn(false);
 
@@ -304,7 +304,7 @@
     }
 
     @Test
-    public void testInitHeader_bgTwoMinFgZeroAndGraphDisabled_hasCorrectSummary() {
+    public void initHeader_bgTwoMinFgZeroAndGraphDisabled_hasCorrectSummary() {
         when(mFeatureFactory.powerUsageFeatureProvider.isChartGraphEnabled(mContext))
                 .thenReturn(false);
 
@@ -324,7 +324,7 @@
     }
 
     @Test
-    public void testInitHeader_bgLessThanAMinFgZeroAndGraphDisabled_hasCorrectSummary() {
+    public void initHeader_bgLessThanAMinFgZeroAndGraphDisabled_hasCorrectSummary() {
         when(mFeatureFactory.powerUsageFeatureProvider.isChartGraphEnabled(mContext))
                 .thenReturn(false);
 
@@ -345,7 +345,7 @@
     }
 
     @Test
-    public void testInitHeader_totalUsageLessThanAMinAndGraphDisabled_hasCorrectSummary() {
+    public void initHeader_totalUsageLessThanAMinAndGraphDisabled_hasCorrectSummary() {
         when(mFeatureFactory.powerUsageFeatureProvider.isChartGraphEnabled(mContext))
                 .thenReturn(false);
 
@@ -367,7 +367,7 @@
     }
 
     @Test
-    public void testInitHeader_TotalAMinutesBgLessThanAMinAndGraphDisabled_hasCorrectSummary() {
+    public void initHeader_TotalAMinutesBgLessThanAMinAndGraphDisabled_hasCorrectSummary() {
         when(mFeatureFactory.powerUsageFeatureProvider.isChartGraphEnabled(mContext))
                 .thenReturn(false);
 
@@ -387,7 +387,7 @@
     }
 
     @Test
-    public void testInitHeader_TotalAMinBackgroundZeroAndGraphDisabled_hasCorrectSummary() {
+    public void initHeader_TotalAMinBackgroundZeroAndGraphDisabled_hasCorrectSummary() {
         when(mFeatureFactory.powerUsageFeatureProvider.isChartGraphEnabled(mContext))
                 .thenReturn(false);
         final long backgroundTimeZero = 0;
@@ -406,7 +406,7 @@
     }
 
     @Test
-    public void testInitHeader_fgTwoMinBgFourMinAndGraphDisabled_hasCorrectSummary() {
+    public void initHeader_fgTwoMinBgFourMinAndGraphDisabled_hasCorrectSummary() {
         when(mFeatureFactory.powerUsageFeatureProvider.isChartGraphEnabled(mContext))
                 .thenReturn(false);
         final long backgroundTimeFourMinute = 240000;
@@ -424,7 +424,7 @@
     }
 
     @Test
-    public void testInitHeader_noUsageTime_hasCorrectSummary() {
+    public void initHeader_noUsageTime_hasCorrectSummary() {
         Bundle bundle = new Bundle(2);
         bundle.putLong(AdvancedPowerUsageDetail.EXTRA_BACKGROUND_TIME, /* value */ 0);
         bundle.putLong(AdvancedPowerUsageDetail.EXTRA_FOREGROUND_TIME, /* value */ 0);
@@ -435,11 +435,11 @@
         ArgumentCaptor<CharSequence> captor = ArgumentCaptor.forClass(CharSequence.class);
         verify(mEntityHeaderController).setSummary(captor.capture());
         assertThat(captor.getValue().toString())
-                .isEqualTo("No usage for past 24 hr");
+                .isEqualTo("No usage from last full charge");
     }
 
     @Test
-    public void testInitHeader_noUsageTimeButConsumedPower_hasEmptySummary() {
+    public void initHeader_noUsageTimeButConsumedPower_hasEmptySummary() {
         Bundle bundle = new Bundle(3);
         bundle.putLong(AdvancedPowerUsageDetail.EXTRA_BACKGROUND_TIME, /* value */ 0);
         bundle.putLong(AdvancedPowerUsageDetail.EXTRA_FOREGROUND_TIME, /* value */ 0);
@@ -454,7 +454,7 @@
     }
 
     @Test
-    public void testInitHeader_backgroundTwoMinForegroundZero_hasCorrectSummary() {
+    public void initHeader_backgroundTwoMinForegroundZero_hasCorrectSummary() {
         final long backgroundTimeTwoMinutes = 120000;
         final long foregroundTimeZero = 0;
         Bundle bundle = new Bundle(2);
@@ -467,11 +467,11 @@
         ArgumentCaptor<CharSequence> captor = ArgumentCaptor.forClass(CharSequence.class);
         verify(mEntityHeaderController).setSummary(captor.capture());
         assertThat(captor.getValue().toString())
-                .isEqualTo("2 min background for past 24 hr");
+                .isEqualTo("2 min background from last full charge");
     }
 
     @Test
-    public void testInitHeader_backgroundLessThanAMinForegroundZero_hasCorrectSummary() {
+    public void initHeader_backgroundLessThanAMinForegroundZero_hasCorrectSummary() {
         final long backgroundTimeLessThanAMinute = 59999;
         final long foregroundTimeZero = 0;
         Bundle bundle = new Bundle(2);
@@ -485,11 +485,11 @@
         ArgumentCaptor<CharSequence> captor = ArgumentCaptor.forClass(CharSequence.class);
         verify(mEntityHeaderController).setSummary(captor.capture());
         assertThat(captor.getValue().toString())
-                .isEqualTo("Background less than a minute for past 24 hr");
+                .isEqualTo("Background less than a minute from last full charge");
     }
 
     @Test
-    public void testInitHeader_totalUsageLessThanAMin_hasCorrectSummary() {
+    public void initHeader_totalUsageLessThanAMin_hasCorrectSummary() {
         final long backgroundTimeLessThanHalfMinute = 20000;
         final long foregroundTimeLessThanHalfMinute = 20000;
         Bundle bundle = new Bundle(2);
@@ -504,11 +504,11 @@
         ArgumentCaptor<CharSequence> captor = ArgumentCaptor.forClass(CharSequence.class);
         verify(mEntityHeaderController).setSummary(captor.capture());
         assertThat(captor.getValue().toString())
-                .isEqualTo("Total less than a minute for past 24 hr");
+                .isEqualTo("Total less than a minute from last full charge");
     }
 
     @Test
-    public void testInitHeader_TotalAMinutesBackgroundLessThanAMin_hasCorrectSummary() {
+    public void initHeader_TotalAMinutesBackgroundLessThanAMin_hasCorrectSummary() {
         final long backgroundTimeZero = 59999;
         final long foregroundTimeTwoMinutes = 1;
         Bundle bundle = new Bundle(2);
@@ -521,11 +521,11 @@
         ArgumentCaptor<CharSequence> captor = ArgumentCaptor.forClass(CharSequence.class);
         verify(mEntityHeaderController).setSummary(captor.capture());
         assertThat(captor.getValue().toString())
-                .isEqualTo("1 min total • background less than a minute\nfor past 24 hr");
+                .isEqualTo("1 min total • background less than a minute\nfrom last full charge");
     }
 
     @Test
-    public void testInitHeader_TotalAMinBackgroundZero_hasCorrectSummary() {
+    public void initHeader_TotalAMinBackgroundZero_hasCorrectSummary() {
         final long backgroundTimeZero = 0;
         final long foregroundTimeAMinutes = 60000;
         Bundle bundle = new Bundle(2);
@@ -538,11 +538,11 @@
         ArgumentCaptor<CharSequence> captor = ArgumentCaptor.forClass(CharSequence.class);
         verify(mEntityHeaderController).setSummary(captor.capture());
         assertThat(captor.getValue().toString())
-                .isEqualTo("1 min total for past 24 hr");
+                .isEqualTo("1 min total from last full charge");
     }
 
     @Test
-    public void testInitHeader_foregroundTwoMinBackgroundFourMin_hasCorrectSummary() {
+    public void initHeader_foregroundTwoMinBackgroundFourMin_hasCorrectSummary() {
         final long backgroundTimeFourMinute = 240000;
         final long foregroundTimeTwoMinutes = 120000;
         Bundle bundle = new Bundle(2);
@@ -555,11 +555,11 @@
         ArgumentCaptor<CharSequence> captor = ArgumentCaptor.forClass(CharSequence.class);
         verify(mEntityHeaderController).setSummary(captor.capture());
         assertThat(captor.getValue().toString())
-                .isEqualTo("6 min total • 4 min background\nfor past 24 hr");
+                .isEqualTo("6 min total • 4 min background\nfrom last full charge");
     }
 
     @Test
-    public void testInitHeader_totalUsageLessThanAMinWithSlotTime_hasCorrectSummary() {
+    public void initHeader_totalUsageLessThanAMinWithSlotTime_hasCorrectSummary() {
         final long backgroundTimeLessThanHalfMinute = 20000;
         final long foregroundTimeLessThanHalfMinute = 20000;
         Bundle bundle = new Bundle(3);
@@ -579,7 +579,7 @@
     }
 
     @Test
-    public void testInitHeader_TotalAMinBackgroundLessThanAMinWithSlotTime_hasCorrectSummary() {
+    public void initHeader_TotalAMinBackgroundLessThanAMinWithSlotTime_hasCorrectSummary() {
         final long backgroundTimeZero = 59999;
         final long foregroundTimeTwoMinutes = 1;
         Bundle bundle = new Bundle(3);
@@ -597,7 +597,7 @@
     }
 
     @Test
-    public void testInitHeader_TotalAMinBackgroundZeroWithSlotTime_hasCorrectSummary() {
+    public void initHeader_TotalAMinBackgroundZeroWithSlotTime_hasCorrectSummary() {
         final long backgroundTimeZero = 0;
         final long foregroundTimeAMinutes = 60000;
         Bundle bundle = new Bundle(3);
@@ -615,7 +615,7 @@
     }
 
     @Test
-    public void testInitHeader_foregroundTwoMinBackgroundFourMinWithSlotTime_hasCorrectSummary() {
+    public void initHeader_foregroundTwoMinBackgroundFourMinWithSlotTime_hasCorrectSummary() {
         final long backgroundTimeFourMinute = 240000;
         final long foregroundTimeTwoMinutes = 120000;
         Bundle bundle = new Bundle(3);
@@ -633,7 +633,7 @@
     }
 
     @Test
-    public void testInitHeader_systemUidWithChartIsDisabled_nullSummary() {
+    public void initHeader_systemUidWithChartIsDisabled_nullSummary() {
         Bundle bundle = new Bundle(3);
         bundle.putLong(AdvancedPowerUsageDetail.EXTRA_BACKGROUND_TIME, 240000);
         bundle.putLong(AdvancedPowerUsageDetail.EXTRA_FOREGROUND_TIME, 120000);
@@ -650,7 +650,7 @@
     }
 
     @Test
-    public void testInitHeader_systemUidWithChartIsEnabled_notNullSummary() {
+    public void initHeader_systemUidWithChartIsEnabled_notNullSummary() {
         Bundle bundle = new Bundle(3);
         bundle.putLong(AdvancedPowerUsageDetail.EXTRA_BACKGROUND_TIME, 240000);
         bundle.putLong(AdvancedPowerUsageDetail.EXTRA_FOREGROUND_TIME, 120000);
@@ -665,21 +665,21 @@
     }
 
     @Test
-    public void testStartBatteryDetailPage_hasBasicData() {
+    public void startBatteryDetailPage_hasBasicData() {
         AdvancedPowerUsageDetail.startBatteryDetailPage(mActivity, mFragment,
                 mBatteryEntry, USAGE_PERCENT, /*isValidToShowSummary=*/ true);
 
         assertThat(mBundle.getInt(AdvancedPowerUsageDetail.EXTRA_UID)).isEqualTo(UID);
         assertThat(mBundle.getLong(AdvancedPowerUsageDetail.EXTRA_BACKGROUND_TIME))
-            .isEqualTo(BACKGROUND_TIME_MS);
+                .isEqualTo(BACKGROUND_TIME_MS);
         assertThat(mBundle.getLong(AdvancedPowerUsageDetail.EXTRA_FOREGROUND_TIME))
-            .isEqualTo(FOREGROUND_TIME_MS);
+                .isEqualTo(FOREGROUND_TIME_MS);
         assertThat(mBundle.getString(AdvancedPowerUsageDetail.EXTRA_POWER_USAGE_PERCENT))
-            .isEqualTo(USAGE_PERCENT);
+                .isEqualTo(USAGE_PERCENT);
     }
 
     @Test
-    public void testStartBatteryDetailPage_invalidToShowSummary_noFGBDData() {
+    public void startBatteryDetailPage_invalidToShowSummary_noFGBDData() {
         AdvancedPowerUsageDetail.startBatteryDetailPage(mActivity, mFragment,
                 mBatteryEntry, USAGE_PERCENT, /*isValidToShowSummary=*/ false);
 
@@ -693,7 +693,7 @@
     }
 
     @Test
-    public void testStartBatteryDetailPage_NormalApp() {
+    public void startBatteryDetailPage_NormalApp() {
         when(mBatteryEntry.getDefaultPackageName()).thenReturn(PACKAGE_NAME[0]);
 
         AdvancedPowerUsageDetail.startBatteryDetailPage(mActivity, mFragment,
@@ -704,7 +704,7 @@
     }
 
     @Test
-    public void testStartBatteryDetailPage_SystemApp() {
+    public void startBatteryDetailPage_SystemApp() {
         when(mBatteryEntry.getDefaultPackageName()).thenReturn(null);
 
         AdvancedPowerUsageDetail.startBatteryDetailPage(mActivity, mFragment,
@@ -716,7 +716,7 @@
     }
 
     @Test
-    public void testStartBatteryDetailPage_WorkApp() {
+    public void startBatteryDetailPage_WorkApp() {
         final int appUid = 1010019;
         doReturn(appUid).when(mBatteryEntry).getUid();
 
@@ -727,7 +727,7 @@
     }
 
     @Test
-    public void testStartBatteryDetailPage_typeUser_startByCurrentUser() {
+    public void startBatteryDetailPage_typeUser_startByCurrentUser() {
         when(mBatteryEntry.isUserEntry()).thenReturn(true);
 
         final int currentUser = 20;
@@ -739,7 +739,7 @@
     }
 
     @Test
-    public void testStartBatteryDetailPage_noBatteryUsage_hasBasicData() {
+    public void startBatteryDetailPage_noBatteryUsage_hasBasicData() {
         final ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class);
 
         AdvancedPowerUsageDetail.startBatteryDetailPage(mActivity, mFragment, PACKAGE_NAME[0]);
@@ -747,16 +747,16 @@
         verify(mActivity).startActivity(captor.capture());
 
         assertThat(captor.getValue().getBundleExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS)
-            .getString(AdvancedPowerUsageDetail.EXTRA_PACKAGE_NAME))
-            .isEqualTo(PACKAGE_NAME[0]);
+                .getString(AdvancedPowerUsageDetail.EXTRA_PACKAGE_NAME))
+                .isEqualTo(PACKAGE_NAME[0]);
 
         assertThat(captor.getValue().getBundleExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS)
-            .getString(AdvancedPowerUsageDetail.EXTRA_POWER_USAGE_PERCENT))
-            .isEqualTo("0%");
+                .getString(AdvancedPowerUsageDetail.EXTRA_POWER_USAGE_PERCENT))
+                .isEqualTo("0%");
     }
 
     @Test
-    public void testStartBatteryDetailPage_batteryEntryNotExisted_extractUidFromPackageName() throws
+    public void startBatteryDetailPage_batteryEntryNotExisted_extractUidFromPackageName() throws
             PackageManager.NameNotFoundException {
         doReturn(UID).when(mPackageManager).getPackageUid(PACKAGE_NAME[0], 0 /* no flag */);
 
@@ -796,7 +796,7 @@
     }
 
     @Test
-    public void testInitPreferenceForTriState_isSystemOrDefaultApp_hasCorrectString() {
+    public void initPreferenceForTriState_isSystemOrDefaultApp_hasCorrectString() {
         when(mBatteryOptimizeUtils.isValidPackageName()).thenReturn(true);
         when(mBatteryOptimizeUtils.isSystemOrDefaultApp()).thenReturn(true);
 
@@ -807,7 +807,7 @@
     }
 
     @Test
-    public void testInitPreferenceForTriState_hasCorrectString() {
+    public void initPreferenceForTriState_hasCorrectString() {
         when(mBatteryOptimizeUtils.isValidPackageName()).thenReturn(true);
         when(mBatteryOptimizeUtils.isSystemOrDefaultApp()).thenReturn(false);
 
@@ -818,7 +818,7 @@
     }
 
     @Test
-    public void testOnRadioButtonClicked_clickOptimizePref_optimizePreferenceChecked() {
+    public void onRadioButtonClicked_clickOptimizePref_optimizePreferenceChecked() {
         mOptimizePreference.setKey(KEY_PREF_OPTIMIZED);
         mRestrictedPreference.setKey(KEY_PREF_RESTRICTED);
         mUnrestrictedPreference.setKey(KEY_PREF_UNRESTRICTED);
@@ -830,7 +830,7 @@
     }
 
     @Test
-    public void testOnPause_optimizationModeChanged_logPreference() {
+    public void onPause_optimizationModeChanged_logPreference() {
         final int mode = BatteryOptimizeUtils.MODE_RESTRICTED;
         mFragment.mOptimizationMode = mode;
         when(mBatteryOptimizeUtils.getAppOptimizationMode()).thenReturn(mode);
@@ -849,7 +849,7 @@
     }
 
     @Test
-    public void testOnPause_optimizationModeIsNotChanged_notInvokeLogging() {
+    public void onPause_optimizationModeIsNotChanged_notInvokeLogging() {
         final int mode = BatteryOptimizeUtils.MODE_OPTIMIZED;
         mFragment.mOptimizationMode = mode;
         when(mBatteryOptimizeUtils.getAppOptimizationMode()).thenReturn(mode);
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batterytip/detectors/BatteryDefenderDetectorTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batterytip/detectors/BatteryDefenderDetectorTest.java
index a1f9d1f..3309f59 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/batterytip/detectors/BatteryDefenderDetectorTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batterytip/detectors/BatteryDefenderDetectorTest.java
@@ -41,7 +41,8 @@
 
         mBatteryInfo.discharging = false;
 
-        mBatteryDefenderDetector = new BatteryDefenderDetector(mBatteryInfo);
+        mBatteryDefenderDetector = new BatteryDefenderDetector(
+            mBatteryInfo, /* extraDefend= */ false);
     }
 
     @Test
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batterytip/tips/BatteryDefenderTipTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batterytip/tips/BatteryDefenderTipTest.java
index 8660c79..c1ec7c6 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/batterytip/tips/BatteryDefenderTipTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batterytip/tips/BatteryDefenderTipTest.java
@@ -17,13 +17,25 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import android.app.settings.SettingsEnums;
 import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.BatteryManager;
+import android.util.Log;
+
+import androidx.preference.Preference;
 
 import com.android.settings.R;
 import com.android.settings.testutils.FakeFeatureFactory;
+import com.android.settings.widget.CardPreference;
 import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
 
 import org.junit.Before;
@@ -33,6 +45,7 @@
 import org.mockito.MockitoAnnotations;
 import org.robolectric.RobolectricTestRunner;
 import org.robolectric.RuntimeEnvironment;
+import org.robolectric.shadows.ShadowLog;
 
 @RunWith(RobolectricTestRunner.class)
 public class BatteryDefenderTipTest {
@@ -43,6 +56,8 @@
     private MetricsFeatureProvider mMetricsFeatureProvider;
 
     @Mock private BatteryTip mBatteryTip;
+    @Mock private Preference mPreference;
+    @Mock private CardPreference mCardPreference;
 
     @Before
     public void setUp() {
@@ -52,6 +67,9 @@
         mMetricsFeatureProvider = mFeatureFactory.metricsFeatureProvider;
         mContext = RuntimeEnvironment.application;
         mBatteryDefenderTip = new BatteryDefenderTip(BatteryTip.StateType.NEW);
+
+        when(mPreference.getContext()).thenReturn(mContext);
+        when(mCardPreference.getContext()).thenReturn(mContext);
     }
 
     @Test
@@ -61,12 +79,20 @@
     }
 
     @Test
-    public void getSummary_showSummary() {
+    public void getSummary_notExtraDefended_showNonExtraDefendedSummary() {
         assertThat(mBatteryDefenderTip.getSummary(mContext))
                 .isEqualTo(mContext.getString(R.string.battery_tip_limited_temporarily_summary));
     }
 
     @Test
+    public void getSummary_extraDefended_showExtraDefendedSummary() {
+        BatteryDefenderTip defenderTip = new BatteryDefenderTip(
+                BatteryTip.StateType.NEW, /* extraDefended= */ true);
+
+        assertThat(defenderTip.getSummary(mContext).toString()).isEqualTo("12%");
+    }
+
+    @Test
     public void getIcon_showIcon() {
         assertThat(mBatteryDefenderTip.getIconId())
                 .isEqualTo(R.drawable.ic_battery_status_good_24dp);
@@ -80,4 +106,94 @@
         verify(mMetricsFeatureProvider).action(mContext,
                 SettingsEnums.ACTION_BATTERY_DEFENDER_TIP, mBatteryTip.mState);
     }
+
+    @Test
+    public void updatePreference_castFail_logErrorMessage() {
+        mBatteryDefenderTip.updatePreference(mPreference);
+
+        assertThat(getLastErrorLog()).isEqualTo("cast Preference to CardPreference failed");
+    }
+
+    @Test
+    public void updatePreference_shouldSetPrimaryButtonText() {
+        String expectedText = mContext.getString(R.string.battery_tip_charge_to_full_button);
+
+        mBatteryDefenderTip.updatePreference(mCardPreference);
+
+        verify(mCardPreference).setPrimaryButtonText(expectedText);
+    }
+
+    @Test
+    public void updatePreference_shouldSetSecondaryButtonText() {
+        String expected = mContext.getString(R.string.see_more);
+
+        mBatteryDefenderTip.updatePreference(mCardPreference);
+
+        verify(mCardPreference).setSecondaryButtonText(expected);
+    }
+
+    @Test
+    public void updatePreference_shouldSetSecondaryButtonVisible() {
+        mBatteryDefenderTip.updatePreference(mCardPreference);
+
+        verify(mCardPreference).setSecondaryButtonVisible(true);
+    }
+
+    @Test
+    public void updatePreference_whenCharging_setPrimaryButtonVisibleToBeTrue() {
+        fakeDeviceIsCharging(true);
+
+        mBatteryDefenderTip.updatePreference(mCardPreference);
+
+        verify(mCardPreference).setPrimaryButtonVisible(true);
+    }
+
+    @Test
+    public void updatePreference_whenNotCharging_setPrimaryButtonVisibleToBeFalse() {
+        fakeDeviceIsCharging(false);
+
+        mBatteryDefenderTip.updatePreference(mCardPreference);
+
+        verify(mCardPreference).setPrimaryButtonVisible(false);
+    }
+
+    @Test
+    public void updatePreference_whenGetChargingStatusFailed_setPrimaryButtonVisibleToBeFalse() {
+        fakeGetChargingStatusFailed();
+
+        mBatteryDefenderTip.updatePreference(mCardPreference);
+
+        verify(mCardPreference).setPrimaryButtonVisible(false);
+    }
+
+    private void fakeDeviceIsCharging(boolean charging) {
+        int charged = charging ? 1 : 0; // 1 means charging, 0:not charging
+        Intent batteryChangedIntent = new Intent(Intent.ACTION_BATTERY_CHANGED);
+        batteryChangedIntent.putExtra(BatteryManager.EXTRA_PLUGGED, charged);
+
+        Context mockContext = mock(Context.class);
+        when(mockContext.getString(anyInt())).thenReturn("fake_string");
+        when(mCardPreference.getContext()).thenReturn(mockContext);
+        when(mockContext.registerReceiver(eq(null), any(IntentFilter.class)))
+                .thenReturn(batteryChangedIntent);
+    }
+
+    private void fakeGetChargingStatusFailed() {
+        Context mockContext = mock(Context.class);
+        when(mockContext.getString(anyInt())).thenReturn("fake_string");
+        when(mCardPreference.getContext()).thenReturn(mockContext);
+        when(mockContext.registerReceiver(eq(null), any(IntentFilter.class))).thenReturn(null);
+    }
+
+    private String getLastErrorLog() {
+        return ShadowLog.getLogsForTag(BatteryDefenderTip.class.getSimpleName()).stream()
+                .filter(log -> log.type == Log.ERROR)
+                .reduce((first, second) -> second)
+                .orElse(createErrorLog("No Error Log"))
+                .msg;
+    }
+
+    private ShadowLog.LogItem createErrorLog(String msg) {
+        return new ShadowLog.LogItem(Log.ERROR, "tag", msg, null);
+    }
 }
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceControllerTest.java
index ec98226..016287e 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceControllerTest.java
@@ -18,23 +18,24 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.anyLong;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
 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.pm.PackageManager;
-import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.graphics.drawable.Drawable;
 import android.os.Bundle;
 import android.os.LocaleList;
 import android.text.format.DateUtils;
+import android.view.View;
+import android.widget.LinearLayout;
 
 import androidx.preference.Preference;
 import androidx.preference.PreferenceCategory;
@@ -58,15 +59,15 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Locale;
 import java.util.Map;
+import java.util.TimeZone;
 
 @RunWith(RobolectricTestRunner.class)
 public final class BatteryChartPreferenceControllerTest {
     private static final String PREF_KEY = "pref_key";
     private static final String PREF_SUMMARY = "fake preference summary";
-    private static final int DESIRED_HISTORY_SIZE =
-            BatteryChartPreferenceController.DESIRED_HISTORY_SIZE;
 
     @Mock
     private InstrumentedPreferenceFragment mFragment;
@@ -79,11 +80,15 @@
     @Mock
     private BatteryHistEntry mBatteryHistEntry;
     @Mock
-    private BatteryChartView mBatteryChartView;
+    private BatteryChartView mDailyChartView;
+    @Mock
+    private BatteryChartView mHourlyChartView;
     @Mock
     private PowerGaugePreference mPowerGaugePreference;
     @Mock
     private BatteryUtils mBatteryUtils;
+    @Mock
+    private LinearLayout.LayoutParams mLayoutParams;
 
     private Context mContext;
     private FakeFeatureFactory mFeatureFactory;
@@ -96,6 +101,7 @@
         MockitoAnnotations.initMocks(this);
         Locale.setDefault(new Locale("en_US"));
         org.robolectric.shadows.ShadowSettings.set24HourTimeFormat(false);
+        TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
         mFeatureFactory = FakeFeatureFactory.setupForTest();
         mMetricsFeatureProvider = mFeatureFactory.metricsFeatureProvider;
         mContext = spy(RuntimeEnvironment.application);
@@ -108,10 +114,12 @@
         doReturn(new String[]{"com.android.gms.persistent"})
                 .when(mFeatureFactory.powerUsageFeatureProvider)
                 .getHideApplicationEntries(mContext);
+        doReturn(mLayoutParams).when(mDailyChartView).getLayoutParams();
         mBatteryChartPreferenceController = createController();
         mBatteryChartPreferenceController.mPrefContext = mContext;
         mBatteryChartPreferenceController.mAppListPrefGroup = mAppListGroup;
-        mBatteryChartPreferenceController.mBatteryChartView = mBatteryChartView;
+        mBatteryChartPreferenceController.mDailyChartView = mDailyChartView;
+        mBatteryChartPreferenceController.mHourlyChartView = mHourlyChartView;
         mBatteryDiffEntry = new BatteryDiffEntry(
                 mContext,
                 /*foregroundUsageTimeInMs=*/ 1,
@@ -123,12 +131,10 @@
         BatteryDiffEntry.sResourceCache.put(
                 "fakeBatteryDiffEntryKey",
                 new BatteryEntry.NameAndIcon("fakeName", /*icon=*/ null, /*iconId=*/ 1));
-        mBatteryChartPreferenceController.setBatteryHistoryMap(
-                createBatteryHistoryMap());
     }
 
     @Test
-    public void testOnDestroy_activityIsChanging_clearBatteryEntryCache() {
+    public void onDestroy_activityIsChanging_clearBatteryEntryCache() {
         doReturn(true).when(mSettingsActivity).isChangingConfigurations();
         // Ensures the testing environment is correct.
         assertThat(BatteryDiffEntry.sResourceCache).hasSize(1);
@@ -138,7 +144,7 @@
     }
 
     @Test
-    public void testOnDestroy_activityIsNotChanging_notClearBatteryEntryCache() {
+    public void onDestroy_activityIsNotChanging_notClearBatteryEntryCache() {
         doReturn(false).when(mSettingsActivity).isChangingConfigurations();
         // Ensures the testing environment is correct.
         assertThat(BatteryDiffEntry.sResourceCache).hasSize(1);
@@ -148,7 +154,7 @@
     }
 
     @Test
-    public void testOnDestroy_clearPreferenceCache() {
+    public void onDestroy_clearPreferenceCache() {
         // Ensures the testing environment is correct.
         mBatteryChartPreferenceController.mPreferenceCache.put(
                 PREF_KEY, mPowerGaugePreference);
@@ -160,113 +166,135 @@
     }
 
     @Test
-    public void testOnDestroy_removeAllPreferenceFromPreferenceGroup() {
+    public void onDestroy_removeAllPreferenceFromPreferenceGroup() {
         mBatteryChartPreferenceController.onDestroy();
         verify(mAppListGroup).removeAll();
     }
 
     @Test
-    public void testSetBatteryHistoryMap_createExpectedKeysAndLevels() {
-        mBatteryChartPreferenceController.setBatteryHistoryMap(
-                createBatteryHistoryMap());
+    public void setBatteryChartViewModel_6Hours() {
+        mBatteryChartPreferenceController.setBatteryHistoryMap(createBatteryHistoryMap(6));
 
-        // 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);
+        verify(mDailyChartView, atLeastOnce()).setVisibility(View.GONE);
+        verify(mHourlyChartView, atLeastOnce()).setVisibility(View.VISIBLE);
+        verify(mHourlyChartView).setViewModel(new BatteryChartViewModel(
+                List.of(100, 97, 95),
+                List.of("8 am", "10 am", "12 pm"),
+                BatteryChartViewModel.AxisLabelPosition.BETWEEN_TRAPEZOIDS));
     }
 
     @Test
-    public void testSetBatteryHistoryMap_largeSize_createExpectedKeysAndLevels() {
-        mBatteryChartPreferenceController.setBatteryHistoryMap(
-                createBatteryHistoryMap());
+    public void setBatteryChartViewModel_60Hours() {
+        BatteryChartViewModel expectedDailyViewModel = new BatteryChartViewModel(
+                List.of(100, 83, 59, 41),
+                List.of("Sat", "Sun", "Mon", "Mon"),
+                BatteryChartViewModel.AxisLabelPosition.CENTER_OF_TRAPEZOIDS);
 
-        // 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);
+        mBatteryChartPreferenceController.setBatteryHistoryMap(createBatteryHistoryMap(60));
+
+        verify(mDailyChartView, atLeastOnce()).setVisibility(View.VISIBLE);
+        verify(mHourlyChartView, atLeastOnce()).setVisibility(View.GONE);
+        verify(mDailyChartView).setViewModel(expectedDailyViewModel);
+
+        reset(mDailyChartView);
+        reset(mHourlyChartView);
+        doReturn(mLayoutParams).when(mDailyChartView).getLayoutParams();
+        mBatteryChartPreferenceController.mDailyChartIndex = 0;
+        mBatteryChartPreferenceController.refreshUi();
+        verify(mDailyChartView).setVisibility(View.VISIBLE);
+        verify(mHourlyChartView).setVisibility(View.VISIBLE);
+
+        expectedDailyViewModel.setSelectedIndex(0);
+        verify(mDailyChartView).setViewModel(expectedDailyViewModel);
+        verify(mHourlyChartView).setViewModel(new BatteryChartViewModel(
+                List.of(100, 97, 95, 93, 91, 89, 87, 85, 83),
+                List.of("8 am", "10 am", "12 pm", "2 pm", "4 pm", "6 pm", "8 pm", "10 pm",
+                        "12 am"),
+                BatteryChartViewModel.AxisLabelPosition.BETWEEN_TRAPEZOIDS));
+
+        reset(mDailyChartView);
+        reset(mHourlyChartView);
+        doReturn(mLayoutParams).when(mDailyChartView).getLayoutParams();
+        mBatteryChartPreferenceController.mDailyChartIndex = 1;
+        mBatteryChartPreferenceController.mHourlyChartIndex = 6;
+        mBatteryChartPreferenceController.refreshUi();
+        verify(mDailyChartView).setVisibility(View.VISIBLE);
+        verify(mHourlyChartView).setVisibility(View.VISIBLE);
+        expectedDailyViewModel.setSelectedIndex(1);
+        verify(mDailyChartView).setViewModel(expectedDailyViewModel);
+        BatteryChartViewModel expectedHourlyViewModel = new BatteryChartViewModel(
+                List.of(83, 81, 79, 77, 75, 73, 71, 69, 67, 65, 63, 61, 59),
+                List.of("12 am", "2 am", "4 am", "6 am", "8 am", "10 am", "12 pm", "2 pm",
+                        "4 pm", "6 pm", "8 pm", "10 pm", "12 am"),
+                BatteryChartViewModel.AxisLabelPosition.BETWEEN_TRAPEZOIDS);
+        expectedHourlyViewModel.setSelectedIndex(6);
+        verify(mHourlyChartView).setViewModel(expectedHourlyViewModel);
+
+        reset(mDailyChartView);
+        reset(mHourlyChartView);
+        doReturn(mLayoutParams).when(mDailyChartView).getLayoutParams();
+        mBatteryChartPreferenceController.mDailyChartIndex = 2;
+        mBatteryChartPreferenceController.mHourlyChartIndex =
+                BatteryChartViewModel.SELECTED_INDEX_ALL;
+        mBatteryChartPreferenceController.refreshUi();
+        verify(mDailyChartView).setVisibility(View.VISIBLE);
+        verify(mHourlyChartView).setVisibility(View.VISIBLE);
+        expectedDailyViewModel.setSelectedIndex(2);
+        verify(mDailyChartView).setViewModel(expectedDailyViewModel);
+        verify(mHourlyChartView).setViewModel(new BatteryChartViewModel(
+                List.of(59, 57, 55, 53, 51, 49, 47, 45, 43, 41),
+                List.of("12 am", "2 am", "4 am", "6 am", "8 am", "10 am", "12 pm", "2 pm",
+                        "4 pm", "6 pm"),
+                BatteryChartViewModel.AxisLabelPosition.BETWEEN_TRAPEZOIDS));
     }
 
     @Test
-    public void testRefreshUi_batteryIndexedMapIsNull_ignoreRefresh() {
+    public void refreshUi_normalCase_returnTrue() {
+        mBatteryChartPreferenceController.setBatteryHistoryMap(createBatteryHistoryMap(6));
+        assertThat(mBatteryChartPreferenceController.refreshUi()).isTrue();
+    }
+
+    @Test
+    public void refreshUi_batteryIndexedMapIsNull_ignoreRefresh() {
         mBatteryChartPreferenceController.setBatteryHistoryMap(null);
-        assertThat(mBatteryChartPreferenceController.refreshUi(
-                /*trapezoidIndex=*/ 1, /*isForce=*/ false)).isFalse();
+        assertThat(mBatteryChartPreferenceController.refreshUi()).isFalse();
     }
 
     @Test
-    public void testRefreshUi_batteryChartViewIsNull_ignoreRefresh() {
-        mBatteryChartPreferenceController.mBatteryChartView = null;
-        assertThat(mBatteryChartPreferenceController.refreshUi(
-                /*trapezoidIndex=*/ 1, /*isForce=*/ false)).isFalse();
+    public void refreshUi_dailyChartViewIsNull_ignoreRefresh() {
+        mBatteryChartPreferenceController.mDailyChartView = null;
+        assertThat(mBatteryChartPreferenceController.refreshUi()).isFalse();
     }
 
     @Test
-    public void testRefreshUi_trapezoidIndexIsNotChanged_ignoreRefresh() {
-        final int trapezoidIndex = 1;
-        mBatteryChartPreferenceController.mTrapezoidIndex = trapezoidIndex;
-        assertThat(mBatteryChartPreferenceController.refreshUi(
-                trapezoidIndex, /*isForce=*/ false)).isFalse();
+    public void refreshUi_hourlyChartViewIsNull_ignoreRefresh() {
+        mBatteryChartPreferenceController.mHourlyChartView = null;
+        assertThat(mBatteryChartPreferenceController.refreshUi()).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 =
-                BatteryChartView.SELECTED_INDEX_INVALID;
-        mBatteryChartPreferenceController.setBatteryHistoryMap(
-                createBatteryHistoryMap());
-
-        assertThat(mBatteryChartPreferenceController.mTrapezoidIndex)
-                .isEqualTo(BatteryChartView.SELECTED_INDEX_ALL);
-    }
-
-    @Test
-    public void testRemoveAndCacheAllPrefs_emptyContent_ignoreRemoveAll() {
-        final int trapezoidIndex = 1;
+    public void removeAndCacheAllPrefs_emptyContent_ignoreRemoveAll() {
+        mBatteryChartPreferenceController.setBatteryHistoryMap(createBatteryHistoryMap(6));
+        mBatteryChartPreferenceController.mBatteryUsageMap = createBatteryUsageMap();
         doReturn(0).when(mAppListGroup).getPreferenceCount();
 
-        mBatteryChartPreferenceController.refreshUi(
-                trapezoidIndex, /*isForce=*/ true);
+        mBatteryChartPreferenceController.refreshUi();
         verify(mAppListGroup, never()).removeAll();
     }
 
     @Test
-    public void testRemoveAndCacheAllPrefs_buildCacheAndRemoveAllPreference() {
-        final int trapezoidIndex = 1;
+    public void removeAndCacheAllPrefs_buildCacheAndRemoveAllPreference() {
+        mBatteryChartPreferenceController.setBatteryHistoryMap(createBatteryHistoryMap(6));
+        mBatteryChartPreferenceController.mBatteryUsageMap = createBatteryUsageMap();
         doReturn(1).when(mAppListGroup).getPreferenceCount();
         doReturn(mPowerGaugePreference).when(mAppListGroup).getPreference(0);
+        doReturn(PREF_KEY).when(mBatteryHistEntry).getKey();
         doReturn(PREF_KEY).when(mPowerGaugePreference).getKey();
+        doReturn(mPowerGaugePreference).when(mAppListGroup).findPreference(PREF_KEY);
         // Ensures the testing data is correct.
         assertThat(mBatteryChartPreferenceController.mPreferenceCache).isEmpty();
 
-        mBatteryChartPreferenceController.refreshUi(
-                trapezoidIndex, /*isForce=*/ true);
+        mBatteryChartPreferenceController.refreshUi();
 
         assertThat(mBatteryChartPreferenceController.mPreferenceCache.get(PREF_KEY))
                 .isEqualTo(mPowerGaugePreference);
@@ -274,14 +302,14 @@
     }
 
     @Test
-    public void testAddPreferenceToScreen_emptyContent_ignoreAddPreference() {
+    public void addPreferenceToScreen_emptyContent_ignoreAddPreference() {
         mBatteryChartPreferenceController.addPreferenceToScreen(
                 new ArrayList<BatteryDiffEntry>());
         verify(mAppListGroup, never()).addPreference(any());
     }
 
     @Test
-    public void testAddPreferenceToScreen_addPreferenceIntoScreen() {
+    public void addPreferenceToScreen_addPreferenceIntoScreen() {
         final String appLabel = "fake app label";
         doReturn(1).when(mAppListGroup).getPreferenceCount();
         doReturn(mDrawable).when(mBatteryDiffEntry).getAppIcon();
@@ -310,7 +338,7 @@
     }
 
     @Test
-    public void testAddPreferenceToScreen_alreadyInScreen_notAddPreferenceAgain() {
+    public void addPreferenceToScreen_alreadyInScreen_notAddPreferenceAgain() {
         final String appLabel = "fake app label";
         doReturn(1).when(mAppListGroup).getPreferenceCount();
         doReturn(mDrawable).when(mBatteryDiffEntry).getAppIcon();
@@ -325,7 +353,7 @@
     }
 
     @Test
-    public void testHandlePreferenceTreeiClick_notPowerGaugePreference_returnFalse() {
+    public void handlePreferenceTreeClick_notPowerGaugePreference_returnFalse() {
         assertThat(mBatteryChartPreferenceController.handlePreferenceTreeClick(mAppListGroup))
                 .isFalse();
 
@@ -336,7 +364,7 @@
     }
 
     @Test
-    public void testHandlePreferenceTreeClick_forAppEntry_returnTrue() {
+    public void handlePreferenceTreeClick_forAppEntry_returnTrue() {
         doReturn(false).when(mBatteryHistEntry).isAppEntry();
         doReturn(mBatteryDiffEntry).when(mPowerGaugePreference).getBatteryDiffEntry();
 
@@ -352,7 +380,7 @@
     }
 
     @Test
-    public void testHandlePreferenceTreeClick_forSystemEntry_returnTrue() {
+    public void handlePreferenceTreeClick_forSystemEntry_returnTrue() {
         mBatteryChartPreferenceController.mBatteryUtils = mBatteryUtils;
         doReturn(true).when(mBatteryHistEntry).isAppEntry();
         doReturn(mBatteryDiffEntry).when(mPowerGaugePreference).getBatteryDiffEntry();
@@ -369,7 +397,7 @@
     }
 
     @Test
-    public void testSetPreferenceSummary_setNullContentIfTotalUsageTimeIsZero() {
+    public void setPreferenceSummary_setNullContentIfTotalUsageTimeIsZero() {
         final PowerGaugePreference pref = new PowerGaugePreference(mContext);
         pref.setSummary(PREF_SUMMARY);
 
@@ -381,7 +409,7 @@
     }
 
     @Test
-    public void testSetPreferenceSummary_setBackgroundUsageTimeOnly() {
+    public void setPreferenceSummary_setBackgroundUsageTimeOnly() {
         final PowerGaugePreference pref = new PowerGaugePreference(mContext);
         pref.setSummary(PREF_SUMMARY);
 
@@ -393,7 +421,7 @@
     }
 
     @Test
-    public void testSetPreferenceSummary_setTotalUsageTimeLessThanAMinute() {
+    public void setPreferenceSummary_setTotalUsageTimeLessThanAMinute() {
         final PowerGaugePreference pref = new PowerGaugePreference(mContext);
         pref.setSummary(PREF_SUMMARY);
 
@@ -405,7 +433,7 @@
     }
 
     @Test
-    public void testSetPreferenceSummary_setTotalTimeIfBackgroundTimeLessThanAMinute() {
+    public void setPreferenceSummary_setTotalTimeIfBackgroundTimeLessThanAMinute() {
         final PowerGaugePreference pref = new PowerGaugePreference(mContext);
         pref.setSummary(PREF_SUMMARY);
 
@@ -418,7 +446,7 @@
     }
 
     @Test
-    public void testSetPreferenceSummary_setTotalAndBackgroundUsageTime() {
+    public void setPreferenceSummary_setTotalAndBackgroundUsageTime() {
         final PowerGaugePreference pref = new PowerGaugePreference(mContext);
         pref.setSummary(PREF_SUMMARY);
 
@@ -430,7 +458,7 @@
     }
 
     @Test
-    public void testSetPreferenceSummary_notAllowShownPackage_setSummayAsNull() {
+    public void setPreferenceSummary_notAllowShownPackage_setSummayAsNull() {
         final PowerGaugePreference pref = new PowerGaugePreference(mContext);
         pref.setSummary(PREF_SUMMARY);
         final BatteryDiffEntry batteryDiffEntry =
@@ -445,36 +473,9 @@
     }
 
     @Test
-    public void testValidateUsageTime_returnTrueIfBatteryDiffEntryIsValid() {
-        assertThat(BatteryChartPreferenceController.validateUsageTime(
-                createBatteryDiffEntry(
-                        /*foregroundUsageTimeInMs=*/ DateUtils.MINUTE_IN_MILLIS,
-                        /*backgroundUsageTimeInMs=*/ DateUtils.MINUTE_IN_MILLIS)))
-                .isTrue();
-    }
-
-    @Test
-    public void testValidateUsageTime_foregroundTimeExceedThreshold_returnFalse() {
-        assertThat(BatteryChartPreferenceController.validateUsageTime(
-                createBatteryDiffEntry(
-                        /*foregroundUsageTimeInMs=*/ DateUtils.HOUR_IN_MILLIS * 3,
-                        /*backgroundUsageTimeInMs=*/ 0)))
-                .isFalse();
-    }
-
-    @Test
-    public void testValidateUsageTime_backgroundTimeExceedThreshold_returnFalse() {
-        assertThat(BatteryChartPreferenceController.validateUsageTime(
-                createBatteryDiffEntry(
-                        /*foregroundUsageTimeInMs=*/ 0,
-                        /*backgroundUsageTimeInMs=*/ DateUtils.HOUR_IN_MILLIS * 3)))
-                .isFalse();
-    }
-
-    @Test
-    public void testOnExpand_expandedIsTrue_addSystemEntriesToPreferenceGroup() {
+    public void onExpand_expandedIsTrue_addSystemEntriesToPreferenceGroup() {
         doReturn(1).when(mAppListGroup).getPreferenceCount();
-        mBatteryChartPreferenceController.mSystemEntries.add(mBatteryDiffEntry);
+        mBatteryChartPreferenceController.mBatteryUsageMap = createBatteryUsageMap();
         doReturn("label").when(mBatteryDiffEntry).getAppLabel();
         doReturn(mDrawable).when(mBatteryDiffEntry).getAppIcon();
         doReturn(PREF_KEY).when(mBatteryHistEntry).getKey();
@@ -493,10 +494,10 @@
     }
 
     @Test
-    public void testOnExpand_expandedIsFalse_removeSystemEntriesFromPreferenceGroup() {
+    public void onExpand_expandedIsFalse_removeSystemEntriesFromPreferenceGroup() {
         doReturn(PREF_KEY).when(mBatteryHistEntry).getKey();
         doReturn(mPowerGaugePreference).when(mAppListGroup).findPreference(PREF_KEY);
-        mBatteryChartPreferenceController.mSystemEntries.add(mBatteryDiffEntry);
+        mBatteryChartPreferenceController.mBatteryUsageMap = createBatteryUsageMap();
         // Verifies the cache is empty first.
         assertThat(mBatteryChartPreferenceController.mPreferenceCache).isEmpty();
 
@@ -513,57 +514,17 @@
     }
 
     @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(
-                BatteryChartView.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() {
+    public void refreshCategoryTitle_setLastFullChargeIntoBothTitleTextView() {
         mBatteryChartPreferenceController = createController();
         mBatteryChartPreferenceController.mAppListPrefGroup =
                 spy(new PreferenceCategory(mContext));
         mBatteryChartPreferenceController.mExpandDividerPreference =
                 spy(new ExpandDividerPreference(mContext));
         // Simulates select all condition.
-        mBatteryChartPreferenceController.mTrapezoidIndex =
-                BatteryChartView.SELECTED_INDEX_ALL;
+        mBatteryChartPreferenceController.mDailyChartIndex =
+                BatteryChartViewModel.SELECTED_INDEX_ALL;
+        mBatteryChartPreferenceController.mHourlyChartIndex =
+                BatteryChartViewModel.SELECTED_INDEX_ALL;
 
         mBatteryChartPreferenceController.refreshCategoryTitle();
 
@@ -572,76 +533,93 @@
         verify(mBatteryChartPreferenceController.mAppListPrefGroup)
                 .setTitle(captor.capture());
         assertThat(captor.getValue())
-                .isEqualTo("App usage for past 24 hr");
+                .isEqualTo("App usage since last full charge");
         // 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");
+                .isEqualTo("System usage since last full charge");
     }
 
     @Test
-    public void testSetTimestampLabel_nullBatteryHistoryKeys_ignore() {
-        mBatteryChartPreferenceController = createController();
-        mBatteryChartPreferenceController.mBatteryHistoryKeys = null;
-        mBatteryChartPreferenceController.mBatteryChartView =
-                spy(new BatteryChartView(mContext));
-        mBatteryChartPreferenceController.setTimestampLabel();
+    public void selectedSlotText_selectAllDaysAllHours_returnNull() {
+        mBatteryChartPreferenceController.setBatteryHistoryMap(createBatteryHistoryMap(60));
+        mBatteryChartPreferenceController.mDailyChartIndex =
+                BatteryChartViewModel.SELECTED_INDEX_ALL;
+        mBatteryChartPreferenceController.mHourlyChartIndex =
+                BatteryChartViewModel.SELECTED_INDEX_ALL;
 
-        verify(mBatteryChartPreferenceController.mBatteryChartView, never())
-                .setLatestTimestamp(anyLong());
+        assertThat(mBatteryChartPreferenceController.getSlotInformation()).isEqualTo(null);
     }
 
     @Test
-    public void testSetTimestampLabel_setExpectedTimestampData() {
-        mBatteryChartPreferenceController = createController();
-        mBatteryChartPreferenceController.mBatteryChartView =
-                spy(new BatteryChartView(mContext));
-        setUpBatteryHistoryKeys();
+    public void selectedSlotText_onlyOneDayDataSelectAllHours_returnNull() {
+        mBatteryChartPreferenceController.setBatteryHistoryMap(createBatteryHistoryMap(6));
+        mBatteryChartPreferenceController.mDailyChartIndex = 0;
+        mBatteryChartPreferenceController.mHourlyChartIndex =
+                BatteryChartViewModel.SELECTED_INDEX_ALL;
 
-        mBatteryChartPreferenceController.setTimestampLabel();
-
-        verify(mBatteryChartPreferenceController.mBatteryChartView)
-                .setLatestTimestamp(1619247636826L);
+        assertThat(mBatteryChartPreferenceController.getSlotInformation()).isEqualTo(null);
     }
 
     @Test
-    public void testSetTimestampLabel_withoutValidTimestamp_setExpectedTimestampData() {
-        mBatteryChartPreferenceController = createController();
-        mBatteryChartPreferenceController.mBatteryChartView =
-                spy(new BatteryChartView(mContext));
-        mBatteryChartPreferenceController.mBatteryHistoryKeys = new long[]{0L};
+    public void selectedSlotText_selectADayAllHours_onlyDayText() {
+        mBatteryChartPreferenceController.setBatteryHistoryMap(createBatteryHistoryMap(60));
+        mBatteryChartPreferenceController.mDailyChartIndex = 1;
+        mBatteryChartPreferenceController.mHourlyChartIndex =
+                BatteryChartViewModel.SELECTED_INDEX_ALL;
 
-        mBatteryChartPreferenceController.setTimestampLabel();
-
-        verify(mBatteryChartPreferenceController.mBatteryChartView)
-                .setLatestTimestamp(anyLong());
+        assertThat(mBatteryChartPreferenceController.getSlotInformation()).isEqualTo("Sunday");
     }
 
     @Test
-    public void testOnSaveInstanceState_restoreSelectedIndexAndExpandState() {
-        final int expectedIndex = 1;
+    public void selectedSlotText_onlyOneDayDataSelectAnHour_onlyHourText() {
+        mBatteryChartPreferenceController.setBatteryHistoryMap(createBatteryHistoryMap(6));
+        mBatteryChartPreferenceController.mDailyChartIndex = 0;
+        mBatteryChartPreferenceController.mHourlyChartIndex = 1;
+
+        assertThat(mBatteryChartPreferenceController.getSlotInformation()).isEqualTo(
+                "10 am - 12 pm");
+    }
+
+    @Test
+    public void selectedSlotText_SelectADayAnHour_dayAndHourText() {
+        mBatteryChartPreferenceController.setBatteryHistoryMap(createBatteryHistoryMap(60));
+        mBatteryChartPreferenceController.mDailyChartIndex = 1;
+        mBatteryChartPreferenceController.mHourlyChartIndex = 8;
+
+        assertThat(mBatteryChartPreferenceController.getSlotInformation()).isEqualTo(
+                "Sunday 4 pm - 6 pm");
+    }
+
+    @Test
+    public void onSaveInstanceState_restoreSelectedIndexAndExpandState() {
+        final int expectedDailyIndex = 1;
+        final int expectedHourlyIndex = 2;
         final boolean isExpanded = true;
         final Bundle bundle = new Bundle();
-        mBatteryChartPreferenceController.mTrapezoidIndex = expectedIndex;
+        mBatteryChartPreferenceController.mDailyChartIndex = expectedDailyIndex;
+        mBatteryChartPreferenceController.mHourlyChartIndex = expectedHourlyIndex;
         mBatteryChartPreferenceController.mIsExpanded = isExpanded;
         mBatteryChartPreferenceController.onSaveInstanceState(bundle);
         // Replaces the original controller with other values.
-        mBatteryChartPreferenceController.mTrapezoidIndex = -1;
+        mBatteryChartPreferenceController.mDailyChartIndex = -1;
+        mBatteryChartPreferenceController.mHourlyChartIndex = -1;
         mBatteryChartPreferenceController.mIsExpanded = false;
 
         mBatteryChartPreferenceController.onCreate(bundle);
-        mBatteryChartPreferenceController.setBatteryHistoryMap(
-                createBatteryHistoryMap());
+        mBatteryChartPreferenceController.setBatteryHistoryMap(createBatteryHistoryMap(25));
 
-        assertThat(mBatteryChartPreferenceController.mTrapezoidIndex)
-                .isEqualTo(expectedIndex);
+        assertThat(mBatteryChartPreferenceController.mDailyChartIndex)
+                .isEqualTo(expectedDailyIndex);
+        assertThat(mBatteryChartPreferenceController.mHourlyChartIndex)
+                .isEqualTo(expectedHourlyIndex);
         assertThat(mBatteryChartPreferenceController.mIsExpanded).isTrue();
     }
 
     @Test
-    public void testIsValidToShowSummary_returnExpectedResult() {
+    public void isValidToShowSummary_returnExpectedResult() {
         assertThat(mBatteryChartPreferenceController
                 .isValidToShowSummary("com.google.android.apps.scone"))
                 .isTrue();
@@ -652,31 +630,42 @@
                 .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 Long generateTimestamp(int index) {
+        // "2021-04-23 07:00:00 UTC" + index hours
+        return 1619247600000L + index * DateUtils.HOUR_IN_MILLIS;
     }
 
-    private static Map<Long, Map<String, BatteryHistEntry>> createBatteryHistoryMap() {
+    private static Map<Long, Map<String, BatteryHistEntry>> createBatteryHistoryMap(
+            int numOfHours) {
         final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap = new HashMap<>();
-        for (int index = 0; index < DESIRED_HISTORY_SIZE; index++) {
+        for (int index = 0; index < numOfHours; index++) {
             final ContentValues values = new ContentValues();
             values.put("batteryLevel", Integer.valueOf(100 - index));
+            values.put("consumePower", 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);
+            batteryHistoryMap.put(generateTimestamp(index), entryMap);
         }
         return batteryHistoryMap;
     }
 
+    private Map<Integer, Map<Integer, BatteryDiffData>> createBatteryUsageMap() {
+        final int selectedAll = BatteryChartViewModel.SELECTED_INDEX_ALL;
+        return Map.of(
+                selectedAll, Map.of(
+                        selectedAll, new BatteryDiffData(
+                                Arrays.asList(mBatteryDiffEntry),
+                                Arrays.asList(mBatteryDiffEntry))),
+                0, Map.of(
+                        selectedAll, new BatteryDiffData(
+                                Arrays.asList(mBatteryDiffEntry),
+                                Arrays.asList(mBatteryDiffEntry)),
+                        0, new BatteryDiffData(
+                                Arrays.asList(mBatteryDiffEntry),
+                                Arrays.asList(mBatteryDiffEntry))));
+    }
+
     private BatteryDiffEntry createBatteryDiffEntry(
             long foregroundUsageTimeInMs, long backgroundUsageTimeInMs) {
         return new BatteryDiffEntry(
@@ -684,13 +673,6 @@
                 /*consumePower=*/ 0, mBatteryHistEntry);
     }
 
-    private void setUpBatteryHistoryKeys() {
-        mBatteryChartPreferenceController.mBatteryHistoryKeys =
-                new long[]{1619196786769L, 0L, 1619247636826L};
-        ConvertUtils.utcToLocalTimeHour(
-                mContext, /*timestamp=*/ 0, /*is24HourFormat=*/ false);
-    }
-
     private BatteryChartPreferenceController createController() {
         final BatteryChartPreferenceController controller =
                 new BatteryChartPreferenceController(
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryChartViewTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryChartViewTest.java
index a2d8ca9..8a43087 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryChartViewTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryChartViewTest.java
@@ -26,6 +26,7 @@
 import android.accessibilityservice.AccessibilityServiceInfo;
 import android.content.Context;
 import android.os.LocaleList;
+import android.view.View;
 import android.view.accessibility.AccessibilityManager;
 
 import com.android.settings.fuelgauge.PowerUsageFeatureProvider;
@@ -41,6 +42,7 @@
 
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.List;
 import java.util.Locale;
 
 @RunWith(RobolectricTestRunner.class)
@@ -55,6 +57,8 @@
     private AccessibilityServiceInfo mMockAccessibilityServiceInfo;
     @Mock
     private AccessibilityManager mMockAccessibilityManager;
+    @Mock
+    private View mMockView;
 
     @Before
     public void setUp() {
@@ -74,13 +78,13 @@
     }
 
     @Test
-    public void testIsAccessibilityEnabled_disable_returnFalse() {
+    public void isAccessibilityEnabled_disable_returnFalse() {
         doReturn(false).when(mMockAccessibilityManager).isEnabled();
         assertThat(BatteryChartView.isAccessibilityEnabled(mContext)).isFalse();
     }
 
     @Test
-    public void testIsAccessibilityEnabled_emptyInfo_returnFalse() {
+    public void isAccessibilityEnabled_emptyInfo_returnFalse() {
         doReturn(true).when(mMockAccessibilityManager).isEnabled();
         doReturn(new ArrayList<AccessibilityServiceInfo>())
                 .when(mMockAccessibilityManager)
@@ -90,68 +94,70 @@
     }
 
     @Test
-    public void testIsAccessibilityEnabled_validServiceId_returnTrue() {
+    public void isAccessibilityEnabled_validServiceId_returnTrue() {
         doReturn(true).when(mMockAccessibilityManager).isEnabled();
         assertThat(BatteryChartView.isAccessibilityEnabled(mContext)).isTrue();
     }
 
     @Test
-    public void testSetSelectedIndex_invokesCallback() {
+    public void onClick_invokesCallback() {
+        final int originalSelectedIndex = 2;
+        BatteryChartViewModel batteryChartViewModel = new BatteryChartViewModel(
+                List.of(90, 80, 70, 60), List.of("", "", "", ""),
+                BatteryChartViewModel.AxisLabelPosition.BETWEEN_TRAPEZOIDS);
+        batteryChartViewModel.setSelectedIndex(originalSelectedIndex);
+        mBatteryChartView.setViewModel(batteryChartViewModel);
+        for (int i = 0; i < mBatteryChartView.mTrapezoidSlots.length; i++) {
+            mBatteryChartView.mTrapezoidSlots[i] = new BatteryChartView.TrapezoidSlot();
+            mBatteryChartView.mTrapezoidSlots[i].mLeft = i;
+            mBatteryChartView.mTrapezoidSlots[i].mRight = i + 0.5f;
+        }
         final int[] selectedIndex = new int[1];
-        final int expectedIndex = 2;
-        mBatteryChartView.mSelectedIndex = 1;
         mBatteryChartView.setOnSelectListener(
                 trapezoidIndex -> {
                     selectedIndex[0] = trapezoidIndex;
                 });
 
-        mBatteryChartView.setSelectedIndex(expectedIndex);
+        // Verify onClick() a different index 1.
+        mBatteryChartView.mTouchUpEventX = 1;
+        selectedIndex[0] = Integer.MIN_VALUE;
+        mBatteryChartView.onClick(mMockView);
+        assertThat(selectedIndex[0]).isEqualTo(1);
 
-        assertThat(mBatteryChartView.mSelectedIndex)
-                .isEqualTo(expectedIndex);
-        assertThat(selectedIndex[0]).isEqualTo(expectedIndex);
+        // Verify onClick() the same index 2.
+        mBatteryChartView.mTouchUpEventX = 2;
+        selectedIndex[0] = Integer.MIN_VALUE;
+        mBatteryChartView.onClick(mMockView);
+        assertThat(selectedIndex[0]).isEqualTo(BatteryChartViewModel.SELECTED_INDEX_ALL);
     }
 
     @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() {
+    public void clickable_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() {
+    public void clickable_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() {
+    public void clickable_accessibilityIsEnabledWithoutValidId_clickable() {
         mBatteryChartView.setClickableForce(true);
         when(mPowerUsageFeatureProvider.isChartGraphSlotsEnabled(mContext))
                 .thenReturn(true);
@@ -161,30 +167,34 @@
                 .getEnabledAccessibilityServiceList(anyInt());
 
         mBatteryChartView.onAttachedToWindow();
+
         assertThat(mBatteryChartView.isClickable()).isTrue();
         assertThat(mBatteryChartView.mTrapezoidCurvePaint).isNull();
     }
 
     @Test
-    public void testClickable_accessibilityIsEnabledWithValidId_notClickable() {
+    public void clickable_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;
+    public void clickable_restoreFromNonClickableState() {
+        final List<Integer> levels = new ArrayList<Integer>();
+        final List<String> texts = new ArrayList<String>();
+        for (int index = 0; index < 13; index++) {
+            levels.add(index + 1);
+            texts.add("");
         }
-        mBatteryChartView.setTrapezoidCount(12);
-        mBatteryChartView.setLevels(levels);
+        mBatteryChartView.setViewModel(new BatteryChartViewModel(levels, texts,
+                BatteryChartViewModel.AxisLabelPosition.BETWEEN_TRAPEZOIDS));
         mBatteryChartView.setClickableForce(true);
         when(mPowerUsageFeatureProvider.isChartGraphSlotsEnabled(mContext))
                 .thenReturn(true);
@@ -201,14 +211,14 @@
     }
 
     @Test
-    public void testOnAttachedToWindow_addAccessibilityStateChangeListener() {
+    public void onAttachedToWindow_addAccessibilityStateChangeListener() {
         mBatteryChartView.onAttachedToWindow();
         verify(mMockAccessibilityManager)
                 .addAccessibilityStateChangeListener(mBatteryChartView);
     }
 
     @Test
-    public void testOnDetachedFromWindow_removeAccessibilityStateChangeListener() {
+    public void onDetachedFromWindow_removeAccessibilityStateChangeListener() {
         mBatteryChartView.onAttachedToWindow();
         mBatteryChartView.mHandler.postDelayed(
                 mBatteryChartView.mUpdateClickableStateRun, 1000);
@@ -223,7 +233,7 @@
     }
 
     @Test
-    public void testOnAccessibilityStateChanged_postUpdateStateRunnable() {
+    public void onAccessibilityStateChanged_postUpdateStateRunnable() {
         mBatteryChartView.mHandler = spy(mBatteryChartView.mHandler);
         mBatteryChartView.onAccessibilityStateChanged(/*enabled=*/ true);
 
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryHistoryLoaderTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryHistoryLoaderTest.java
index 98a44de..5717857 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryHistoryLoaderTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryHistoryLoaderTest.java
@@ -53,7 +53,7 @@
     public void testLoadIBackground_returnsMapFromPowerFeatureProvider() {
         final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap = new HashMap<>();
         doReturn(batteryHistoryMap).when(mFeatureFactory.powerUsageFeatureProvider)
-                .getBatteryHistory(mContext);
+                .getBatteryHistorySinceLastFullCharge(mContext);
 
         assertThat(mBatteryHistoryLoader.loadInBackground())
                 .isSameInstanceAs(batteryHistoryMap);
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/ConvertUtilsTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/ConvertUtilsTest.java
index c1f9815..c9bac03 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/ConvertUtilsTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/ConvertUtilsTest.java
@@ -26,6 +26,7 @@
 import android.os.BatteryUsageStats;
 import android.os.LocaleList;
 import android.os.UserHandle;
+import android.text.format.DateUtils;
 
 import com.android.settings.fuelgauge.BatteryUtils;
 import com.android.settings.fuelgauge.PowerUsageFeatureProvider;
@@ -39,8 +40,8 @@
 import org.robolectric.RobolectricTestRunner;
 import org.robolectric.RuntimeEnvironment;
 
-import java.util.Arrays;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
@@ -173,7 +174,8 @@
     public void getIndexedUsageMap_returnsExpectedResult() {
         // Creates the fake testing data.
         final int timeSlotSize = 2;
-        final long[] batteryHistoryKeys = new long[]{101L, 102L, 103L, 104L, 105L};
+        final long[] batteryHistoryKeys = new long[]{generateTimestamp(0), generateTimestamp(1),
+                generateTimestamp(2), generateTimestamp(3), generateTimestamp(4)};
         final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap =
                 new HashMap<>();
         final BatteryHistEntry fakeEntry = createBatteryHistEntry(
@@ -270,11 +272,11 @@
         for (int index = 0; index < remainingSize; index++) {
             batteryHistoryMap.put(105L + index + 1, new HashMap<>());
         }
-        when(mPowerUsageFeatureProvider.getBatteryHistory(mContext))
+        when(mPowerUsageFeatureProvider.getBatteryHistorySinceLastFullCharge(mContext))
                 .thenReturn(batteryHistoryMap);
 
         final List<BatteryDiffEntry> batteryDiffEntryList =
-                BatteryChartPreferenceController.getBatteryLast24HrUsageData(mContext);
+                BatteryChartPreferenceController.getAppBatteryUsageData(mContext);
 
         assertThat(batteryDiffEntryList).isNotEmpty();
         final BatteryDiffEntry resultEntry = batteryDiffEntryList.get(0);
@@ -472,4 +474,9 @@
         assertThat(entry.mForegroundUsageTimeInMs).isEqualTo(foregroundUsageTimeInMs);
         assertThat(entry.mBackgroundUsageTimeInMs).isEqualTo(backgroundUsageTimeInMs);
     }
+
+    private static Long generateTimestamp(int index) {
+        // "2021-04-23 07:00:00 UTC" + index hours
+        return 1619247600000L + index * DateUtils.HOUR_IN_MILLIS;
+    }
 }
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DataProcessorTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DataProcessorTest.java
new file mode 100644
index 0000000..883b0e7
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DataProcessorTest.java
@@ -0,0 +1,950 @@
+/*
+ * 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.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.text.format.DateUtils;
+
+import com.android.settings.fuelgauge.BatteryUtils;
+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.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.TimeZone;
+
+@RunWith(RobolectricTestRunner.class)
+public class DataProcessorTest {
+    private static final String FAKE_ENTRY_KEY = "fake_entry_key";
+
+    private Context mContext;
+
+    private FakeFeatureFactory mFeatureFactory;
+    private PowerUsageFeatureProvider mPowerUsageFeatureProvider;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        TimeZone.setDefault(TimeZone.getTimeZone("GMT+8"));
+
+        mContext = spy(RuntimeEnvironment.application);
+        mFeatureFactory = FakeFeatureFactory.setupForTest();
+        mPowerUsageFeatureProvider = mFeatureFactory.powerUsageFeatureProvider;
+    }
+
+    @Test
+    public void getBatteryLevelData_emptyHistoryMap_returnNull() {
+        assertThat(DataProcessor.getBatteryLevelData(
+                mContext,
+                /*handler=*/ null,
+                /*batteryHistoryMap=*/ null,
+                /*asyncResponseDelegate=*/ null))
+                .isNull();
+        assertThat(DataProcessor.getBatteryLevelData(
+                mContext, /*handler=*/ null, new HashMap<>(), /*asyncResponseDelegate=*/ null))
+                .isNull();
+    }
+
+    @Test
+    public void getBatteryLevelData_notEnoughData_returnNull() {
+        // The timestamps are within 1 hour.
+        final long[] timestamps = {1000000L, 2000000L, 3000000L};
+        final int[] levels = {100, 99, 98};
+        final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap =
+                createHistoryMap(timestamps, levels);
+
+        assertThat(DataProcessor.getBatteryLevelData(
+                mContext, /*handler=*/ null, batteryHistoryMap, /*asyncResponseDelegate=*/ null))
+                .isNull();
+    }
+
+    @Test
+    public void getBatteryLevelData_returnExpectedResult() {
+        // Timezone GMT+8: 2022-01-01 00:00:00, 2022-01-01 01:00:00, 2022-01-01 02:00:00
+        final long[] timestamps = {1640966400000L, 1640970000000L, 1640973600000L};
+        final int[] levels = {100, 99, 98};
+        final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap =
+                createHistoryMap(timestamps, levels);
+
+        final BatteryLevelData resultData =
+                DataProcessor.getBatteryLevelData(
+                        mContext,
+                        /*handler=*/ null,
+                        batteryHistoryMap,
+                        /*asyncResponseDelegate=*/ null);
+
+        final List<Long> expectedDailyTimestamps = List.of(timestamps[0], timestamps[2]);
+        final List<Integer> expectedDailyLevels = List.of(levels[0], levels[2]);
+        final List<List<Long>> expectedHourlyTimestamps = List.of(expectedDailyTimestamps);
+        final List<List<Integer>> expectedHourlyLevels = List.of(expectedDailyLevels);
+        verifyExpectedBatteryLevelData(
+                resultData,
+                expectedDailyTimestamps,
+                expectedDailyLevels,
+                expectedHourlyTimestamps,
+                expectedHourlyLevels);
+    }
+
+    @Test
+    public void getHistoryMapWithExpectedTimestamps_emptyHistoryMap_returnEmptyMap() {
+        assertThat(DataProcessor
+                .getHistoryMapWithExpectedTimestamps(mContext, new HashMap<>()))
+                .isEmpty();
+    }
+
+    @Test
+    public void getHistoryMapWithExpectedTimestamps_returnExpectedMap() {
+        // Timezone GMT+8
+        final long[] timestamps = {
+                1640966700000L, // 2022-01-01 00:05:00
+                1640970180000L, // 2022-01-01 01:03:00
+                1640973840000L, // 2022-01-01 02:04:00
+                1640978100000L, // 2022-01-01 03:15:00
+                1640981400000L  // 2022-01-01 04:10:00
+        };
+        final int[] levels = {100, 94, 90, 82, 50};
+        final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap =
+                createHistoryMap(timestamps, levels);
+
+        final Map<Long, Map<String, BatteryHistEntry>> resultMap =
+                DataProcessor.getHistoryMapWithExpectedTimestamps(mContext, batteryHistoryMap);
+
+        // Timezone GMT+8
+        final long[] expectedTimestamps = {
+                1640966400000L, // 2022-01-01 00:00:00
+                1640970000000L, // 2022-01-01 01:00:00
+                1640973600000L, // 2022-01-01 02:00:00
+                1640977200000L, // 2022-01-01 03:00:00
+                1640980800000L  // 2022-01-01 04:00:00
+        };
+        final int[] expectedLevels = {100, 94, 90, 84, 56};
+        assertThat(resultMap).hasSize(expectedLevels.length);
+        for (int index = 0; index < expectedLevels.length; index++) {
+            assertThat(resultMap.get(expectedTimestamps[index]).get(FAKE_ENTRY_KEY).mBatteryLevel)
+                    .isEqualTo(expectedLevels[index]);
+        }
+    }
+
+    @Test
+    public void getLevelDataThroughProcessedHistoryMap_notEnoughData_returnNull() {
+        final long[] timestamps = {100L};
+        final int[] levels = {100};
+        final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap =
+                createHistoryMap(timestamps, levels);
+
+        assertThat(
+                DataProcessor.getLevelDataThroughProcessedHistoryMap(mContext, batteryHistoryMap))
+                .isNull();
+    }
+
+    @Test
+    public void getLevelDataThroughProcessedHistoryMap_OneDayData_returnExpectedResult() {
+        // Timezone GMT+8
+        final long[] timestamps = {
+                1640966400000L, // 2022-01-01 00:00:00
+                1640970000000L, // 2022-01-01 01:00:00
+                1640973600000L, // 2022-01-01 02:00:00
+                1640977200000L, // 2022-01-01 03:00:00
+                1640980800000L  // 2022-01-01 04:00:00
+        };
+        final int[] levels = {100, 94, 90, 82, 50};
+        final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap =
+                createHistoryMap(timestamps, levels);
+
+        final BatteryLevelData resultData =
+                DataProcessor.getLevelDataThroughProcessedHistoryMap(mContext, batteryHistoryMap);
+
+        final List<Long> expectedDailyTimestamps = List.of(timestamps[0], timestamps[4]);
+        final List<Integer> expectedDailyLevels = List.of(levels[0], levels[4]);
+        final List<List<Long>> expectedHourlyTimestamps = List.of(
+                List.of(timestamps[0], timestamps[2], timestamps[4])
+        );
+        final List<List<Integer>> expectedHourlyLevels = List.of(
+                List.of(levels[0], levels[2], levels[4])
+        );
+        verifyExpectedBatteryLevelData(
+                resultData,
+                expectedDailyTimestamps,
+                expectedDailyLevels,
+                expectedHourlyTimestamps,
+                expectedHourlyLevels);
+    }
+
+    @Test
+    public void getLevelDataThroughProcessedHistoryMap_MultipleDaysData_returnExpectedResult() {
+        // Timezone GMT+8
+        final long[] timestamps = {
+                1641038400000L, // 2022-01-01 20:00:00
+                1641060000000L, // 2022-01-02 02:00:00
+                1641067200000L, // 2022-01-02 04:00:00
+                1641081600000L, // 2022-01-02 08:00:00
+        };
+        final int[] levels = {100, 94, 90, 82};
+        final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap =
+                createHistoryMap(timestamps, levels);
+
+        final BatteryLevelData resultData =
+                DataProcessor.getLevelDataThroughProcessedHistoryMap(mContext, batteryHistoryMap);
+
+        final List<Long> expectedDailyTimestamps = List.of(
+                1641038400000L, // 2022-01-01 20:00:00
+                1641052800000L, // 2022-01-02 00:00:00
+                1641081600000L  // 2022-01-02 08:00:00
+        );
+        final List<Integer> expectedDailyLevels = new ArrayList<>();
+        expectedDailyLevels.add(100);
+        expectedDailyLevels.add(null);
+        expectedDailyLevels.add(82);
+        final List<List<Long>> expectedHourlyTimestamps = List.of(
+                List.of(
+                        1641038400000L, // 2022-01-01 20:00:00
+                        1641045600000L, // 2022-01-01 22:00:00
+                        1641052800000L  // 2022-01-02 00:00:00
+                ),
+                List.of(
+                        1641052800000L, // 2022-01-02 00:00:00
+                        1641060000000L, // 2022-01-02 02:00:00
+                        1641067200000L, // 2022-01-02 04:00:00
+                        1641074400000L, // 2022-01-02 06:00:00
+                        1641081600000L  // 2022-01-02 08:00:00
+                )
+        );
+        final List<Integer> expectedHourlyLevels1 = new ArrayList<>();
+        expectedHourlyLevels1.add(100);
+        expectedHourlyLevels1.add(null);
+        expectedHourlyLevels1.add(null);
+        final List<Integer> expectedHourlyLevels2 = new ArrayList<>();
+        expectedHourlyLevels2.add(null);
+        expectedHourlyLevels2.add(94);
+        expectedHourlyLevels2.add(90);
+        expectedHourlyLevels2.add(null);
+        expectedHourlyLevels2.add(82);
+        final List<List<Integer>> expectedHourlyLevels = List.of(
+                expectedHourlyLevels1,
+                expectedHourlyLevels2
+        );
+        verifyExpectedBatteryLevelData(
+                resultData,
+                expectedDailyTimestamps,
+                expectedDailyLevels,
+                expectedHourlyTimestamps,
+                expectedHourlyLevels);
+    }
+
+    @Test
+    public void getTimestampSlots_emptyRawList_returnEmptyList() {
+        final List<Long> resultList =
+                DataProcessor.getTimestampSlots(new ArrayList<>());
+        assertThat(resultList).isEmpty();
+    }
+
+    @Test
+    public void getTimestampSlots_startWithEvenHour_returnExpectedResult() {
+        final Calendar startCalendar = Calendar.getInstance();
+        startCalendar.set(2022, 6, 5, 6, 30, 50); // 2022-07-05 06:30:50
+        final Calendar endCalendar = Calendar.getInstance();
+        endCalendar.set(2022, 6, 5, 22, 30, 50); // 2022-07-05 22:30:50
+
+        final Calendar expectedStartCalendar = Calendar.getInstance();
+        expectedStartCalendar.set(2022, 6, 5, 6, 0, 0); // 2022-07-05 06:00:00
+        final Calendar expectedEndCalendar = Calendar.getInstance();
+        expectedEndCalendar.set(2022, 6, 5, 22, 0, 0); // 2022-07-05 22:00:00
+        verifyExpectedTimestampSlots(
+                startCalendar, endCalendar, expectedStartCalendar, expectedEndCalendar);
+    }
+
+    @Test
+    public void getTimestampSlots_startWithOddHour_returnExpectedResult() {
+        final Calendar startCalendar = Calendar.getInstance();
+        startCalendar.set(2022, 6, 5, 5, 0, 50); // 2022-07-05 05:00:50
+        final Calendar endCalendar = Calendar.getInstance();
+        endCalendar.set(2022, 6, 6, 21, 00, 50); // 2022-07-06 21:00:50
+
+        final Calendar expectedStartCalendar = Calendar.getInstance();
+        expectedStartCalendar.set(2022, 6, 5, 6, 00, 00); // 2022-07-05 06:00:00
+        final Calendar expectedEndCalendar = Calendar.getInstance();
+        expectedEndCalendar.set(2022, 6, 6, 20, 00, 00); // 2022-07-06 20:00:00
+        verifyExpectedTimestampSlots(
+                startCalendar, endCalendar, expectedStartCalendar, expectedEndCalendar);
+    }
+
+    @Test
+    public void getDailyTimestamps_notEnoughData_returnEmptyList() {
+        assertThat(DataProcessor.getDailyTimestamps(new ArrayList<>())).isEmpty();
+        assertThat(DataProcessor.getDailyTimestamps(List.of(100L))).isEmpty();
+    }
+
+    @Test
+    public void getDailyTimestamps_OneDayData_returnExpectedList() {
+        // Timezone GMT+8
+        final List<Long> timestamps = List.of(
+                1640966400000L, // 2022-01-01 00:00:00
+                1640970000000L, // 2022-01-01 01:00:00
+                1640973600000L, // 2022-01-01 02:00:00
+                1640977200000L, // 2022-01-01 03:00:00
+                1640980800000L  // 2022-01-01 04:00:00
+        );
+
+        final List<Long> expectedTimestamps = List.of(
+                1640966400000L, // 2022-01-01 00:00:00
+                1640980800000L  // 2022-01-01 04:00:00
+        );
+        assertThat(DataProcessor.getDailyTimestamps(timestamps)).isEqualTo(expectedTimestamps);
+    }
+
+    @Test
+    public void getDailyTimestamps_MultipleDaysData_returnExpectedList() {
+        // Timezone GMT+8
+        final List<Long> timestamps = List.of(
+                1640988000000L, // 2022-01-01 06:00:00
+                1641060000000L, // 2022-01-02 02:00:00
+                1641160800000L, // 2022-01-03 06:00:00
+                1641254400000L  // 2022-01-04 08:00:00
+        );
+
+        final List<Long> expectedTimestamps = List.of(
+                1640988000000L, // 2022-01-01 06:00:00
+                1641052800000L, // 2022-01-02 00:00:00
+                1641139200000L, // 2022-01-03 00:00:00
+                1641225600000L, // 2022-01-04 00:00:00
+                1641254400000L  // 2022-01-04 08:00:00
+        );
+        assertThat(DataProcessor.getDailyTimestamps(timestamps)).isEqualTo(expectedTimestamps);
+    }
+
+    @Test
+    public void isFromFullCharge_emptyData_returnFalse() {
+        assertThat(DataProcessor.isFromFullCharge(null)).isFalse();
+        assertThat(DataProcessor.isFromFullCharge(new HashMap<>())).isFalse();
+    }
+
+    @Test
+    public void isFromFullCharge_notChargedData_returnFalse() {
+        final Map<String, BatteryHistEntry> entryMap = new HashMap<>();
+        final ContentValues values = new ContentValues();
+        values.put("batteryLevel", 98);
+        final BatteryHistEntry entry = new BatteryHistEntry(values);
+        entryMap.put(FAKE_ENTRY_KEY, entry);
+
+        assertThat(DataProcessor.isFromFullCharge(entryMap)).isFalse();
+    }
+
+    @Test
+    public void isFromFullCharge_chargedData_returnTrue() {
+        final Map<String, BatteryHistEntry> entryMap = new HashMap<>();
+        final ContentValues values = new ContentValues();
+        values.put("batteryLevel", 100);
+        final BatteryHistEntry entry = new BatteryHistEntry(values);
+        entryMap.put(FAKE_ENTRY_KEY, entry);
+
+        assertThat(DataProcessor.isFromFullCharge(entryMap)).isTrue();
+    }
+
+    @Test
+    public void findNearestTimestamp_returnExpectedResult() {
+        long[] results = DataProcessor.findNearestTimestamp(
+                Arrays.asList(10L, 20L, 30L, 40L), /*target=*/ 15L);
+        assertThat(results).isEqualTo(new long[] {10L, 20L});
+
+        results = DataProcessor.findNearestTimestamp(
+                Arrays.asList(10L, 20L, 30L, 40L), /*target=*/ 10L);
+        assertThat(results).isEqualTo(new long[] {10L, 10L});
+
+        results = DataProcessor.findNearestTimestamp(
+                Arrays.asList(10L, 20L, 30L, 40L), /*target=*/ 5L);
+        assertThat(results).isEqualTo(new long[] {0L, 10L});
+
+        results = DataProcessor.findNearestTimestamp(
+                Arrays.asList(10L, 20L, 30L, 40L), /*target=*/ 50L);
+        assertThat(results).isEqualTo(new long[] {40L, 0L});
+    }
+
+    @Test
+    public void getTimestampOfNextDay_returnExpectedResult() {
+        // 2021-02-28 06:00:00 => 2021-03-01 00:00:00
+        assertThat(DataProcessor.getTimestampOfNextDay(1614463200000L))
+                .isEqualTo(1614528000000L);
+        // 2021-12-31 16:00:00 => 2022-01-01 00:00:00
+        assertThat(DataProcessor.getTimestampOfNextDay(1640937600000L))
+                .isEqualTo(1640966400000L);
+    }
+
+    @Test
+    public void isForDailyChart_returnExpectedResult() {
+        assertThat(DataProcessor.isForDailyChart(/*isStartOrEnd=*/ true, 0L)).isTrue();
+        // 2022-01-01 00:00:00
+        assertThat(DataProcessor.isForDailyChart(/*isStartOrEnd=*/ false, 1640966400000L))
+                .isTrue();
+        // 2022-01-01 01:00:05
+        assertThat(DataProcessor.isForDailyChart(/*isStartOrEnd=*/ false, 1640970005000L))
+                .isFalse();
+    }
+
+    @Test
+    public void getBatteryUsageMap_emptyHistoryMap_returnNull() {
+        final List<BatteryLevelData.PeriodBatteryLevelData> hourlyBatteryLevelsPerDay =
+                new ArrayList<>();
+        hourlyBatteryLevelsPerDay.add(
+                new BatteryLevelData.PeriodBatteryLevelData(new ArrayList<>(), new ArrayList<>()));
+
+        assertThat(DataProcessor.getBatteryUsageMap(
+                mContext, hourlyBatteryLevelsPerDay, new HashMap<>())).isNull();
+    }
+
+    @Test
+    public void getBatteryUsageMap_returnsExpectedResult() {
+        final long[] batteryHistoryKeys = new long[]{
+                1641045600000L, // 2022-01-01 22:00:00
+                1641049200000L, // 2022-01-01 23:00:00
+                1641052800000L, // 2022-01-02 00:00:00
+                1641056400000L, // 2022-01-02 01:00:00
+                1641060000000L, // 2022-01-02 02:00:00
+        };
+        final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap = new HashMap<>();
+        final int currentUserId = mContext.getUserId();
+        final BatteryHistEntry fakeEntry = createBatteryHistEntry(
+                ConvertUtils.FAKE_PACKAGE_NAME, "fake_label", /*consumePower=*/ 0, /*uid=*/ 0L,
+                currentUserId, ConvertUtils.CONSUMER_TYPE_UID_BATTERY,
+                /*foregroundUsageTimeInMs=*/ 0L, /*backgroundUsageTimeInMs=*/ 0L);
+        // Adds the index = 0 data.
+        Map<String, BatteryHistEntry> entryMap = new HashMap<>();
+        BatteryHistEntry entry = createBatteryHistEntry(
+                "package1", "label1", /*consumePower=*/ 5.0, /*uid=*/ 1L, currentUserId,
+                ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 10L,
+                /*backgroundUsageTimeInMs=*/ 20L);
+        entryMap.put(entry.getKey(), entry);
+        entryMap.put(fakeEntry.getKey(), fakeEntry);
+        batteryHistoryMap.put(batteryHistoryKeys[0], entryMap);
+        // Adds the index = 1 data.
+        entryMap = new HashMap<>();
+        entryMap.put(fakeEntry.getKey(), fakeEntry);
+        batteryHistoryMap.put(batteryHistoryKeys[1], entryMap);
+        // Adds the index = 2 data.
+        entryMap = new HashMap<>();
+        entry = createBatteryHistEntry(
+                "package2", "label2", /*consumePower=*/ 20.0, /*uid=*/ 2L, currentUserId,
+                ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 15L,
+                25L);
+        entryMap.put(entry.getKey(), entry);
+        entryMap.put(fakeEntry.getKey(), fakeEntry);
+        batteryHistoryMap.put(batteryHistoryKeys[2], entryMap);
+        // Adds the index = 3 data.
+        entryMap = new HashMap<>();
+        entry = createBatteryHistEntry(
+                "package2", "label2", /*consumePower=*/ 40.0, /*uid=*/ 2L, currentUserId,
+                ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 25L,
+                /*backgroundUsageTimeInMs=*/ 35L);
+        entryMap.put(entry.getKey(), entry);
+        entry = createBatteryHistEntry(
+                "package2", "label2", /*consumePower=*/ 10.0, /*uid=*/ 3L, currentUserId,
+                ConvertUtils.CONSUMER_TYPE_SYSTEM_BATTERY, /*foregroundUsageTimeInMs=*/ 40L,
+                /*backgroundUsageTimeInMs=*/ 50L);
+        entryMap.put(entry.getKey(), entry);
+        entry = createBatteryHistEntry(
+                "package3", "label3", /*consumePower=*/ 15.0, /*uid=*/ 4L, currentUserId,
+                ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 5L,
+                /*backgroundUsageTimeInMs=*/ 5L);
+        entryMap.put(entry.getKey(), entry);
+        entryMap.put(fakeEntry.getKey(), fakeEntry);
+        batteryHistoryMap.put(batteryHistoryKeys[3], entryMap);
+        // Adds the index = 4 data.
+        entryMap = new HashMap<>();
+        entry = createBatteryHistEntry(
+                "package2", "label2", /*consumePower=*/ 40.0, /*uid=*/ 2L, currentUserId,
+                ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 30L,
+                /*backgroundUsageTimeInMs=*/ 40L);
+        entryMap.put(entry.getKey(), entry);
+        entry = createBatteryHistEntry(
+                "package2", "label2", /*consumePower=*/ 20.0, /*uid=*/ 3L, currentUserId,
+                ConvertUtils.CONSUMER_TYPE_SYSTEM_BATTERY, /*foregroundUsageTimeInMs=*/ 50L,
+                /*backgroundUsageTimeInMs=*/ 60L);
+        entryMap.put(entry.getKey(), entry);
+        entry = createBatteryHistEntry(
+                "package3", "label3", /*consumePower=*/ 40.0, /*uid=*/ 4L, currentUserId,
+                ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 5L,
+                /*backgroundUsageTimeInMs=*/ 5L);
+        entryMap.put(entry.getKey(), entry);
+        entryMap.put(fakeEntry.getKey(), fakeEntry);
+        batteryHistoryMap.put(batteryHistoryKeys[4], entryMap);
+        final List<BatteryLevelData.PeriodBatteryLevelData> hourlyBatteryLevelsPerDay =
+                new ArrayList<>();
+        // Adds the day 1 data.
+        List<Long> timestamps =
+                List.of(batteryHistoryKeys[0], batteryHistoryKeys[2]);
+        final List<Integer> levels = List.of(100, 100);
+        hourlyBatteryLevelsPerDay.add(
+                new BatteryLevelData.PeriodBatteryLevelData(timestamps, levels));
+        // Adds the day 2 data.
+        timestamps = List.of(batteryHistoryKeys[2], batteryHistoryKeys[4]);
+        hourlyBatteryLevelsPerDay.add(
+                new BatteryLevelData.PeriodBatteryLevelData(timestamps, levels));
+
+        final Map<Integer, Map<Integer, BatteryDiffData>> resultMap =
+                DataProcessor.getBatteryUsageMap(
+                        mContext, hourlyBatteryLevelsPerDay, batteryHistoryMap);
+
+        BatteryDiffData resultDiffData =
+                resultMap
+                        .get(DataProcessor.SELECTED_INDEX_ALL)
+                        .get(DataProcessor.SELECTED_INDEX_ALL);
+        assertBatteryDiffEntry(
+                resultDiffData.getAppDiffEntryList().get(0), currentUserId, /*uid=*/ 2L,
+                ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*consumePercentage=*/ 40.0,
+                /*foregroundUsageTimeInMs=*/ 30, /*backgroundUsageTimeInMs=*/ 40);
+        assertBatteryDiffEntry(
+                resultDiffData.getAppDiffEntryList().get(1), currentUserId, /*uid=*/ 4L,
+                ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*consumePercentage=*/ 40.0,
+                /*foregroundUsageTimeInMs=*/ 5, /*backgroundUsageTimeInMs=*/ 5);
+        assertBatteryDiffEntry(
+                resultDiffData.getSystemDiffEntryList().get(0), currentUserId, /*uid=*/ 3L,
+                ConvertUtils.CONSUMER_TYPE_SYSTEM_BATTERY, /*consumePercentage=*/ 20.0,
+                /*foregroundUsageTimeInMs=*/ 50, /*backgroundUsageTimeInMs=*/ 60);
+        resultDiffData = resultMap.get(0).get(DataProcessor.SELECTED_INDEX_ALL);
+        assertBatteryDiffEntry(
+                resultDiffData.getAppDiffEntryList().get(0), currentUserId, /*uid=*/ 2L,
+                ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*consumePercentage=*/ 100.0,
+                /*foregroundUsageTimeInMs=*/ 15, /*backgroundUsageTimeInMs=*/ 25);
+        resultDiffData = resultMap.get(1).get(DataProcessor.SELECTED_INDEX_ALL);
+        assertBatteryDiffEntry(
+                resultDiffData.getAppDiffEntryList().get(0), currentUserId, /*uid=*/ 4L,
+                ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*consumePercentage=*/ 50.0,
+                /*foregroundUsageTimeInMs=*/ 5, /*backgroundUsageTimeInMs=*/ 5);
+        assertBatteryDiffEntry(
+                resultDiffData.getAppDiffEntryList().get(1), currentUserId, /*uid=*/ 2L,
+                ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*consumePercentage=*/ 25.0,
+                /*foregroundUsageTimeInMs=*/ 15, /*backgroundUsageTimeInMs=*/ 15);
+        assertBatteryDiffEntry(
+                resultDiffData.getSystemDiffEntryList().get(0), currentUserId, /*uid=*/ 3L,
+                ConvertUtils.CONSUMER_TYPE_SYSTEM_BATTERY, /*consumePercentage=*/ 25.0,
+                /*foregroundUsageTimeInMs=*/ 50, /*backgroundUsageTimeInMs=*/ 60);
+    }
+
+    @Test
+    public void getBatteryUsageMap_multipleUsers_returnsExpectedResult() {
+        final long[] batteryHistoryKeys = new long[]{
+                1641052800000L, // 2022-01-02 00:00:00
+                1641056400000L, // 2022-01-02 01:00:00
+                1641060000000L  // 2022-01-02 02:00:00
+        };
+        final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap = new HashMap<>();
+        final int currentUserId = mContext.getUserId();
+        // Adds the index = 0 data.
+        Map<String, BatteryHistEntry> entryMap = new HashMap<>();
+        BatteryHistEntry entry = createBatteryHistEntry(
+                "package1", "label1", /*consumePower=*/ 5.0, /*uid=*/ 1L, currentUserId,
+                ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 10L,
+                /*backgroundUsageTimeInMs=*/ 20L);
+        entryMap.put(entry.getKey(), entry);
+        entry = createBatteryHistEntry(
+                "package1", "label1", /*consumePower=*/ 10.0, /*uid=*/ 2L, currentUserId + 1,
+                ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 10L,
+                /*backgroundUsageTimeInMs=*/ 20L);
+        entryMap.put(entry.getKey(), entry);
+        entry = createBatteryHistEntry(
+                "package2", "label2", /*consumePower=*/ 5.0, /*uid=*/ 3L, currentUserId + 2,
+                ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 20L,
+                /*backgroundUsageTimeInMs=*/ 30L);
+        entryMap.put(entry.getKey(), entry);
+        batteryHistoryMap.put(batteryHistoryKeys[0], entryMap);
+        // Adds the index = 1 data.
+        entryMap = new HashMap<>();
+        entry = createBatteryHistEntry(
+                "package1", "label1", /*consumePower=*/ 15.0, /*uid=*/ 1L, currentUserId,
+                ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 20L,
+                /*backgroundUsageTimeInMs=*/ 30L);
+        entryMap.put(entry.getKey(), entry);
+        entry = createBatteryHistEntry(
+                "package1", "label1", /*consumePower=*/ 30.0, /*uid=*/ 2L, currentUserId + 1,
+                ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 10L,
+                /*backgroundUsageTimeInMs=*/ 20L);
+        entryMap.put(entry.getKey(), entry);
+        entry = createBatteryHistEntry(
+                "package2", "label2", /*consumePower=*/ 15.0, /*uid=*/ 3L, currentUserId + 2,
+                ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 30L,
+                /*backgroundUsageTimeInMs=*/ 30L);
+        entryMap.put(entry.getKey(), entry);
+        batteryHistoryMap.put(batteryHistoryKeys[1], entryMap);
+        // Adds the index = 2 data.
+        entryMap = new HashMap<>();
+        entry = createBatteryHistEntry(
+                "package1", "label1", /*consumePower=*/ 25.0, /*uid=*/ 1L, currentUserId,
+                ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 20L,
+                /*backgroundUsageTimeInMs=*/ 30L);
+        entryMap.put(entry.getKey(), entry);
+        entry = createBatteryHistEntry(
+                "package1", "label1", /*consumePower=*/ 50.0, /*uid=*/ 2L, currentUserId + 1,
+                ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 20L,
+                /*backgroundUsageTimeInMs=*/ 20L);
+        entryMap.put(entry.getKey(), entry);
+        entry = createBatteryHistEntry(
+                "package2", "label2", /*consumePower=*/ 25.0, /*uid=*/ 3L, currentUserId + 2,
+                ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 30L,
+                /*backgroundUsageTimeInMs=*/ 30L);
+        entryMap.put(entry.getKey(), entry);
+        batteryHistoryMap.put(batteryHistoryKeys[2], entryMap);
+        final List<BatteryLevelData.PeriodBatteryLevelData> hourlyBatteryLevelsPerDay =
+                new ArrayList<>();
+        List<Long> timestamps = List.of(batteryHistoryKeys[0], batteryHistoryKeys[2]);
+        final List<Integer> levels = List.of(100, 100);
+        hourlyBatteryLevelsPerDay.add(
+                new BatteryLevelData.PeriodBatteryLevelData(timestamps, levels));
+
+        final Map<Integer, Map<Integer, BatteryDiffData>> resultMap =
+                DataProcessor.getBatteryUsageMap(
+                        mContext, hourlyBatteryLevelsPerDay, batteryHistoryMap);
+
+        final BatteryDiffData resultDiffData =
+                resultMap
+                        .get(DataProcessor.SELECTED_INDEX_ALL)
+                        .get(DataProcessor.SELECTED_INDEX_ALL);
+        assertBatteryDiffEntry(
+                resultDiffData.getAppDiffEntryList().get(0), currentUserId, /*uid=*/ 1L,
+                ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*consumePercentage=*/ 25.0,
+                /*foregroundUsageTimeInMs=*/ 10, /*backgroundUsageTimeInMs=*/ 10);
+        assertBatteryDiffEntry(
+                resultDiffData.getSystemDiffEntryList().get(0), BatteryUtils.UID_OTHER_USERS,
+                /*uid=*/ BatteryUtils.UID_OTHER_USERS, ConvertUtils.CONSUMER_TYPE_UID_BATTERY,
+                /*consumePercentage=*/ 75.0, /*foregroundUsageTimeInMs=*/ 0,
+                /*backgroundUsageTimeInMs=*/ 0);
+        assertThat(resultMap.get(0).get(0)).isNotNull();
+        assertThat(resultMap.get(0).get(DataProcessor.SELECTED_INDEX_ALL)).isNotNull();
+    }
+
+    @Test
+    public void getBatteryUsageMap_usageTimeExceed_returnsExpectedResult() {
+        final long[] batteryHistoryKeys = new long[]{
+                1641052800000L, // 2022-01-02 00:00:00
+                1641056400000L, // 2022-01-02 01:00:00
+                1641060000000L  // 2022-01-02 02:00:00
+        };
+        final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap = new HashMap<>();
+        final int currentUserId = mContext.getUserId();
+        // Adds the index = 0 data.
+        Map<String, BatteryHistEntry> entryMap = new HashMap<>();
+        BatteryHistEntry entry = createBatteryHistEntry(
+                "package1", "label1", /*consumePower=*/ 0, /*uid=*/ 1L, currentUserId,
+                ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 0L,
+                /*backgroundUsageTimeInMs=*/ 0L);
+        entryMap.put(entry.getKey(), entry);
+        batteryHistoryMap.put(batteryHistoryKeys[0], entryMap);
+        // Adds the index = 1 data.
+        entryMap = new HashMap<>();
+        entry = createBatteryHistEntry(
+                "package1", "label1", /*consumePower=*/ 0, /*uid=*/ 1L, currentUserId,
+                ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 0L,
+                /*backgroundUsageTimeInMs=*/ 0L);
+        entryMap.put(entry.getKey(), entry);
+        batteryHistoryMap.put(batteryHistoryKeys[1], entryMap);
+        // Adds the index = 2 data.
+        entryMap = new HashMap<>();
+        entry = createBatteryHistEntry(
+                "package1", "label1", /*consumePower=*/ 500.0, /*uid=*/ 1L, currentUserId,
+                ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 3600000L,
+                /*backgroundUsageTimeInMs=*/ 7200000L);
+        entryMap.put(entry.getKey(), entry);
+        batteryHistoryMap.put(batteryHistoryKeys[2], entryMap);
+        final List<BatteryLevelData.PeriodBatteryLevelData> hourlyBatteryLevelsPerDay =
+                new ArrayList<>();
+        List<Long> timestamps = List.of(batteryHistoryKeys[0], batteryHistoryKeys[2]);
+        final List<Integer> levels = List.of(100, 100);
+        hourlyBatteryLevelsPerDay.add(
+                new BatteryLevelData.PeriodBatteryLevelData(timestamps, levels));
+
+        final Map<Integer, Map<Integer, BatteryDiffData>> resultMap =
+                DataProcessor.getBatteryUsageMap(
+                        mContext, hourlyBatteryLevelsPerDay, batteryHistoryMap);
+
+        final BatteryDiffData resultDiffData =
+                resultMap
+                        .get(DataProcessor.SELECTED_INDEX_ALL)
+                        .get(DataProcessor.SELECTED_INDEX_ALL);
+        // Verifies the clipped usage time.
+        final float ratio = (float) (7200) / (float) (3600 + 7200);
+        final BatteryDiffEntry resultEntry = resultDiffData.getAppDiffEntryList().get(0);
+        assertThat(resultEntry.mForegroundUsageTimeInMs)
+                .isEqualTo(Math.round(entry.mForegroundUsageTimeInMs * ratio));
+        assertThat(resultEntry.mBackgroundUsageTimeInMs)
+                .isEqualTo(Math.round(entry.mBackgroundUsageTimeInMs * ratio));
+        assertThat(resultEntry.mConsumePower)
+                .isEqualTo(entry.mConsumePower * ratio);
+        assertThat(resultMap.get(0).get(0)).isNotNull();
+        assertThat(resultMap.get(0).get(DataProcessor.SELECTED_INDEX_ALL)).isNotNull();
+    }
+
+    @Test
+    public void getBatteryUsageMap_hideApplicationEntries_returnsExpectedResult() {
+        final long[] batteryHistoryKeys = new long[]{
+                1641052800000L, // 2022-01-02 00:00:00
+                1641056400000L, // 2022-01-02 01:00:00
+                1641060000000L  // 2022-01-02 02:00:00
+        };
+        final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap = new HashMap<>();
+        final int currentUserId = mContext.getUserId();
+        // Adds the index = 0 data.
+        Map<String, BatteryHistEntry> entryMap = new HashMap<>();
+        BatteryHistEntry entry = createBatteryHistEntry(
+                "package1", "label1", /*consumePower=*/ 0, /*uid=*/ 1L, currentUserId,
+                ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 0L,
+                /*backgroundUsageTimeInMs=*/ 0L);
+        entryMap.put(entry.getKey(), entry);
+        entry = createBatteryHistEntry(
+                "package2", "label2", /*consumePower=*/ 0, /*uid=*/ 2L, currentUserId,
+                ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 0L,
+                /*backgroundUsageTimeInMs=*/ 0L);
+        entryMap.put(entry.getKey(), entry);
+        batteryHistoryMap.put(batteryHistoryKeys[0], entryMap);
+        // Adds the index = 1 data.
+        entryMap = new HashMap<>();
+        entry = createBatteryHistEntry(
+                "package1", "label1", /*consumePower=*/ 0, /*uid=*/ 1L, currentUserId,
+                ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 0L,
+                /*backgroundUsageTimeInMs=*/ 0L);
+        entryMap.put(entry.getKey(), entry);
+        entry = createBatteryHistEntry(
+                "package2", "label2", /*consumePower=*/ 0, /*uid=*/ 2L, currentUserId,
+                ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 0L,
+                /*backgroundUsageTimeInMs=*/ 0L);
+        entryMap.put(entry.getKey(), entry);
+        batteryHistoryMap.put(batteryHistoryKeys[1], entryMap);
+        // Adds the index = 2 data.
+        entryMap = new HashMap<>();
+        entry = createBatteryHistEntry(
+                "package1", "label1", /*consumePower=*/ 10.0, /*uid=*/ 1L, currentUserId,
+                ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 10L,
+                /*backgroundUsageTimeInMs=*/ 20L);
+        entryMap.put(entry.getKey(), entry);
+        entry = createBatteryHistEntry(
+                "package2", "label2", /*consumePower=*/ 10.0, /*uid=*/ 2L, currentUserId,
+                ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 10L,
+                /*backgroundUsageTimeInMs=*/ 20L);
+        entryMap.put(entry.getKey(), entry);
+        batteryHistoryMap.put(batteryHistoryKeys[2], entryMap);
+        final List<BatteryLevelData.PeriodBatteryLevelData> hourlyBatteryLevelsPerDay =
+                new ArrayList<>();
+        List<Long> timestamps = List.of(batteryHistoryKeys[0], batteryHistoryKeys[2]);
+        final List<Integer> levels = List.of(100, 100);
+        hourlyBatteryLevelsPerDay.add(
+                new BatteryLevelData.PeriodBatteryLevelData(timestamps, levels));
+        when(mPowerUsageFeatureProvider.getHideApplicationEntries(mContext))
+                .thenReturn(new CharSequence[]{"package1"});
+
+        final Map<Integer, Map<Integer, BatteryDiffData>> resultMap =
+                DataProcessor.getBatteryUsageMap(
+                        mContext, hourlyBatteryLevelsPerDay, batteryHistoryMap);
+
+        final BatteryDiffData resultDiffData =
+                resultMap
+                        .get(DataProcessor.SELECTED_INDEX_ALL)
+                        .get(DataProcessor.SELECTED_INDEX_ALL);
+        assertBatteryDiffEntry(
+                resultDiffData.getAppDiffEntryList().get(0), currentUserId, /*uid=*/ 2L,
+                ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*consumePercentage=*/ 50.0,
+                /*foregroundUsageTimeInMs=*/ 10, /*backgroundUsageTimeInMs=*/ 20);
+    }
+
+    @Test
+    public void getBatteryUsageMap_hideBackgroundUsageTime_returnsExpectedResult() {
+        final long[] batteryHistoryKeys = new long[]{
+                1641052800000L, // 2022-01-02 00:00:00
+                1641056400000L, // 2022-01-02 01:00:00
+                1641060000000L  // 2022-01-02 02:00:00
+        };
+        final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap = new HashMap<>();
+        final int currentUserId = mContext.getUserId();
+        // Adds the index = 0 data.
+        Map<String, BatteryHistEntry> entryMap = new HashMap<>();
+        BatteryHistEntry entry = createBatteryHistEntry(
+                "package1", "label1", /*consumePower=*/ 0, /*uid=*/ 1L, currentUserId,
+                ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 0L,
+                /*backgroundUsageTimeInMs=*/ 0L);
+        entryMap.put(entry.getKey(), entry);
+        entry = createBatteryHistEntry(
+                "package2", "label2", /*consumePower=*/ 0, /*uid=*/ 2L, currentUserId,
+                ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 0L,
+                /*backgroundUsageTimeInMs=*/ 0L);
+        entryMap.put(entry.getKey(), entry);
+        batteryHistoryMap.put(batteryHistoryKeys[0], entryMap);
+        // Adds the index = 1 data.
+        entryMap = new HashMap<>();
+        entry = createBatteryHistEntry(
+                "package1", "label1", /*consumePower=*/ 0, /*uid=*/ 1L, currentUserId,
+                ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 0L,
+                /*backgroundUsageTimeInMs=*/ 0L);
+        entryMap.put(entry.getKey(), entry);
+        entry = createBatteryHistEntry(
+                "package2", "label2", /*consumePower=*/ 0, /*uid=*/ 2L, currentUserId,
+                ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 0L,
+                /*backgroundUsageTimeInMs=*/ 0L);
+        entryMap.put(entry.getKey(), entry);
+        batteryHistoryMap.put(batteryHistoryKeys[1], entryMap);
+        // Adds the index = 2 data.
+        entryMap = new HashMap<>();
+        entry = createBatteryHistEntry(
+                "package1", "label1", /*consumePower=*/ 10.0, /*uid=*/ 1L, currentUserId,
+                ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 10L,
+                /*backgroundUsageTimeInMs=*/ 20L);
+        entryMap.put(entry.getKey(), entry);
+        entry = createBatteryHistEntry(
+                "package2", "label2", /*consumePower=*/ 10.0, /*uid=*/ 2L, currentUserId,
+                ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 10L,
+                /*backgroundUsageTimeInMs=*/ 20L);
+        entryMap.put(entry.getKey(), entry);
+        batteryHistoryMap.put(batteryHistoryKeys[2], entryMap);
+        final List<BatteryLevelData.PeriodBatteryLevelData> hourlyBatteryLevelsPerDay =
+                new ArrayList<>();
+        List<Long> timestamps = List.of(batteryHistoryKeys[0], batteryHistoryKeys[2]);
+        final List<Integer> levels = List.of(100, 100);
+        hourlyBatteryLevelsPerDay.add(
+                new BatteryLevelData.PeriodBatteryLevelData(timestamps, levels));
+        when(mPowerUsageFeatureProvider.getHideBackgroundUsageTimeSet(mContext))
+                .thenReturn(new HashSet(Arrays.asList((CharSequence) "package2")));
+
+        final Map<Integer, Map<Integer, BatteryDiffData>> resultMap =
+                DataProcessor.getBatteryUsageMap(
+                        mContext, hourlyBatteryLevelsPerDay, batteryHistoryMap);
+
+        final BatteryDiffData resultDiffData =
+                resultMap
+                        .get(DataProcessor.SELECTED_INDEX_ALL)
+                        .get(DataProcessor.SELECTED_INDEX_ALL);
+        BatteryDiffEntry resultEntry = resultDiffData.getAppDiffEntryList().get(0);
+        assertThat(resultEntry.mBackgroundUsageTimeInMs).isEqualTo(20);
+        resultEntry = resultDiffData.getAppDiffEntryList().get(1);
+        assertThat(resultEntry.mBackgroundUsageTimeInMs).isEqualTo(0);
+    }
+
+    private static Map<Long, Map<String, BatteryHistEntry>> createHistoryMap(
+            final long[] timestamps, final int[] levels) {
+        final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap = new HashMap<>();
+        for (int index = 0; index < timestamps.length; index++) {
+            final Map<String, BatteryHistEntry> entryMap = new HashMap<>();
+            final ContentValues values = new ContentValues();
+            values.put(BatteryHistEntry.KEY_BATTERY_LEVEL, levels[index]);
+            final BatteryHistEntry entry = new BatteryHistEntry(values);
+            entryMap.put(FAKE_ENTRY_KEY, entry);
+            batteryHistoryMap.put(timestamps[index], entryMap);
+        }
+        return batteryHistoryMap;
+    }
+
+    private static BatteryHistEntry createBatteryHistEntry(
+            final String packageName, final String appLabel, final double consumePower,
+            final long uid, final long userId, final int consumerType,
+            final long foregroundUsageTimeInMs, final long backgroundUsageTimeInMs) {
+        // Only insert required fields.
+        final ContentValues values = new ContentValues();
+        values.put(BatteryHistEntry.KEY_PACKAGE_NAME, packageName);
+        values.put(BatteryHistEntry.KEY_APP_LABEL, appLabel);
+        values.put(BatteryHistEntry.KEY_UID, uid);
+        values.put(BatteryHistEntry.KEY_USER_ID, userId);
+        values.put(BatteryHistEntry.KEY_CONSUMER_TYPE, consumerType);
+        values.put(BatteryHistEntry.KEY_CONSUME_POWER, consumePower);
+        values.put(BatteryHistEntry.KEY_FOREGROUND_USAGE_TIME, foregroundUsageTimeInMs);
+        values.put(BatteryHistEntry.KEY_BACKGROUND_USAGE_TIME, backgroundUsageTimeInMs);
+        return new BatteryHistEntry(values);
+    }
+
+    private static void verifyExpectedBatteryLevelData(
+            final BatteryLevelData resultData,
+            final List<Long> expectedDailyTimestamps,
+            final List<Integer> expectedDailyLevels,
+            final List<List<Long>> expectedHourlyTimestamps,
+            final List<List<Integer>> expectedHourlyLevels) {
+        final BatteryLevelData.PeriodBatteryLevelData dailyResultData =
+                resultData.getDailyBatteryLevels();
+        final List<BatteryLevelData.PeriodBatteryLevelData> hourlyResultData =
+                resultData.getHourlyBatteryLevelsPerDay();
+        verifyExpectedDailyBatteryLevelData(
+                dailyResultData, expectedDailyTimestamps, expectedDailyLevels);
+        verifyExpectedHourlyBatteryLevelData(
+                hourlyResultData, expectedHourlyTimestamps, expectedHourlyLevels);
+    }
+
+    private static void verifyExpectedDailyBatteryLevelData(
+            final BatteryLevelData.PeriodBatteryLevelData dailyResultData,
+            final List<Long> expectedDailyTimestamps,
+            final List<Integer> expectedDailyLevels) {
+        assertThat(dailyResultData.getTimestamps()).isEqualTo(expectedDailyTimestamps);
+        assertThat(dailyResultData.getLevels()).isEqualTo(expectedDailyLevels);
+    }
+
+    private static void verifyExpectedHourlyBatteryLevelData(
+            final List<BatteryLevelData.PeriodBatteryLevelData> hourlyResultData,
+            final List<List<Long>> expectedHourlyTimestamps,
+            final List<List<Integer>> expectedHourlyLevels) {
+        final int expectedHourlySize = expectedHourlyTimestamps.size();
+        assertThat(hourlyResultData).hasSize(expectedHourlySize);
+        for (int dailyIndex = 0; dailyIndex < expectedHourlySize; dailyIndex++) {
+            assertThat(hourlyResultData.get(dailyIndex).getTimestamps())
+                    .isEqualTo(expectedHourlyTimestamps.get(dailyIndex));
+            assertThat(hourlyResultData.get(dailyIndex).getLevels())
+                    .isEqualTo(expectedHourlyLevels.get(dailyIndex));
+        }
+    }
+
+    private static void verifyExpectedTimestampSlots(
+            final Calendar start,
+            final Calendar end,
+            final Calendar expectedStart,
+            final Calendar expectedEnd) {
+        expectedStart.set(Calendar.MILLISECOND, 0);
+        expectedEnd.set(Calendar.MILLISECOND, 0);
+        final ArrayList<Long> timestampSlots = new ArrayList<>();
+        timestampSlots.add(start.getTimeInMillis());
+        timestampSlots.add(end.getTimeInMillis());
+        final List<Long> resultList =
+                DataProcessor.getTimestampSlots(timestampSlots);
+
+        for (int index = 0; index < resultList.size(); index++) {
+            final long expectedTimestamp =
+                    expectedStart.getTimeInMillis() + index * DateUtils.HOUR_IN_MILLIS;
+            assertThat(resultList.get(index)).isEqualTo(expectedTimestamp);
+        }
+        assertThat(resultList.get(resultList.size() - 1))
+                .isEqualTo(expectedEnd.getTimeInMillis());
+    }
+
+    private static void assertBatteryDiffEntry(
+            final BatteryDiffEntry entry, final long userId, final long uid,
+            final int consumerType, final double consumePercentage,
+            final long foregroundUsageTimeInMs, final long backgroundUsageTimeInMs) {
+        assertThat(entry.mBatteryHistEntry.mUserId).isEqualTo(userId);
+        assertThat(entry.mBatteryHistEntry.mUid).isEqualTo(uid);
+        assertThat(entry.mBatteryHistEntry.mConsumerType).isEqualTo(consumerType);
+        assertThat(entry.getPercentOfTotal()).isEqualTo(consumePercentage);
+        assertThat(entry.mForegroundUsageTimeInMs).isEqualTo(foregroundUsageTimeInMs);
+        assertThat(entry.mBackgroundUsageTimeInMs).isEqualTo(backgroundUsageTimeInMs);
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/PowerUsageSummaryTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/PowerUsageSummaryTest.java
index 81b574a..c049497 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/PowerUsageSummaryTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/PowerUsageSummaryTest.java
@@ -139,17 +139,7 @@
     }
 
     @Test
-    public void initPreference_chartGraphEnabled_hasCorrectSummary() {
-        mFragment.initPreference();
-
-        verify(mBatteryUsagePreference).setSummary("View usage for past 24 hours");
-    }
-
-    @Test
-    public void initPreference_chartGraphDisabled_hasCorrectSummary() {
-        when(mFeatureFactory.powerUsageFeatureProvider.isChartGraphEnabled(mRealContext))
-                .thenReturn(false);
-
+    public void initPreference_hasCorrectSummary() {
         mFragment.initPreference();
 
         verify(mBatteryUsagePreference).setSummary("View usage from last full charge");
diff --git a/tests/robotests/src/com/android/settings/widget/CardPreferenceTest.java b/tests/robotests/src/com/android/settings/widget/CardPreferenceTest.java
index 85ab609..eba447b 100644
--- a/tests/robotests/src/com/android/settings/widget/CardPreferenceTest.java
+++ b/tests/robotests/src/com/android/settings/widget/CardPreferenceTest.java
@@ -16,9 +16,18 @@
 
 package com.android.settings.widget;
 
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
+
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
 
 import android.content.Context;
+import android.view.View;
+import android.widget.Button;
+
+import androidx.preference.PreferenceViewHolder;
+import androidx.test.core.app.ApplicationProvider;
 
 import com.android.settings.R;
 
@@ -26,23 +35,301 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.robolectric.RobolectricTestRunner;
-import org.robolectric.RuntimeEnvironment;
 
 @RunWith(RobolectricTestRunner.class)
 public class CardPreferenceTest {
 
-    private Context mContext;
     private CardPreference mCardPreference;
+    private PreferenceViewHolder mHolder;
 
     @Before
     public void setUp() {
-        mContext = RuntimeEnvironment.application;
-        mContext.setTheme(R.style.SettingsPreferenceTheme);
-        mCardPreference = new CardPreference(mContext);
+        Context context = ApplicationProvider.getApplicationContext();
+        context.setTheme(R.style.Theme_Settings);
+        mCardPreference = new CardPreference(context);
+
+        View rootView = View.inflate(context, R.layout.card_preference_layout, /* parent= */ null);
+        mHolder = PreferenceViewHolder.createInstanceForTests(rootView);
     }
 
     @Test
-    public void getLayoutResource() {
-        assertThat(mCardPreference.getLayoutResource()).isEqualTo(R.layout.card_preference_layout);
+    public void newACardPreference_layoutResourceShouldBeCardPreferenceLayout() {
+        Context context = ApplicationProvider.getApplicationContext();
+        context.setTheme(R.style.SettingsPreferenceTheme);
+
+        CardPreference cardPreference = new CardPreference(context);
+
+        assertThat(cardPreference.getLayoutResource()).isEqualTo(R.layout.card_preference_layout);
+    }
+
+    @Test
+    public void onBindViewHolder_noButtonVisible_buttonsLayoutShouldBeGone() {
+        mCardPreference.onBindViewHolder(mHolder);
+
+        assertThat(getCardPreferenceButtonsView().getVisibility()).isEqualTo(GONE);
+    }
+
+    @Test
+    public void onBindViewHolder_setPrimaryButtonVisibility_buttonsLayoutShouldBeVisible() {
+        mCardPreference.setPrimaryButtonVisible(true);
+
+        mCardPreference.onBindViewHolder(mHolder);
+
+        assertThat(getCardPreferenceButtonsView().getVisibility()).isEqualTo(VISIBLE);
+    }
+
+    @Test
+    public void onBindViewHolder_setPrimaryButtonVisibility_shouldApplyToPrimaryButton() {
+        mCardPreference.setPrimaryButtonVisible(true);
+
+        mCardPreference.onBindViewHolder(mHolder);
+
+        assertThat(getPrimaryButton().getVisibility()).isEqualTo(VISIBLE);
+    }
+
+    @Test
+    public void onBindViewHolder_setSecondaryButtonVisibility_buttonsLayoutShouldBeVisible() {
+        mCardPreference.setSecondaryButtonVisible(true);
+
+        mCardPreference.onBindViewHolder(mHolder);
+
+        assertThat(getCardPreferenceButtonsView().getVisibility()).isEqualTo(VISIBLE);
+    }
+
+    @Test
+    public void onBindViewHolder_setSecondaryButtonVisibility_shouldApplyToSecondaryButton() {
+        mCardPreference.setSecondaryButtonVisible(true);
+
+        mCardPreference.onBindViewHolder(mHolder);
+
+        assertThat(getSecondaryButton().getVisibility()).isEqualTo(VISIBLE);
+    }
+
+    @Test
+    public void onBindViewHolder_setPrimaryButtonText_shouldApplyToPrimaryButton() {
+        String expectedText = "primary-button";
+        mCardPreference.setPrimaryButtonText(expectedText);
+
+        mCardPreference.onBindViewHolder(mHolder);
+
+        assertThat(getPrimaryButton().getText().toString()).isEqualTo(expectedText);
+    }
+
+    @Test
+    public void onBindViewHolder_setSecondaryButtonText_shouldApplyToSecondaryButton() {
+        String expectedText = "secondary-button";
+        mCardPreference.setSecondaryButtonText(expectedText);
+
+        mCardPreference.onBindViewHolder(mHolder);
+
+        assertThat(getSecondaryButton().getText().toString()).isEqualTo(expectedText);
+    }
+
+    @Test
+    public void onBindViewHolder_initialTextForPrimaryButtonShouldBeEmpty() {
+        mCardPreference.onBindViewHolder(mHolder);
+
+        assertThat(getPrimaryButton().getText().toString()).isEqualTo("");
+    }
+
+    @Test
+    public void onBindViewHolder_initialTextForSecondaryButtonShouldBeEmpty() {
+        mCardPreference.onBindViewHolder(mHolder);
+
+        assertThat(getSecondaryButton().getText().toString()).isEqualTo("");
+    }
+
+    @Test
+    public void performClickOnPrimaryButton_shouldCalledClickListener() {
+        final boolean[] hasCalled = {false};
+        View.OnClickListener clickListener = v -> hasCalled[0] = true;
+        mCardPreference.setPrimaryButtonClickListener(clickListener);
+
+        mCardPreference.onBindViewHolder(mHolder);
+        getPrimaryButton().performClick();
+
+        assertThat(hasCalled[0]).isTrue();
+    }
+
+    @Test
+    public void performClickOnSecondaryButton_shouldCalledClickListener() {
+        final boolean[] hasCalled = {false};
+        View.OnClickListener clickListener = v -> hasCalled[0] = true;
+        mCardPreference.setSecondaryButtonClickListener(clickListener);
+
+        mCardPreference.onBindViewHolder(mHolder);
+        getSecondaryButton().performClick();
+
+        assertThat(hasCalled[0]).isTrue();
+    }
+
+    @Test
+    public void onBindViewHolder_primaryButtonDefaultIsGone() {
+        mCardPreference.onBindViewHolder(mHolder);
+
+        assertThat(getPrimaryButton().getVisibility()).isEqualTo(GONE);
+    }
+
+    @Test
+    public void onBindViewHolder_secondaryButtonDefaultIsGone() {
+        mCardPreference.onBindViewHolder(mHolder);
+
+        assertThat(getSecondaryButton().getVisibility()).isEqualTo(GONE);
+    }
+
+    @Test
+    public void setPrimaryButtonVisibility_setTrueAfterBindViewHolder_shouldBeVisible() {
+        mCardPreference.setPrimaryButtonVisible(false);
+        mCardPreference.onBindViewHolder(mHolder);
+
+        mCardPreference.setPrimaryButtonVisible(true);
+
+        assertThat(getPrimaryButton().getVisibility()).isEqualTo(VISIBLE);
+    }
+
+    @Test
+    public void setPrimaryButtonText_setAfterBindViewHolder_setOnUi() {
+        String expectedText = "123456";
+        mCardPreference.onBindViewHolder(mHolder);
+
+        mCardPreference.setPrimaryButtonText(expectedText);
+
+        assertThat(getPrimaryButton().getText().toString()).isEqualTo(expectedText);
+    }
+
+    @Test
+    public void setPrimaryButtonText_setNull_shouldBeEmptyText() {
+        final String emptyString = "";
+        mCardPreference.setPrimaryButtonText("1234");
+        mCardPreference.onBindViewHolder(mHolder);
+
+        mCardPreference.setPrimaryButtonText(null);
+
+        assertThat(getPrimaryButton().getText().toString()).isEqualTo(emptyString);
+    }
+
+    @Test
+    public void setPrimaryButtonClickListener_setAfterOnBindViewHolder() {
+        final String[] hasCalled = {""};
+        String expectedClickedResult = "was called";
+        View.OnClickListener clickListener = v -> hasCalled[0] = expectedClickedResult;
+        mCardPreference.onBindViewHolder(mHolder);
+
+        mCardPreference.setPrimaryButtonClickListener(clickListener);
+        getPrimaryButton().performClick();
+
+        assertThat(hasCalled[0]).isEqualTo(expectedClickedResult);
+    }
+
+    @Test
+    public void setPrimaryButtonClickListener_setNull_shouldClearTheOnClickListener() {
+        final String[] hasCalled = {"not called"};
+        View.OnClickListener clickListener = v -> hasCalled[0] = "called once";
+        mCardPreference.setPrimaryButtonClickListener(clickListener);
+        mCardPreference.onBindViewHolder(mHolder);
+
+        mCardPreference.setPrimaryButtonClickListener(null);
+        getPrimaryButton().performClick();
+
+        assertThat(hasCalled[0]).isEqualTo("not called");
+    }
+
+    @Test
+    public void setSecondaryButtonVisibility_setTrueAfterBindViewHolder_shouldBeVisible() {
+        mCardPreference.setSecondaryButtonVisible(false);
+        mCardPreference.onBindViewHolder(mHolder);
+
+        mCardPreference.setSecondaryButtonVisible(true);
+
+        assertThat(getSecondaryButton().getVisibility()).isEqualTo(VISIBLE);
+    }
+
+    @Test
+    public void setSecondaryButtonText_setAfterBindViewHolder_setOnUi() {
+        String expectedText = "10101010";
+        mCardPreference.onBindViewHolder(mHolder);
+
+        mCardPreference.setSecondaryButtonText(expectedText);
+
+        assertThat(getSecondaryButton().getText().toString()).isEqualTo(expectedText);
+    }
+
+    @Test
+    public void setSecondaryButtonText_setNull_shouldBeEmptyText() {
+        String emptyString = "";
+        mCardPreference.setSecondaryButtonText("1234");
+        mCardPreference.onBindViewHolder(mHolder);
+
+        mCardPreference.setSecondaryButtonText(null);
+
+        assertThat(getSecondaryButton().getText().toString()).isEqualTo(emptyString);
+    }
+
+    @Test
+    public void setSecondaryButtonClickListener_setAfterOnBindViewHolder() {
+        final String[] hasCalled = {""};
+        String expectedClickedResult = "2nd was called";
+        View.OnClickListener clickListener = v -> hasCalled[0] = expectedClickedResult;
+        mCardPreference.onBindViewHolder(mHolder);
+
+        mCardPreference.setSecondaryButtonClickListener(clickListener);
+        getSecondaryButton().performClick();
+
+        assertThat(hasCalled[0]).isEqualTo(expectedClickedResult);
+    }
+
+    @Test
+    public void setSecondaryButtonClickListener_setNull_shouldClearTheOnClickListener() {
+        final String[] hasCalled = {"not called"};
+        View.OnClickListener clickListener = v -> hasCalled[0] = "called once";
+        mCardPreference.setSecondaryButtonClickListener(clickListener);
+        mCardPreference.onBindViewHolder(mHolder);
+
+        mCardPreference.setSecondaryButtonClickListener(null);
+        getSecondaryButton().performClick();
+
+        assertThat(hasCalled[0]).isEqualTo("not called");
+    }
+
+    @Test
+    public void
+            setPrimaryButtonVisibility_onlyPrimaryButtonVisible_setGone_buttonGroupShouldBeGone() {
+        mCardPreference.setPrimaryButtonVisible(true);
+        mCardPreference.setSecondaryButtonVisible(false);
+        mCardPreference.onBindViewHolder(mHolder);
+        assertWithMessage("PreCondition: buttonsView should be Visible")
+                .that(getCardPreferenceButtonsView().getVisibility())
+                .isEqualTo(VISIBLE);
+
+        mCardPreference.setPrimaryButtonVisible(false);
+
+        assertThat(getCardPreferenceButtonsView().getVisibility()).isEqualTo(GONE);
+    }
+
+    @Test
+    public void
+            setSecondaryButtonVisibility_only2ndButtonVisible_setGone_buttonGroupShouldBeGone() {
+        mCardPreference.setPrimaryButtonVisible(false);
+        mCardPreference.setSecondaryButtonVisible(true);
+        mCardPreference.onBindViewHolder(mHolder);
+        assertWithMessage("PreCondition: buttonsView should be Visible")
+                .that(getCardPreferenceButtonsView().getVisibility())
+                .isEqualTo(VISIBLE);
+
+        mCardPreference.setSecondaryButtonVisible(false);
+
+        assertThat(getCardPreferenceButtonsView().getVisibility()).isEqualTo(GONE);
+    }
+
+    private View getCardPreferenceButtonsView() {
+        return mHolder.findViewById(R.id.card_preference_buttons);
+    }
+
+    private Button getPrimaryButton() {
+        return (Button) mHolder.findViewById(android.R.id.button1);
+    }
+
+    private Button getSecondaryButton() {
+        return (Button) mHolder.findViewById(android.R.id.button2);
     }
 }
diff --git a/tests/robotests/src/com/android/settings/wifi/details2/WifiDetailPreferenceController2Test.java b/tests/robotests/src/com/android/settings/wifi/details2/WifiDetailPreferenceController2Test.java
index 08bcd2a..274ac16 100644
--- a/tests/robotests/src/com/android/settings/wifi/details2/WifiDetailPreferenceController2Test.java
+++ b/tests/robotests/src/com/android/settings/wifi/details2/WifiDetailPreferenceController2Test.java
@@ -58,6 +58,7 @@
 import android.net.wifi.WifiManager;
 import android.os.Handler;
 import android.provider.Settings;
+import android.telephony.SubscriptionInfo;
 import android.telephony.TelephonyManager;
 import android.view.View;
 import android.view.View.OnClickListener;
@@ -108,6 +109,7 @@
 import java.time.format.DateTimeFormatter;
 import java.time.format.FormatStyle;
 import java.util.Arrays;
+import java.util.List;
 import java.util.stream.Collectors;
 
 // TODO(b/143326832): Should add test cases for connect button.
@@ -1791,4 +1793,74 @@
 
         return pref;
     }
+
+    @Test
+    public void fineSubscriptionInfo_noMatchedCarrierId_returnNull() {
+        setUpSpyController();
+        SubscriptionInfo sub1 = mockSubscriptionInfo(1, "sim1", 1111);
+        SubscriptionInfo sub2 = mockSubscriptionInfo(2, "sim2", 2222);
+        List<SubscriptionInfo> activeSubInfos = Arrays.asList(sub1, sub2);
+
+        SubscriptionInfo info = mController.fineSubscriptionInfo(3333, activeSubInfos, 1);
+
+        assertThat(info).isNull();
+
+        info = mController.fineSubscriptionInfo(3333, activeSubInfos, 2);
+
+        assertThat(info).isNull();
+    }
+
+    @Test
+    public void fineSubscriptionInfo_diffCarrierId_returnMatchedOne() {
+        setUpSpyController();
+        SubscriptionInfo sub1 = mockSubscriptionInfo(1, "sim1", 1111);
+        SubscriptionInfo sub2 = mockSubscriptionInfo(2, "sim2", 2222);
+        List<SubscriptionInfo> activeSubInfos = Arrays.asList(sub1, sub2);
+
+        SubscriptionInfo info = mController.fineSubscriptionInfo(1111, activeSubInfos, 1);
+
+        assertThat(info).isNotNull();
+        assertThat(info.getDisplayName().toString()).isEqualTo("sim1");
+
+        info = mController.fineSubscriptionInfo(1111, activeSubInfos, 2);
+
+        assertThat(info).isNotNull();
+        assertThat(info.getDisplayName().toString()).isEqualTo("sim1");
+
+        info = mController.fineSubscriptionInfo(2222, activeSubInfos, 1);
+
+        assertThat(info).isNotNull();
+        assertThat(info.getDisplayName().toString()).isEqualTo("sim2");
+
+        info = mController.fineSubscriptionInfo(2222, activeSubInfos, 2);
+
+        assertThat(info).isNotNull();
+        assertThat(info.getDisplayName().toString()).isEqualTo("sim2");
+    }
+
+    @Test
+    public void fineSubscriptionInfo_sameCarrierId_returnDefaultDataOne() {
+        setUpSpyController();
+        SubscriptionInfo sub1 = mockSubscriptionInfo(1, "sim1", 1111);
+        SubscriptionInfo sub2 = mockSubscriptionInfo(2, "sim2", 1111);
+        List<SubscriptionInfo> activeSubInfos = Arrays.asList(sub1, sub2);
+
+        SubscriptionInfo info = mController.fineSubscriptionInfo(1111, activeSubInfos, 1);
+
+        assertThat(info).isNotNull();
+        assertThat(info.getDisplayName().toString()).isEqualTo("sim1");
+
+        info = mController.fineSubscriptionInfo(1111, activeSubInfos, 2);
+
+        assertThat(info).isNotNull();
+        assertThat(info.getDisplayName().toString()).isEqualTo("sim2");
+    }
+
+    private SubscriptionInfo mockSubscriptionInfo(int subId, String displayName, int carrierId) {
+        SubscriptionInfo info = mock(SubscriptionInfo.class);
+        when(info.getSubscriptionId()).thenReturn(subId);
+        when(info.getDisplayName()).thenReturn(displayName);
+        when(info.getCarrierId()).thenReturn(carrierId);
+        return info;
+    }
 }
diff --git a/tests/unit/src/com/android/settings/applications/specialaccess/notificationaccess/BridgedAppsLinkPreferenceControllerTest.java b/tests/unit/src/com/android/settings/applications/specialaccess/notificationaccess/BridgedAppsLinkPreferenceControllerTest.java
index c594131..8799879 100644
--- a/tests/unit/src/com/android/settings/applications/specialaccess/notificationaccess/BridgedAppsLinkPreferenceControllerTest.java
+++ b/tests/unit/src/com/android/settings/applications/specialaccess/notificationaccess/BridgedAppsLinkPreferenceControllerTest.java
@@ -31,6 +31,7 @@
 import android.os.Build;
 import android.service.notification.NotificationListenerFilter;
 
+import androidx.preference.Preference;
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 
@@ -68,6 +69,11 @@
         mController.setTargetSdk(Build.VERSION_CODES.CUR_DEVELOPMENT + 1);
 
         assertThat(mController.getAvailabilityStatus()).isEqualTo(DISABLED_DEPENDENT_SETTING);
+
+        // disables field
+        Preference p = new Preference(mContext);
+        mController.updateState(p);
+        assertThat(p.isEnabled()).isFalse();
     }
 
     @Test
@@ -77,6 +83,11 @@
         when(mNm.getListenerFilter(mCn, 0)).thenReturn(new NotificationListenerFilter());
 
         assertThat(mController.getAvailabilityStatus()).isEqualTo(DISABLED_DEPENDENT_SETTING);
+
+        // disables field
+        Preference p = new Preference(mContext);
+        mController.updateState(p);
+        assertThat(p.isEnabled()).isFalse();
     }
 
     @Test
@@ -88,6 +99,11 @@
         when(mNm.getListenerFilter(mCn, 0)).thenReturn(nlf);
 
         assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE);
+
+        // enables field
+        Preference p = new Preference(mContext);
+        mController.updateState(p);
+        assertThat(p.isEnabled()).isTrue();
     }
 
     @Test
@@ -97,5 +113,10 @@
         when(mNm.getListenerFilter(mCn, 0)).thenReturn(new NotificationListenerFilter());
 
         assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE);
+
+        // enables field
+        Preference p = new Preference(mContext);
+        mController.updateState(p);
+        assertThat(p.isEnabled()).isTrue();
     }
 }
diff --git a/tests/unit/src/com/android/settings/security/SecuritySettingsTest.java b/tests/unit/src/com/android/settings/security/SecuritySettingsTest.java
index c50d993..272c840 100644
--- a/tests/unit/src/com/android/settings/security/SecuritySettingsTest.java
+++ b/tests/unit/src/com/android/settings/security/SecuritySettingsTest.java
@@ -16,26 +16,42 @@
 
 package com.android.settings.security;
 
+import static android.content.Context.FACE_SERVICE;
+import static android.content.Context.FINGERPRINT_SERVICE;
+import static android.content.pm.PackageManager.FEATURE_FACE;
+import static android.content.pm.PackageManager.FEATURE_FINGERPRINT;
+
 import static com.android.settings.core.PreferenceXmlParserUtils.METADATA_KEY;
 import static com.android.settings.core.PreferenceXmlParserUtils.MetadataFlag.FLAG_INCLUDE_PREF_SCREEN;
 import static com.android.settings.core.PreferenceXmlParserUtils.MetadataFlag.FLAG_NEED_KEY;
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.when;
 
 import android.annotation.XmlRes;
 import android.content.Context;
+import android.content.pm.PackageManager;
+import android.hardware.face.FaceManager;
+import android.hardware.fingerprint.FingerprintManager;
 import android.os.Bundle;
+import android.os.Looper;
 import android.provider.SearchIndexableResource;
 
+import androidx.test.annotation.UiThreadTest;
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 
+import com.android.settings.biometrics.combination.CombinedBiometricStatusPreferenceController;
+import com.android.settings.biometrics.face.FaceStatusPreferenceController;
+import com.android.settings.biometrics.fingerprint.FingerprintStatusPreferenceController;
 import com.android.settings.core.PreferenceXmlParserUtils;
 import com.android.settings.search.BaseSearchIndexProvider;
 import com.android.settings.security.trustagent.TrustAgentManager;
 import com.android.settings.testutils.FakeFeatureFactory;
+import com.android.settingslib.core.AbstractPreferenceController;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -52,26 +68,44 @@
 
     private Context mContext;
     private SecuritySettingsFeatureProvider mSecuritySettingsFeatureProvider;
+    private SecuritySettings mSecuritySettings;
 
     @Mock
     private TrustAgentManager mTrustAgentManager;
+    @Mock
+    private FaceManager mFaceManager;
+    @Mock
+    private FingerprintManager mFingerprintManager;
+    @Mock
+    private PackageManager mPackageManager;
 
     @Before
+    @UiThreadTest
     public void setup() {
+        if (Looper.myLooper() == null) {
+            Looper.prepare();
+        }
+
         MockitoAnnotations.initMocks(this);
-        mContext = ApplicationProvider.getApplicationContext();
+        mContext = spy(ApplicationProvider.getApplicationContext());
+        when(mContext.getPackageManager()).thenReturn(mPackageManager);
+        when(mPackageManager.hasSystemFeature(FEATURE_FACE)).thenReturn(true);
+        when(mPackageManager.hasSystemFeature(FEATURE_FINGERPRINT)).thenReturn(true);
+        doReturn(mFaceManager).when(mContext).getSystemService(FACE_SERVICE);
+        doReturn(mFingerprintManager).when(mContext).getSystemService(FINGERPRINT_SERVICE);
         FakeFeatureFactory mFeatureFactory = FakeFeatureFactory.setupForTest();
-        mSecuritySettingsFeatureProvider = mFeatureFactory.getSecuritySettingsFeatureProvider();
         SecurityFeatureProvider mSecurityFeatureProvider =
                 mFeatureFactory.getSecurityFeatureProvider();
 
         when(mSecurityFeatureProvider.getTrustAgentManager()).thenReturn(mTrustAgentManager);
+        mSecuritySettingsFeatureProvider = mFeatureFactory.getSecuritySettingsFeatureProvider();
+        mSecuritySettings = new SecuritySettings();
     }
 
     @Test
     public void noAlternativeFragmentAvailable_pageIndexIncluded() throws Exception {
-        when(mSecuritySettingsFeatureProvider.hasAlternativeSecuritySettingsFragment())
-                .thenReturn(false);
+        when(mSecuritySettingsFeatureProvider.hasAlternativeSecuritySettingsFragment()).thenReturn(
+                false);
         BaseSearchIndexProvider indexProvider = SecuritySettings.SEARCH_INDEX_DATA_PROVIDER;
 
         List<String> allXmlKeys = getAllXmlKeys(indexProvider);
@@ -83,8 +117,8 @@
 
     @Test
     public void alternativeFragmentAvailable_pageIndexExcluded() throws Exception {
-        when(mSecuritySettingsFeatureProvider.hasAlternativeSecuritySettingsFragment())
-                .thenReturn(true);
+        when(mSecuritySettingsFeatureProvider.hasAlternativeSecuritySettingsFragment()).thenReturn(
+                true);
         BaseSearchIndexProvider indexProvider = SecuritySettings.SEARCH_INDEX_DATA_PROVIDER;
 
         List<String> allXmlKeys = getAllXmlKeys(indexProvider);
@@ -94,6 +128,45 @@
         assertThat(allXmlKeys).isEmpty();
     }
 
+    @Test
+    @UiThreadTest
+    public void preferenceController_containsFaceWhenAvailable() {
+        when(mFaceManager.isHardwareDetected()).thenReturn(true);
+        when(mFingerprintManager.isHardwareDetected()).thenReturn(false);
+        final List<AbstractPreferenceController> controllers =
+                mSecuritySettings.createPreferenceControllers(mContext);
+
+        assertThat(isFacePrefAvailable(controllers)).isTrue();
+        assertThat(isFingerprintPrefAvailable(controllers)).isFalse();
+        assertThat(isCombinedPrefAvailable(controllers)).isFalse();
+    }
+
+    @Test
+    @UiThreadTest
+    public void preferenceController_containsFingerprintWhenAvailable() {
+        when(mFaceManager.isHardwareDetected()).thenReturn(false);
+        when(mFingerprintManager.isHardwareDetected()).thenReturn(true);
+        final List<AbstractPreferenceController> controllers =
+                mSecuritySettings.createPreferenceControllers(mContext);
+
+        assertThat(isFacePrefAvailable(controllers)).isFalse();
+        assertThat(isFingerprintPrefAvailable(controllers)).isTrue();
+        assertThat(isCombinedPrefAvailable(controllers)).isFalse();
+    }
+
+    @Test
+    @UiThreadTest
+    public void preferenceController_containsCombinedBiometricWhenAvailable() {
+        when(mFaceManager.isHardwareDetected()).thenReturn(true);
+        when(mFingerprintManager.isHardwareDetected()).thenReturn(true);
+        final List<AbstractPreferenceController> controllers =
+                mSecuritySettings.createPreferenceControllers(mContext);
+
+        assertThat(isFacePrefAvailable(controllers)).isFalse();
+        assertThat(isFingerprintPrefAvailable(controllers)).isFalse();
+        assertThat(isCombinedPrefAvailable(controllers)).isTrue();
+    }
+
     private List<String> getAllXmlKeys(BaseSearchIndexProvider indexProvider) throws Exception {
         final List<SearchIndexableResource> resources = indexProvider.getXmlResourcesToIndex(
                 mContext, true /* not used*/);
@@ -109,11 +182,29 @@
 
     private List<String> getKeysFromXml(@XmlRes int xmlResId) throws Exception {
         final List<String> keys = new ArrayList<>();
-        final List<Bundle> metadata = PreferenceXmlParserUtils
-                .extractMetadata(mContext, xmlResId, FLAG_NEED_KEY | FLAG_INCLUDE_PREF_SCREEN);
+        final List<Bundle> metadata = PreferenceXmlParserUtils.extractMetadata(mContext, xmlResId,
+                FLAG_NEED_KEY | FLAG_INCLUDE_PREF_SCREEN);
         for (Bundle bundle : metadata) {
             keys.add(bundle.getString(METADATA_KEY));
         }
         return keys;
     }
+
+    boolean isFacePrefAvailable(List<AbstractPreferenceController> controllers) {
+        return controllers.stream().filter(
+                controller -> controller instanceof FaceStatusPreferenceController
+                        && controller.isAvailable()).count() == 1;
+    }
+
+    boolean isFingerprintPrefAvailable(List<AbstractPreferenceController> controllers) {
+        return controllers.stream().filter(
+                controller -> controller instanceof FingerprintStatusPreferenceController
+                        && controller.isAvailable()).count() == 1;
+    }
+
+    boolean isCombinedPrefAvailable(List<AbstractPreferenceController> controllers) {
+        return controllers.stream().filter(
+                controller -> controller instanceof CombinedBiometricStatusPreferenceController
+                        && controller.isAvailable()).count() == 1;
+    }
 }
diff --git a/tests/unit/src/com/android/settings/vpn2/VpnSettingsTest.java b/tests/unit/src/com/android/settings/vpn2/VpnSettingsTest.java
index 86bd1e7..953a524 100644
--- a/tests/unit/src/com/android/settings/vpn2/VpnSettingsTest.java
+++ b/tests/unit/src/com/android/settings/vpn2/VpnSettingsTest.java
@@ -20,15 +20,22 @@
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doNothing;
 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 static org.mockito.Mockito.when;
 
 import android.app.AppOpsManager;
 import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.os.Looper;
 import android.os.UserHandle;
+import android.text.TextUtils;
 import android.util.ArraySet;
 
 import androidx.preference.Preference;
@@ -46,6 +53,7 @@
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.junit.MockitoJUnit;
 import org.mockito.junit.MockitoRule;
@@ -56,19 +64,26 @@
 
 @RunWith(AndroidJUnit4.class)
 public class VpnSettingsTest {
-    private static final String ADVANCED_VPN_GROUP_KEY = "advanced_vpn_group";
-    private static final String VPN_GROUP_KEY = "vpn_group";
-    private static final String ADVANCED_VPN_GROUP_TITLE = "advanced_vpn_group_title";
-    private static final String VPN_GROUP_TITLE = "vpn_group_title";
-    private static final String FAKE_PACKAGE_NAME = "com.fake.package.name";
-    private static final String ADVANCED_VPN_GROUP_PACKAGE_NAME = "com.advanced.package.name";
     private static final int USER_ID_1 = UserHandle.USER_NULL;
+    private static final String VPN_GROUP_KEY = "vpn_group";
+    private static final String VPN_GROUP_TITLE = "vpn_group_title";
+    private static final String VPN_PACKAGE_NAME = "vpn.package.name";
+    private static final String VPN_LAUNCH_INTENT = "vpn.action";
+    private static final String ADVANCED_VPN_GROUP_KEY = "advanced_vpn_group";
+    private static final String ADVANCED_VPN_GROUP_TITLE = "advanced_vpn_group_title";
+    private static final String ADVANCED_VPN_PACKAGE_NAME = "advanced.vpn.package.name";
+    private static final String ADVANCED_VPN_LAUNCH_INTENT = "advanced.vpn.action";
+
+    private final Intent mVpnIntent = new Intent().setAction(VPN_LAUNCH_INTENT);
+    private final Intent mAdvancedVpnIntent = new Intent().setAction(ADVANCED_VPN_LAUNCH_INTENT);
 
     @Rule
     public final MockitoRule mMockitoRule = MockitoJUnit.rule();
 
     @Mock
     private AppOpsManager mAppOpsManager;
+    @Mock
+    private PackageManager mPackageManager;
 
     private VpnSettings mVpnSettings;
     private Context mContext;
@@ -104,9 +119,10 @@
         when(mFakeFeatureFactory.mAdvancedVpnFeatureProvider.getVpnPreferenceGroupTitle(mContext))
                 .thenReturn(VPN_GROUP_TITLE);
         when(mFakeFeatureFactory.mAdvancedVpnFeatureProvider.getAdvancedVpnPackageName())
-                .thenReturn(ADVANCED_VPN_GROUP_PACKAGE_NAME);
+                .thenReturn(ADVANCED_VPN_PACKAGE_NAME);
         when(mFakeFeatureFactory.mAdvancedVpnFeatureProvider.isAdvancedVpnSupported(any()))
                 .thenReturn(true);
+        when(mContext.getPackageManager()).thenReturn(mPackageManager);
         doReturn(mContext).when(mContext).createContextAsUser(any(), anyInt());
         doReturn(mContext).when(mContext).createPackageContextAsUser(any(), anyInt(), any());
         doReturn(mPreferenceManager).when(mVpnGroup).getPreferenceManager();
@@ -117,7 +133,7 @@
     public void setShownAdvancedPreferences_hasGeneralVpn_returnsVpnCountAs1() {
         Set<Preference> updates = new ArraySet<>();
         AppPreference pref =
-                spy(new AppPreference(mContext, USER_ID_1, FAKE_PACKAGE_NAME));
+                spy(new AppPreference(mContext, USER_ID_1, VPN_PACKAGE_NAME));
         updates.add(pref);
 
         mVpnSettings.setShownAdvancedPreferences(updates);
@@ -131,7 +147,7 @@
     public void setShownAdvancedPreferences_hasAdvancedVpn_returnsAdvancedVpnCountAs1() {
         Set<Preference> updates = new ArraySet<>();
         AppPreference pref =
-                spy(new AppPreference(mContext, USER_ID_1, ADVANCED_VPN_GROUP_PACKAGE_NAME));
+                spy(new AppPreference(mContext, USER_ID_1, ADVANCED_VPN_PACKAGE_NAME));
         updates.add(pref);
 
         mVpnSettings.setShownAdvancedPreferences(updates);
@@ -154,14 +170,10 @@
     }
 
     @Test
-    public void getVpnApps_isAdvancedVpn_returnsOne() {
-        int uid = 1111;
-        List<AppOpsManager.OpEntry> opEntries = new ArrayList<>();
-        List<AppOpsManager.PackageOps> apps = new ArrayList<>();
-        AppOpsManager.PackageOps packageOps =
-                new AppOpsManager.PackageOps(ADVANCED_VPN_GROUP_PACKAGE_NAME, uid, opEntries);
-        apps.add(packageOps);
-        when(mAppOpsManager.getPackagesForOps((int[]) any())).thenReturn(apps);
+    public void getVpnApps_isAdvancedVpn_returnsOne() throws Exception {
+        ApplicationInfo info = new ApplicationInfo();
+        info.uid = 1111;
+        when(mPackageManager.getApplicationInfo(anyString(), anyInt())).thenReturn(info);
 
         assertThat(VpnSettings.getVpnApps(mContext, /* includeProfiles= */ false,
                 mFakeFeatureFactory.getAdvancedVpnFeatureProvider(),
@@ -174,12 +186,100 @@
         List<AppOpsManager.OpEntry> opEntries = new ArrayList<>();
         List<AppOpsManager.PackageOps> apps = new ArrayList<>();
         AppOpsManager.PackageOps packageOps =
-                new AppOpsManager.PackageOps(FAKE_PACKAGE_NAME, uid, opEntries);
+                new AppOpsManager.PackageOps(VPN_PACKAGE_NAME, uid, opEntries);
         apps.add(packageOps);
         when(mAppOpsManager.getPackagesForOps((int[]) any())).thenReturn(apps);
+        when(mFakeFeatureFactory.mAdvancedVpnFeatureProvider.isAdvancedVpnSupported(any()))
+                .thenReturn(false);
 
         assertThat(VpnSettings.getVpnApps(mContext, /* includeProfiles= */ false,
                 mFakeFeatureFactory.getAdvancedVpnFeatureProvider(),
                 mAppOpsManager)).isEmpty();
     }
+
+    @Test
+    public void clickVpn_VpnConnected_doesNotStartVpnLaunchIntent()
+            throws PackageManager.NameNotFoundException {
+        Set<Preference> updates = new ArraySet<>();
+        AppPreference pref = spy(new AppPreference(mContext, USER_ID_1, VPN_PACKAGE_NAME));
+        pref.setState(AppPreference.STATE_CONNECTED);
+        updates.add(pref);
+        when(mContext.createPackageContextAsUser(any(), anyInt(), any())).thenReturn(mContext);
+        when(mContext.getPackageManager()).thenReturn(mPackageManager);
+        when(mPackageManager.getLaunchIntentForPackage(any())).thenReturn(mVpnIntent);
+        ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class);
+        doNothing().when(mContext).startActivityAsUser(captor.capture(), any());
+        mVpnSettings.setShownPreferences(updates);
+
+        mVpnSettings.onPreferenceClick(pref);
+
+        verify(mContext, never()).startActivityAsUser(any(), any());
+    }
+
+    @Test
+    public void clickVpn_VpnDisconnected_startsVpnLaunchIntent()
+            throws PackageManager.NameNotFoundException {
+        Set<Preference> updates = new ArraySet<>();
+        AppPreference pref = spy(new AppPreference(mContext, USER_ID_1, VPN_PACKAGE_NAME));
+        pref.setState(AppPreference.STATE_DISCONNECTED);
+        updates.add(pref);
+        when(mContext.createPackageContextAsUser(any(), anyInt(), any())).thenReturn(mContext);
+        when(mContext.getPackageManager()).thenReturn(mPackageManager);
+        when(mPackageManager.getLaunchIntentForPackage(any())).thenReturn(mVpnIntent);
+        ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class);
+        doNothing().when(mContext).startActivityAsUser(captor.capture(), any());
+        mVpnSettings.setShownPreferences(updates);
+
+        mVpnSettings.onPreferenceClick(pref);
+
+        verify(mContext).startActivityAsUser(captor.capture(), any());
+        assertThat(TextUtils.equals(captor.getValue().getAction(),
+                VPN_LAUNCH_INTENT)).isTrue();
+    }
+
+    @Test
+    public void clickAdvancedVpn_VpnConnectedDisconnectDialogDisabled_startsAppLaunchIntent()
+            throws PackageManager.NameNotFoundException {
+        Set<Preference> updates = new ArraySet<>();
+        AppPreference pref =
+                spy(new AppPreference(mContext, USER_ID_1, ADVANCED_VPN_PACKAGE_NAME));
+        pref.setState(AppPreference.STATE_CONNECTED);
+        updates.add(pref);
+        when(mFakeFeatureFactory.mAdvancedVpnFeatureProvider.isDisconnectDialogEnabled())
+                .thenReturn(false);
+        when(mContext.createPackageContextAsUser(any(), anyInt(), any())).thenReturn(mContext);
+        when(mContext.getPackageManager()).thenReturn(mPackageManager);
+        when(mPackageManager.getLaunchIntentForPackage(any())).thenReturn(mAdvancedVpnIntent);
+        ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class);
+        doNothing().when(mContext).startActivityAsUser(captor.capture(), any());
+        mVpnSettings.setShownAdvancedPreferences(updates);
+
+        mVpnSettings.onPreferenceClick(pref);
+
+        verify(mContext).startActivityAsUser(captor.capture(), any());
+        assertThat(TextUtils.equals(captor.getValue().getAction(),
+                ADVANCED_VPN_LAUNCH_INTENT)).isTrue();
+    }
+
+    @Test
+    public void clickAdvancedVpn_VpnConnectedDisconnectDialogEnabled_doesNotStartAppLaunchIntent()
+            throws PackageManager.NameNotFoundException {
+        Set<Preference> updates = new ArraySet<>();
+        AppPreference pref =
+                spy(new AppPreference(mContext, USER_ID_1, ADVANCED_VPN_PACKAGE_NAME));
+        pref.setState(AppPreference.STATE_CONNECTED);
+        updates.add(pref);
+        when(mFakeFeatureFactory.mAdvancedVpnFeatureProvider.isDisconnectDialogEnabled())
+                .thenReturn(true);
+        when(mContext.createPackageContextAsUser(any(), anyInt(), any())).thenReturn(mContext);
+        when(mContext.getPackageManager()).thenReturn(mPackageManager);
+        when(mPackageManager.getLaunchIntentForPackage(any())).thenReturn(mAdvancedVpnIntent);
+        ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class);
+        doNothing().when(mContext).startActivityAsUser(captor.capture(), any());
+        mVpnSettings.setShownAdvancedPreferences(updates);
+
+        mVpnSettings.onPreferenceClick(pref);
+
+        verify(mContext, never()).startActivityAsUser(any(), any());
+    }
 }