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