diff --git a/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtils.java b/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtils.java index 4ad1ac3df53..0e2a81f70f4 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> 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> 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 sendBatteryEntryData( + Context context, + List 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 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> 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> 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 4f145c24bc6..61d4efad5b7 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 @@ import java.util.List; /** 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 00000000000..cb5255e0446 --- /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 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 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 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 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 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> batteryHistMap = + DatabaseUtils.getHistoryMapSinceLastFullCharge( + mContext, /*calendar=*/ null); + + assertThat(batteryHistMap).hasSize(2); + // Verifies the BatteryHistEntry data for timestamp1. + Map 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> 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}); + } +}