Merge "Copy DatabaseUtils from SettingsGoogle to Settings and use the new Settings database."
diff --git a/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtils.java b/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtils.java
index 4ad1ac3..0e2a81f 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtils.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtils.java
@@ -15,22 +15,319 @@
  */
 
 package com.android.settings.fuelgauge.batteryusage;
-
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.ContentValues;
 import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.BatteryManager;
+import android.os.BatteryUsageStats;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.SystemClock;
+import android.os.UserHandle;
 import android.os.UserManager;
+import android.util.Log;
+
+import androidx.annotation.VisibleForTesting;
+
+import com.android.settingslib.fuelgauge.BatteryStatus;
+
+import java.time.Clock;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
 
 /** A utility class to operate battery usage database. */
 public final class DatabaseUtils {
     private static final String TAG = "DatabaseUtils";
+    private static final String PREF_FILE_NAME = "battery_module_preference";
+    private static final String PREF_FULL_CHARGE_TIMESTAMP_KEY = "last_full_charge_timestamp_key";
+    /** Key for query parameter timestamp used in BATTERY_CONTENT_URI **/
+    private static final String QUERY_KEY_TIMESTAMP = "timestamp";
+    /** Clear memory threshold for device booting phase. **/
+    private static final long CLEAR_MEMORY_THRESHOLD_MS = Duration.ofMinutes(5).toMillis();
+    private static final long CLEAR_MEMORY_DELAYED_MS = Duration.ofSeconds(2).toMillis();
 
     /** An authority name of the battery content provider. */
     public static final String AUTHORITY = "com.android.settings.battery.usage.provider";
     /** A table name for battery usage history. */
     public static final String BATTERY_STATE_TABLE = "BatteryState";
+    /** A class name for battery usage data provider. */
+    public static final String SETTINGS_PACKAGE_PATH = "com.android.settings";
+    public static final String BATTERY_PROVIDER_CLASS_PATH =
+            "com.android.settings.fuelgauge.batteryusage.BatteryUsageContentProvider";
+
+    /** A content URI to access battery usage states data. */
+    public static final Uri BATTERY_CONTENT_URI =
+            new Uri.Builder()
+                    .scheme(ContentResolver.SCHEME_CONTENT)
+                    .authority(AUTHORITY)
+                    .appendPath(BATTERY_STATE_TABLE)
+                    .build();
+
+    private DatabaseUtils() {
+    }
 
     /** Returns true if current user is a work profile user. */
     public static boolean isWorkProfile(Context context) {
         final UserManager userManager = context.getSystemService(UserManager.class);
         return userManager.isManagedProfile() && !userManager.isSystemUser();
     }
+
+    /** Returns true if the chart graph design is enabled. */
+    public static boolean isChartGraphEnabled(Context context) {
+        return isContentProviderEnabled(context);
+    }
+
+    /** Long: for timestamp and String: for BatteryHistEntry.getKey() */
+    public static Map<Long, Map<String, BatteryHistEntry>> getHistoryMapSinceLastFullCharge(
+            Context context, Calendar calendar) {
+        final long startTime = System.currentTimeMillis();
+        final long lastFullChargeTimestamp =
+                getStartTimestampForLastFullCharge(context, calendar);
+        // Builds the content uri everytime to avoid cache.
+        final Uri batteryStateUri =
+                new Uri.Builder()
+                        .scheme(ContentResolver.SCHEME_CONTENT)
+                        .authority(AUTHORITY)
+                        .appendPath(BATTERY_STATE_TABLE)
+                        .appendQueryParameter(
+                                QUERY_KEY_TIMESTAMP, Long.toString(lastFullChargeTimestamp))
+                        .build();
+
+        final Map<Long, Map<String, BatteryHistEntry>> resultMap =
+                loadHistoryMapFromContentProvider(context, batteryStateUri);
+        if (resultMap == null || resultMap.isEmpty()) {
+            Log.d(TAG, "getHistoryMapSinceLastFullCharge() returns empty or null");
+        } else {
+            Log.d(TAG, String.format("getHistoryMapSinceLastFullCharge() size=%d in %d/ms",
+                    resultMap.size(), (System.currentTimeMillis() - startTime)));
+        }
+        return resultMap;
+    }
+
+    static boolean isContentProviderEnabled(Context context) {
+        return context.getPackageManager()
+                .getComponentEnabledSetting(
+                        new ComponentName(SETTINGS_PACKAGE_PATH, BATTERY_PROVIDER_CLASS_PATH))
+                == PackageManager.COMPONENT_ENABLED_STATE_ENABLED;
+    }
+
+    static List<ContentValues> sendBatteryEntryData(
+            Context context,
+            List<BatteryEntry> batteryEntryList,
+            BatteryUsageStats batteryUsageStats) {
+        final long startTime = System.currentTimeMillis();
+        final Intent intent = getBatteryIntent(context);
+        if (intent == null) {
+            Log.e(TAG, "sendBatteryEntryData(): cannot fetch battery intent");
+            clearMemory();
+            return null;
+        }
+        final int batteryLevel = getBatteryLevel(intent);
+        final int batteryStatus = intent.getIntExtra(
+                BatteryManager.EXTRA_STATUS, BatteryManager.BATTERY_STATUS_UNKNOWN);
+        final int batteryHealth = intent.getIntExtra(
+                BatteryManager.EXTRA_HEALTH, BatteryManager.BATTERY_HEALTH_UNKNOWN);
+        // We should use the same timestamp for each data snapshot.
+        final long snapshotTimestamp = Clock.systemUTC().millis();
+        final long snapshotBootTimestamp = SystemClock.elapsedRealtime();
+
+        // Creates the ContentValues list to insert them into provider.
+        final List<ContentValues> valuesList = new ArrayList<>();
+        if (batteryEntryList != null) {
+            batteryEntryList.stream()
+                    .filter(entry -> {
+                        final long foregroundMs = entry.getTimeInForegroundMs();
+                        final long backgroundMs = entry.getTimeInBackgroundMs();
+                        if (entry.getConsumedPower() == 0
+                                && (foregroundMs != 0 || backgroundMs != 0)) {
+                            Log.w(TAG, String.format(
+                                    "no consumed power but has running time for %s time=%d|%d",
+                                    entry.getLabel(), foregroundMs, backgroundMs));
+                        }
+                        return entry.getConsumedPower() != 0
+                                || foregroundMs != 0
+                                || backgroundMs != 0;
+                    })
+                    .forEach(entry -> valuesList.add(
+                            ConvertUtils.convertToContentValues(
+                                    entry,
+                                    batteryUsageStats,
+                                    batteryLevel,
+                                    batteryStatus,
+                                    batteryHealth,
+                                    snapshotBootTimestamp,
+                                    snapshotTimestamp)));
+        }
+
+        int size = 1;
+        final ContentResolver resolver = context.getContentResolver();
+        // Inserts all ContentValues into battery provider.
+        if (!valuesList.isEmpty()) {
+            final ContentValues[] valuesArray = new ContentValues[valuesList.size()];
+            valuesList.toArray(valuesArray);
+            try {
+                size = resolver.bulkInsert(BATTERY_CONTENT_URI, valuesArray);
+            } catch (Exception e) {
+                Log.e(TAG, "bulkInsert() data into database error:\n" + e);
+            }
+        } else {
+            // Inserts one fake data into battery provider.
+            final ContentValues contentValues =
+                    ConvertUtils.convertToContentValues(
+                            /*entry=*/ null,
+                            /*batteryUsageStats=*/ null,
+                            batteryLevel,
+                            batteryStatus,
+                            batteryHealth,
+                            snapshotBootTimestamp,
+                            snapshotTimestamp);
+            try {
+                resolver.insert(BATTERY_CONTENT_URI, contentValues);
+            } catch (Exception e) {
+                Log.e(TAG, "insert() data into database error:\n" + e);
+            }
+            valuesList.add(contentValues);
+        }
+        saveLastFullChargeTimestampPref(context, batteryStatus, batteryLevel, snapshotTimestamp);
+        resolver.notifyChange(BATTERY_CONTENT_URI, /*observer=*/ null);
+        Log.d(TAG, String.format("sendBatteryEntryData() size=%d in %d/ms",
+                size, (System.currentTimeMillis() - startTime)));
+        clearMemory();
+        return valuesList;
+    }
+
+    @VisibleForTesting
+    static void saveLastFullChargeTimestampPref(
+            Context context, int batteryStatus, int batteryLevel, long timestamp) {
+        // Updates the SharedPreference only when timestamp is valid and phone is full charge.
+        if (!BatteryStatus.isCharged(batteryStatus, batteryLevel)) {
+            return;
+        }
+
+        final boolean success =
+                getSharedPreferences(context)
+                        .edit()
+                        .putLong(PREF_FULL_CHARGE_TIMESTAMP_KEY, timestamp)
+                        .commit();
+        if (!success) {
+            Log.w(TAG, "saveLastFullChargeTimestampPref() fail: value=" + timestamp);
+        }
+    }
+
+    @VisibleForTesting
+    static long getLastFullChargeTimestampPref(Context context) {
+        return getSharedPreferences(context).getLong(PREF_FULL_CHARGE_TIMESTAMP_KEY, 0);
+    }
+
+    /**
+     * Returns the start timestamp for "since last full charge" battery usage chart.
+     * If the last full charge happens within the last 7 days, returns the timestamp of last full
+     * charge. Otherwise, returns the timestamp for 00:00 6 days before the calendar date.
+     */
+    @VisibleForTesting
+    static long getStartTimestampForLastFullCharge(
+            Context context, Calendar calendar) {
+        final long lastFullChargeTimestamp = getLastFullChargeTimestampPref(context);
+        final long sixDayAgoTimestamp = getTimestampSixDaysAgo(calendar);
+        return Math.max(lastFullChargeTimestamp, sixDayAgoTimestamp);
+    }
+
+    private static Map<Long, Map<String, BatteryHistEntry>> loadHistoryMapFromContentProvider(
+            Context context, Uri batteryStateUri) {
+        final boolean isWorkProfileUser = isWorkProfile(context);
+        Log.d(TAG, "loadHistoryMapFromContentProvider() isWorkProfileUser:" + isWorkProfileUser);
+        if (isWorkProfileUser) {
+            try {
+                context = context.createPackageContextAsUser(
+                        /*packageName=*/ context.getPackageName(),
+                        /*flags=*/ 0,
+                        /*user=*/ UserHandle.OWNER);
+            } catch (PackageManager.NameNotFoundException e) {
+                Log.e(TAG, "context.createPackageContextAsUser() fail:" + e);
+                return null;
+            }
+        }
+        if (!isContentProviderEnabled(context)) {
+            return null;
+        }
+        final Map<Long, Map<String, BatteryHistEntry>> resultMap = new HashMap();
+        try (Cursor cursor =
+                     context.getContentResolver().query(batteryStateUri, null, null, null)) {
+            if (cursor == null || cursor.getCount() == 0) {
+                return resultMap;
+            }
+            // Loads and recovers all BatteryHistEntry data from cursor.
+            while (cursor.moveToNext()) {
+                final BatteryHistEntry entry = new BatteryHistEntry(cursor);
+                final long timestamp = entry.mTimestamp;
+                final String key = entry.getKey();
+                Map batteryHistEntryMap = resultMap.get(timestamp);
+                // Creates new one if there is no corresponding map.
+                if (batteryHistEntryMap == null) {
+                    batteryHistEntryMap = new HashMap<>();
+                    resultMap.put(timestamp, batteryHistEntryMap);
+                }
+                batteryHistEntryMap.put(key, entry);
+            }
+        }
+        return resultMap;
+    }
+
+    /** Gets the latest sticky battery intent from framework. */
+    private static Intent getBatteryIntent(Context context) {
+        return context.registerReceiver(
+                /*receiver=*/ null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
+    }
+
+    private static int getBatteryLevel(Intent intent) {
+        final int level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
+        final int scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, 0);
+        return scale == 0
+                ? -1 /*invalid battery level*/
+                : Math.round((level / (float) scale) * 100f);
+    }
+
+    private static void clearMemory() {
+        if (SystemClock.uptimeMillis() > CLEAR_MEMORY_THRESHOLD_MS) {
+            return;
+        }
+        final Handler mainHandler = new Handler(Looper.getMainLooper());
+        mainHandler.postDelayed(() -> {
+            System.gc();
+            System.runFinalization();
+            System.gc();
+            Log.w(TAG, "invoke clearMemory()");
+        }, CLEAR_MEMORY_DELAYED_MS);
+    }
+
+    private static SharedPreferences getSharedPreferences(Context context) {
+        return context
+                .getApplicationContext() // ensures we bind it with application
+                .createDeviceProtectedStorageContext()
+                .getSharedPreferences(PREF_FILE_NAME, Context.MODE_PRIVATE);
+    }
+
+    /** Returns the timestamp for 00:00 6 days before the calendar date. */
+    private static long getTimestampSixDaysAgo(Calendar calendar) {
+        Calendar startCalendar =
+                calendar == null ? Calendar.getInstance() : (Calendar) calendar.clone();
+        startCalendar.add(Calendar.DAY_OF_YEAR, -6);
+        startCalendar.set(Calendar.HOUR_OF_DAY, 0);
+        startCalendar.set(Calendar.MINUTE, 0);
+        startCalendar.set(Calendar.SECOND, 0);
+        startCalendar.set(Calendar.MILLISECOND, 0);
+        return startCalendar.getTimeInMillis();
+    }
+
 }
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageContentProviderTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageContentProviderTest.java
index 4f145c2..61d4efa 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageContentProviderTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageContentProviderTest.java
@@ -44,12 +44,7 @@
 /** Tests for {@link BatteryUsageContentProvider}. */
 @RunWith(RobolectricTestRunner.class)
 public final class BatteryUsageContentProviderTest {
-    private static final Uri VALID_BATTERY_STATE_CONTENT_URI =
-            new Uri.Builder()
-                    .scheme(ContentResolver.SCHEME_CONTENT)
-                    .authority(DatabaseUtils.AUTHORITY)
-                    .appendPath(DatabaseUtils.BATTERY_STATE_TABLE)
-                    .build();
+    private static final Uri VALID_BATTERY_STATE_CONTENT_URI = DatabaseUtils.BATTERY_CONTENT_URI;
 
     private Context mContext;
     private BatteryUsageContentProvider mProvider;
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtilsTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtilsTest.java
new file mode 100644
index 0000000..cb5255e
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtilsTest.java
@@ -0,0 +1,422 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.fuelgauge.batteryusage;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.database.MatrixCursor;
+import android.os.BatteryManager;
+import android.os.BatteryUsageStats;
+import android.os.UserHandle;
+import android.os.UserManager;
+
+import com.android.settings.testutils.BatteryTestUtils;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.Shadows;
+
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.List;
+import java.util.Map;
+
+@RunWith(RobolectricTestRunner.class)
+public final class DatabaseUtilsTest {
+
+    private Context mContext;
+
+    @Mock
+    private PackageManager mPackageManager;
+    @Mock private ContentResolver mMockContentResolver;
+    @Mock private ContentResolver mMockContentResolver2;
+    @Mock private BatteryUsageStats mBatteryUsageStats;
+    @Mock private BatteryEntry mMockBatteryEntry1;
+    @Mock private BatteryEntry mMockBatteryEntry2;
+    @Mock private BatteryEntry mMockBatteryEntry3;
+    @Mock private Context mMockContext;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mContext = spy(RuntimeEnvironment.application);
+        setProviderSetting(PackageManager.COMPONENT_ENABLED_STATE_ENABLED);
+        doReturn(mMockContentResolver2).when(mMockContext).getContentResolver();
+        doReturn(mMockContentResolver).when(mContext).getContentResolver();
+        doReturn(mPackageManager).when(mMockContext).getPackageManager();
+        doReturn(mPackageManager).when(mContext).getPackageManager();
+    }
+
+    @Test
+    public void isWorkProfile_defaultValue_returnFalse() {
+        assertThat(DatabaseUtils.isWorkProfile(mContext)).isFalse();
+    }
+
+    @Test
+    public void isWorkProfile_withManagedUser_returnTrue() {
+        BatteryTestUtils.setWorkProfile(mContext);
+        assertThat(DatabaseUtils.isWorkProfile(mContext)).isTrue();
+    }
+
+    @Test
+    public void isWorkProfile_withSystemUser_returnFalse() {
+        BatteryTestUtils.setWorkProfile(mContext);
+        Shadows.shadowOf(mContext.getSystemService(UserManager.class)).setIsSystemUser(true);
+
+        assertThat(DatabaseUtils.isWorkProfile(mContext)).isFalse();
+    }
+
+    @Test
+    public void isChartGraphEnabled_providerIsEnabled_returnTrue() {
+        setProviderSetting(PackageManager.COMPONENT_ENABLED_STATE_ENABLED);
+        assertThat(DatabaseUtils.isChartGraphEnabled(mContext)).isTrue();
+    }
+
+    @Test
+    public void isChartGraphEnabled_providerIsDisabled_returnFalse() {
+        setProviderSetting(PackageManager.COMPONENT_ENABLED_STATE_DISABLED);
+        assertThat(DatabaseUtils.isChartGraphEnabled(mContext)).isFalse();
+    }
+
+    @Test
+    public void isContentProviderEnabled_providerEnabled_returnsTrue() {
+        setProviderSetting(PackageManager.COMPONENT_ENABLED_STATE_ENABLED);
+        assertThat(DatabaseUtils.isContentProviderEnabled(mContext)).isTrue();
+    }
+
+    @Test
+    public void isContentProviderEnabled_providerDisabled_returnsFalse() {
+        setProviderSetting(PackageManager.COMPONENT_ENABLED_STATE_DISABLED);
+        assertThat(DatabaseUtils.isContentProviderEnabled(mContext)).isFalse();
+    }
+
+    @Test
+    public void sendBatteryEntryData_nullBatteryIntent_returnsNullValue() {
+        doReturn(null).when(mContext).registerReceiver(any(), any());
+        assertThat(
+                DatabaseUtils.sendBatteryEntryData(
+                        mContext, /*batteryEntryList=*/ null, mBatteryUsageStats))
+                .isNull();
+    }
+
+    @Test
+    public void sendBatteryEntryData_returnsExpectedList() {
+        doReturn(getBatteryIntent()).when(mContext).registerReceiver(any(), any());
+        // Configures the testing BatteryEntry data.
+        final List<BatteryEntry> batteryEntryList = new ArrayList<>();
+        batteryEntryList.add(mMockBatteryEntry1);
+        batteryEntryList.add(mMockBatteryEntry2);
+        batteryEntryList.add(mMockBatteryEntry3);
+        doReturn(0.0).when(mMockBatteryEntry1).getConsumedPower();
+        doReturn(0.5).when(mMockBatteryEntry2).getConsumedPower();
+        doReturn(0.0).when(mMockBatteryEntry3).getConsumedPower();
+        doReturn(1L).when(mMockBatteryEntry3).getTimeInForegroundMs();
+
+        final List<ContentValues> valuesList =
+                DatabaseUtils.sendBatteryEntryData(
+                        mContext, batteryEntryList, mBatteryUsageStats);
+
+        assertThat(valuesList).hasSize(2);
+        // Verifies the ContentValues content.
+        verifyContentValues(0.5, valuesList.get(0));
+        verifyContentValues(0.0, valuesList.get(1));
+        // Verifies the inserted ContentValues into content provider.
+        final ContentValues[] valuesArray =
+                new ContentValues[] {valuesList.get(0), valuesList.get(1)};
+        verify(mMockContentResolver).bulkInsert(
+                DatabaseUtils.BATTERY_CONTENT_URI, valuesArray);
+        verify(mMockContentResolver).notifyChange(
+                DatabaseUtils.BATTERY_CONTENT_URI, /*observer=*/ null);
+    }
+
+    @Test
+    public void sendBatteryEntryData_emptyBatteryEntryList_sendFakeDataIntoProvider() {
+        doReturn(getBatteryIntent()).when(mContext).registerReceiver(any(), any());
+
+        final List<ContentValues> valuesList =
+                DatabaseUtils.sendBatteryEntryData(
+                        mContext,
+                        new ArrayList<>(),
+                        mBatteryUsageStats);
+
+        assertThat(valuesList).hasSize(1);
+        verifyFakeContentValues(valuesList.get(0));
+        // Verifies the inserted ContentValues into content provider.
+        verify(mMockContentResolver).insert(any(), any());
+        verify(mMockContentResolver).notifyChange(
+                DatabaseUtils.BATTERY_CONTENT_URI, /*observer=*/ null);
+    }
+
+    @Test
+    public void sendBatteryEntryData_nullBatteryEntryList_sendFakeDataIntoProvider() {
+        doReturn(getBatteryIntent()).when(mContext).registerReceiver(any(), any());
+
+        final List<ContentValues> valuesList =
+                DatabaseUtils.sendBatteryEntryData(
+                        mContext,
+                        /*batteryEntryList=*/ null,
+                        mBatteryUsageStats);
+
+        assertThat(valuesList).hasSize(1);
+        verifyFakeContentValues(valuesList.get(0));
+        // Verifies the inserted ContentValues into content provider.
+        verify(mMockContentResolver).insert(any(), any());
+        verify(mMockContentResolver).notifyChange(
+                DatabaseUtils.BATTERY_CONTENT_URI, /*observer=*/ null);
+    }
+
+    @Test
+    public void sendBatteryEntryData_nullBatteryUsageStats_sendFakeDataIntoProvider() {
+        doReturn(getBatteryIntent()).when(mContext).registerReceiver(any(), any());
+
+        final List<ContentValues> valuesList =
+                DatabaseUtils.sendBatteryEntryData(
+                        mContext,
+                        /*batteryEntryList=*/ null,
+                        /*batteryUsageStats=*/ null);
+
+        assertThat(valuesList).hasSize(1);
+        verifyFakeContentValues(valuesList.get(0));
+        // Verifies the inserted ContentValues into content provider.
+        verify(mMockContentResolver).insert(any(), any());
+        verify(mMockContentResolver).notifyChange(
+                DatabaseUtils.BATTERY_CONTENT_URI, /*observer=*/ null);
+    }
+
+    @Test
+    public void getHistoryMapSinceLastFullCharge_providerIsDisabled_returnNull() {
+        setProviderSetting(PackageManager.COMPONENT_ENABLED_STATE_DISABLED);
+        assertThat(DatabaseUtils.getHistoryMapSinceLastFullCharge(
+                mContext, /*calendar=*/ null)).isNull();
+    }
+
+    @Test
+    public void getHistoryMapSinceLastFullCharge_emptyCursorContent_returnEmptyMap() {
+        final MatrixCursor cursor = new MatrixCursor(
+                new String[] {
+                        BatteryHistEntry.KEY_UID,
+                        BatteryHistEntry.KEY_USER_ID,
+                        BatteryHistEntry.KEY_TIMESTAMP});
+        doReturn(cursor).when(mMockContentResolver).query(any(), any(), any(), any());
+
+        assertThat(DatabaseUtils.getHistoryMapSinceLastFullCharge(
+                mContext, /*calendar=*/ null)).isEmpty();
+    }
+
+    @Test
+    public void getHistoryMapSinceLastFullCharge_nullCursor_returnEmptyMap() {
+        doReturn(null).when(mMockContentResolver).query(any(), any(), any(), any());
+        assertThat(DatabaseUtils.getHistoryMapSinceLastFullCharge(
+                mContext, /*calendar=*/ null)).isEmpty();
+    }
+
+    @Test
+    public void getHistoryMapSinceLastFullCharge_returnExpectedMap() {
+        final Long timestamp1 = Long.valueOf(1001L);
+        final Long timestamp2 = Long.valueOf(1002L);
+        final MatrixCursor cursor = getMatrixCursor();
+        doReturn(cursor).when(mMockContentResolver).query(any(), any(), any(), any());
+        // Adds fake data into the cursor.
+        cursor.addRow(new Object[] {
+                "app name1", timestamp1, 1, ConvertUtils.CONSUMER_TYPE_UID_BATTERY});
+        cursor.addRow(new Object[] {
+                "app name2", timestamp2, 2, ConvertUtils.CONSUMER_TYPE_UID_BATTERY});
+        cursor.addRow(new Object[] {
+                "app name3", timestamp2, 3, ConvertUtils.CONSUMER_TYPE_UID_BATTERY});
+        cursor.addRow(new Object[] {
+                "app name4", timestamp2, 4, ConvertUtils.CONSUMER_TYPE_UID_BATTERY});
+
+        final Map<Long, Map<String, BatteryHistEntry>> batteryHistMap =
+                DatabaseUtils.getHistoryMapSinceLastFullCharge(
+                        mContext, /*calendar=*/ null);
+
+        assertThat(batteryHistMap).hasSize(2);
+        // Verifies the BatteryHistEntry data for timestamp1.
+        Map<String, BatteryHistEntry> batteryMap = batteryHistMap.get(timestamp1);
+        assertThat(batteryMap).hasSize(1);
+        assertThat(batteryMap.get("1").mAppLabel).isEqualTo("app name1");
+        // Verifies the BatteryHistEntry data for timestamp2.
+        batteryMap = batteryHistMap.get(timestamp2);
+        assertThat(batteryMap).hasSize(3);
+        assertThat(batteryMap.get("2").mAppLabel).isEqualTo("app name2");
+        assertThat(batteryMap.get("3").mAppLabel).isEqualTo("app name3");
+        assertThat(batteryMap.get("4").mAppLabel).isEqualTo("app name4");
+    }
+
+    @Test
+    public void getHistoryMapSinceLastFullCharge_withWorkProfile_returnExpectedMap()
+            throws PackageManager.NameNotFoundException {
+        doReturn("com.fake.package").when(mContext).getPackageName();
+        doReturn(mMockContext).when(mContext).createPackageContextAsUser(
+                "com.fake.package", /*flags=*/ 0, UserHandle.OWNER);
+        BatteryTestUtils.setWorkProfile(mContext);
+        doReturn(getMatrixCursor()).when(mMockContentResolver2)
+                .query(any(), any(), any(), any());
+        doReturn(null).when(mMockContentResolver).query(any(), any(), any(), any());
+
+        final Map<Long, Map<String, BatteryHistEntry>> batteryHistMap =
+                DatabaseUtils.getHistoryMapSinceLastFullCharge(
+                        mContext, /*calendar=*/ null);
+
+        assertThat(batteryHistMap).isEmpty();
+    }
+
+    @Test
+    public void saveLastFullChargeTimestampPref_notFullCharge_returnsFalse() {
+        DatabaseUtils.saveLastFullChargeTimestampPref(
+                mContext,
+                BatteryManager.BATTERY_STATUS_UNKNOWN,
+                /* level */ 10,
+                /* timestamp */ 1);
+        assertThat(DatabaseUtils.getLastFullChargeTimestampPref(mContext)).isEqualTo(0);
+    }
+
+    @Test
+    public void saveLastFullChargeTimestampPref_fullStatus_returnsTrue() {
+        long expectedTimestamp = 1;
+        DatabaseUtils.saveLastFullChargeTimestampPref(
+                mContext,
+                BatteryManager.BATTERY_STATUS_FULL,
+                /* level */ 10,
+                /* timestamp */ expectedTimestamp);
+        assertThat(DatabaseUtils.getLastFullChargeTimestampPref(mContext))
+                .isEqualTo(expectedTimestamp);
+    }
+
+    @Test
+    public void saveLastFullChargeTimestampPref_level100_returnsTrue() {
+        long expectedTimestamp = 1;
+        DatabaseUtils.saveLastFullChargeTimestampPref(
+                mContext,
+                BatteryManager.BATTERY_STATUS_UNKNOWN,
+                /* level */ 100,
+                /* timestamp */ expectedTimestamp);
+        assertThat(DatabaseUtils.getLastFullChargeTimestampPref(mContext))
+                .isEqualTo(expectedTimestamp);
+    }
+
+    @Test
+    public void getStartTimestampForLastFullCharge_noTimestampPreference_returnsSixDaysAgo() {
+        Calendar currentCalendar = Calendar.getInstance();
+        currentCalendar.set(2022, 6, 5, 6, 30, 50); // 2022-07-05 06:30:50
+        Calendar expectedCalendar = Calendar.getInstance();
+        expectedCalendar.set(2022, 5, 29, 0, 0, 0); // 2022-06-29 00:00:00
+        expectedCalendar.set(Calendar.MILLISECOND, 0);
+
+        assertThat(DatabaseUtils.getStartTimestampForLastFullCharge(mContext, currentCalendar))
+                .isEqualTo(expectedCalendar.getTimeInMillis());
+    }
+
+    @Test
+    public void getStartTimestampForLastFullCharge_lastFullChargeEarlier_returnsSixDaysAgo() {
+        Calendar lastFullCalendar = Calendar.getInstance();
+        lastFullCalendar.set(2021, 11, 25, 6, 30, 50); // 2021-12-25 06:30:50
+        DatabaseUtils.saveLastFullChargeTimestampPref(
+                mContext,
+                BatteryManager.BATTERY_STATUS_UNKNOWN,
+                /* level */ 100,
+                /* timestamp */ lastFullCalendar.getTimeInMillis());
+        Calendar currentCalendar = Calendar.getInstance();
+        currentCalendar.set(2022, 0, 2, 6, 30, 50); // 2022-01-02 06:30:50
+        Calendar expectedCalendar = Calendar.getInstance();
+        expectedCalendar.set(2021, 11, 27, 0, 0, 0); // 2021-12-27 00:00:00
+        expectedCalendar.set(Calendar.MILLISECOND, 0);
+
+        assertThat(DatabaseUtils.getStartTimestampForLastFullCharge(mContext, currentCalendar))
+                .isEqualTo(expectedCalendar.getTimeInMillis());
+    }
+
+    @Test
+    public void getStartTimestampForLastFullCharge_lastFullChargeLater_returnsLastFullCharge() {
+        Calendar lastFullCalendar = Calendar.getInstance();
+        lastFullCalendar.set(2022, 6, 1, 6, 30, 50); // 2022-07-01 06:30:50
+        long expectedTimestamp = lastFullCalendar.getTimeInMillis();
+        DatabaseUtils.saveLastFullChargeTimestampPref(
+                mContext,
+                BatteryManager.BATTERY_STATUS_UNKNOWN,
+                /* level */ 100,
+                /* timestamp */ expectedTimestamp);
+        Calendar currentCalendar = Calendar.getInstance();
+        currentCalendar.set(2022, 6, 5, 6, 30, 50); // 2022-07-05 06:30:50
+
+        assertThat(DatabaseUtils.getStartTimestampForLastFullCharge(mContext, currentCalendar))
+                .isEqualTo(expectedTimestamp);
+    }
+
+    private void setProviderSetting(int value) {
+        when(mPackageManager.getComponentEnabledSetting(
+                new ComponentName(
+                        DatabaseUtils.SETTINGS_PACKAGE_PATH,
+                        DatabaseUtils.BATTERY_PROVIDER_CLASS_PATH)))
+                .thenReturn(value);
+    }
+
+    private static void verifyContentValues(double consumedPower, ContentValues values) {
+        assertThat(values.getAsDouble(BatteryHistEntry.KEY_CONSUME_POWER))
+                .isEqualTo(consumedPower);
+        assertThat(values.getAsInteger(BatteryHistEntry.KEY_BATTERY_LEVEL)).isEqualTo(20);
+        assertThat(values.getAsInteger(BatteryHistEntry.KEY_BATTERY_STATUS))
+                .isEqualTo(BatteryManager.BATTERY_STATUS_FULL);
+        assertThat(values.getAsInteger(BatteryHistEntry.KEY_BATTERY_HEALTH))
+                .isEqualTo(BatteryManager.BATTERY_HEALTH_COLD);
+    }
+
+    private static void verifyFakeContentValues(ContentValues values) {
+        assertThat(values.getAsInteger("batteryLevel")).isEqualTo(20);
+        assertThat(values.getAsInteger("batteryStatus"))
+                .isEqualTo(BatteryManager.BATTERY_STATUS_FULL);
+        assertThat(values.getAsInteger("batteryHealth"))
+                .isEqualTo(BatteryManager.BATTERY_HEALTH_COLD);
+        assertThat(values.getAsString("packageName"))
+                .isEqualTo(ConvertUtils.FAKE_PACKAGE_NAME);
+    }
+
+    private static Intent getBatteryIntent() {
+        final Intent intent = new Intent(Intent.ACTION_BATTERY_CHANGED);
+        intent.putExtra(BatteryManager.EXTRA_LEVEL, 20);
+        intent.putExtra(BatteryManager.EXTRA_SCALE, 100);
+        intent.putExtra(BatteryManager.EXTRA_STATUS, BatteryManager.BATTERY_STATUS_FULL);
+        intent.putExtra(BatteryManager.EXTRA_HEALTH, BatteryManager.BATTERY_HEALTH_COLD);
+        return intent;
+    }
+
+    private static MatrixCursor getMatrixCursor() {
+        return new MatrixCursor(
+                new String[] {
+                        BatteryHistEntry.KEY_APP_LABEL,
+                        BatteryHistEntry.KEY_TIMESTAMP,
+                        BatteryHistEntry.KEY_UID,
+                        BatteryHistEntry.KEY_CONSUMER_TYPE});
+    }
+}