diff --git a/Android.bp b/Android.bp index ea2c983ae3f..4d07913b11a 100644 --- a/Android.bp +++ b/Android.bp @@ -81,6 +81,7 @@ android_library { "jsr305", "net-utils-framework-common", "app-usage-event-protos-lite", + "battery-event-protos-lite", "settings-contextual-card-protos-lite", "settings-log-bridge-protos-lite", "settings-telephony-protos-lite", diff --git a/AndroidManifest.xml b/AndroidManifest.xml index ccf002a8e1d..21b3fa4a5b3 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -3110,6 +3110,7 @@ + diff --git a/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageBroadcastReceiver.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageBroadcastReceiver.java index a437877df47..07a97dcc25e 100644 --- a/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageBroadcastReceiver.java +++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageBroadcastReceiver.java @@ -30,6 +30,8 @@ import com.android.settings.overlay.FeatureFactory; import com.android.settingslib.fuelgauge.BatteryStatus; import java.time.Duration; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; /** A {@link BatteryUsageBroadcastReceiver} for battery usage data requesting. */ public final class BatteryUsageBroadcastReceiver extends BroadcastReceiver { @@ -37,7 +39,10 @@ public final class BatteryUsageBroadcastReceiver extends BroadcastReceiver { /** An intent action to request Settings to clear cache data. */ public static final String ACTION_CLEAR_BATTERY_CACHE_DATA = "com.android.settings.battery.action.CLEAR_BATTERY_CACHE_DATA"; - /** An intent action to request Settings to clear cache data. */ + /** An intent action for power is plugging. */ + public static final String ACTION_BATTERY_PLUGGING = + "com.android.settings.battery.action.ACTION_BATTERY_PLUGGING"; + /** An intent action for power is unplugging. */ public static final String ACTION_BATTERY_UNPLUGGING = "com.android.settings.battery.action.ACTION_BATTERY_UNPLUGGING"; @@ -49,6 +54,8 @@ public final class BatteryUsageBroadcastReceiver extends BroadcastReceiver { @VisibleForTesting boolean mFetchBatteryUsageData = false; + private final ExecutorService mExecutor = Executors.newSingleThreadExecutor(); + @Override public void onReceive(Context context, Intent intent) { if (intent == null || intent.getAction() == null) { @@ -70,7 +77,11 @@ public final class BatteryUsageBroadcastReceiver extends BroadcastReceiver { tryToFetchUsageData(context); } break; + case ACTION_BATTERY_PLUGGING: + sendBatteryEventData(context, BatteryEventType.POWER_CONNECTED); + break; case ACTION_BATTERY_UNPLUGGING: + sendBatteryEventData(context, BatteryEventType.POWER_DISCONNECTED); // Only when fullChargeIntentAction is ACTION_POWER_DISCONNECTED, // ACTION_BATTERY_UNPLUGGING will be considered as the full charge event and then // start usage events fetching. @@ -106,4 +117,12 @@ public final class BatteryUsageBroadcastReceiver extends BroadcastReceiver { mFetchBatteryUsageData = true; BatteryUsageDataLoader.enqueueWork(context, /*isFullChargeStart=*/ true); } + + private void sendBatteryEventData(Context context, BatteryEventType batteryEventType) { + final long timestamp = System.currentTimeMillis(); + final Intent intent = BatteryUtils.getBatteryIntent(context); + final int batteryLevel = BatteryStatus.getBatteryLevel(intent); + mExecutor.execute(() -> DatabaseUtils.sendBatteryEventData(context, + ConvertUtils.convertToBatteryEvent(timestamp, batteryEventType, batteryLevel))); + } } diff --git a/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageContentProvider.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageContentProvider.java index 42937e5688a..1b2d4cd0406 100644 --- a/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageContentProvider.java +++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageContentProvider.java @@ -31,6 +31,8 @@ import androidx.annotation.VisibleForTesting; import com.android.settings.fuelgauge.batteryusage.db.AppUsageEventDao; import com.android.settings.fuelgauge.batteryusage.db.AppUsageEventEntity; +import com.android.settings.fuelgauge.batteryusage.db.BatteryEventDao; +import com.android.settings.fuelgauge.batteryusage.db.BatteryEventEntity; import com.android.settings.fuelgauge.batteryusage.db.BatteryState; import com.android.settings.fuelgauge.batteryusage.db.BatteryStateDao; import com.android.settings.fuelgauge.batteryusage.db.BatteryStateDatabase; @@ -52,6 +54,7 @@ public class BatteryUsageContentProvider extends ContentProvider { private static final int BATTERY_STATE_CODE = 1; private static final int APP_USAGE_LATEST_TIMESTAMP_CODE = 2; private static final int APP_USAGE_EVENT_CODE = 3; + private static final int BATTERY_EVENT_CODE = 4; private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); @@ -68,11 +71,16 @@ public class BatteryUsageContentProvider extends ContentProvider { DatabaseUtils.AUTHORITY, /*path=*/ DatabaseUtils.APP_USAGE_EVENT_TABLE, /*code=*/ APP_USAGE_EVENT_CODE); + sUriMatcher.addURI( + DatabaseUtils.AUTHORITY, + /*path=*/ DatabaseUtils.BATTERY_EVENT_TABLE, + /*code=*/ BATTERY_EVENT_CODE); } private Clock mClock; private BatteryStateDao mBatteryStateDao; private AppUsageEventDao mAppUsageEventDao; + private BatteryEventDao mBatteryEventDao; @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) public void setClock(Clock clock) { @@ -88,6 +96,7 @@ public class BatteryUsageContentProvider extends ContentProvider { mClock = Clock.systemUTC(); mBatteryStateDao = BatteryStateDatabase.getInstance(getContext()).batteryStateDao(); mAppUsageEventDao = BatteryStateDatabase.getInstance(getContext()).appUsageEventDao(); + mBatteryEventDao = BatteryStateDatabase.getInstance(getContext()).batteryEventDao(); Log.w(TAG, "create content provider from " + getCallingPackage()); return true; } @@ -107,6 +116,8 @@ public class BatteryUsageContentProvider extends ContentProvider { return getAppUsageEvents(uri); case APP_USAGE_LATEST_TIMESTAMP_CODE: return getAppUsageLatestTimestamp(uri); + case BATTERY_EVENT_CODE: + return getBatteryEvents(uri); default: throw new IllegalArgumentException("unknown URI: " + uri); } @@ -138,6 +149,14 @@ public class BatteryUsageContentProvider extends ContentProvider { Log.e(TAG, "insert() from:" + uri + " error:" + e); return null; } + case BATTERY_EVENT_CODE: + try { + mBatteryEventDao.insert(BatteryEventEntity.create(contentValues)); + return uri; + } catch (RuntimeException e) { + Log.e(TAG, "insert() from:" + uri + " error:" + e); + return null; + } default: throw new IllegalArgumentException("unknown URI: " + uri); } @@ -190,7 +209,6 @@ public class BatteryUsageContentProvider extends ContentProvider { } Log.w(TAG, "query app usage events in " + (mClock.millis() - timestamp) + "/ms"); return cursor; - } private Cursor getAppUsageLatestTimestamp(Uri uri) { @@ -210,6 +228,19 @@ public class BatteryUsageContentProvider extends ContentProvider { return cursor; } + private Cursor getBatteryEvents(Uri uri) { + final long queryTimestamp = getQueryTimestamp(uri); + final long timestamp = mClock.millis(); + Cursor cursor = null; + try { + cursor = mBatteryEventDao.getAllAfter(queryTimestamp); + } catch (RuntimeException e) { + Log.e(TAG, "query() from:" + uri + " error:" + e); + } + Log.w(TAG, "query app usage events in " + (mClock.millis() - timestamp) + "/ms"); + return cursor; + } + // If URI contains query parameter QUERY_KEY_USERID, use the value directly. // Otherwise, return null. private List getQueryUserIds(Uri uri) { diff --git a/src/com/android/settings/fuelgauge/batteryusage/ConvertUtils.java b/src/com/android/settings/fuelgauge/batteryusage/ConvertUtils.java index fcf934ff8e3..c6c548f0dce 100644 --- a/src/com/android/settings/fuelgauge/batteryusage/ConvertUtils.java +++ b/src/com/android/settings/fuelgauge/batteryusage/ConvertUtils.java @@ -38,6 +38,7 @@ import androidx.annotation.VisibleForTesting; import com.android.settings.fuelgauge.BatteryUtils; import com.android.settings.fuelgauge.batteryusage.db.AppUsageEventEntity; +import com.android.settings.fuelgauge.batteryusage.db.BatteryEventEntity; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -125,6 +126,15 @@ public final class ConvertUtils { return values; } + /** Converts {@link BatteryEvent} to content values */ + public static ContentValues convertBatteryEventToContentValues(final BatteryEvent event) { + final ContentValues values = new ContentValues(); + values.put(BatteryEventEntity.KEY_TIMESTAMP, event.getTimestamp()); + values.put(BatteryEventEntity.KEY_BATTERY_EVENT_TYPE, event.getType().getNumber()); + values.put(BatteryEventEntity.KEY_BATTERY_LEVEL, event.getBatteryLevel()); + return values; + } + /** Gets the encoded string from {@link BatteryInformation} instance. */ public static String convertBatteryInformationToString( final BatteryInformation batteryInformation) { @@ -237,6 +247,29 @@ public final class ConvertUtils { return eventBuilder.build(); } + /** Converts to {@link BatteryEvent} from {@link BatteryEventType} */ + public static BatteryEvent convertToBatteryEvent( + long timestamp, BatteryEventType type, int batteryLevel) { + final BatteryEvent.Builder eventBuilder = BatteryEvent.newBuilder(); + eventBuilder.setTimestamp(timestamp); + eventBuilder.setType(type); + eventBuilder.setBatteryLevel(batteryLevel); + return eventBuilder.build(); + } + + /** Converts to {@link BatteryEvent} from {@link Cursor} */ + public static BatteryEvent convertToBatteryEventFromCursor(final Cursor cursor) { + final BatteryEvent.Builder eventBuilder = BatteryEvent.newBuilder(); + eventBuilder.setTimestamp(getLongFromCursor(cursor, BatteryEventEntity.KEY_TIMESTAMP)); + eventBuilder.setType( + BatteryEventType.forNumber( + getIntegerFromCursor( + cursor, BatteryEventEntity.KEY_BATTERY_EVENT_TYPE))); + eventBuilder.setBatteryLevel( + getIntegerFromCursor(cursor, BatteryEventEntity.KEY_BATTERY_LEVEL)); + return eventBuilder.build(); + } + /** Converts UTC timestamp to local time string for logging only, so use the US locale for * better readability in debugging. */ public static String utcToLocalTimeForLogging(long timestamp) { diff --git a/src/com/android/settings/fuelgauge/batteryusage/DataProcessManager.java b/src/com/android/settings/fuelgauge/batteryusage/DataProcessManager.java index 1f25ae0fa8a..6b007de8c61 100644 --- a/src/com/android/settings/fuelgauge/batteryusage/DataProcessManager.java +++ b/src/com/android/settings/fuelgauge/batteryusage/DataProcessManager.java @@ -72,6 +72,8 @@ public class DataProcessManager { private final Handler mHandler; private final DataProcessor.UsageMapAsyncResponse mCallbackFunction; + private final List mAppUsageEventList = new ArrayList<>(); + private final List mBatteryEventList = new ArrayList<>(); private Context mContext; private UserManager mUserManager; @@ -84,12 +86,12 @@ public class DataProcessManager { private boolean mIsCurrentBatteryHistoryLoaded = false; private boolean mIsCurrentAppUsageLoaded = false; private boolean mIsDatabaseAppUsageLoaded = false; + private boolean mIsBatteryEventLoaded = false; // Used to identify whether screen-on time data should be shown in the UI. private boolean mShowScreenOnTime = true; // Used to identify whether battery level data should be shown in the UI. private boolean mShowBatteryLevel = true; - private List mAppUsageEventList = new ArrayList<>(); /** * The indexed {@link AppUsagePeriod} list data for each corresponding time slot. *

{@code Long} stands for the userId.

@@ -146,6 +148,8 @@ public class DataProcessManager { loadDatabaseAppUsageList(); // Loads the latest app usage list from the service. loadCurrentAppUsageList(); + // Loads the battery event list from database. + loadBatteryEventList(); } else { // If there is no battery level data, only load the battery history data from service // and show it as the app list directly. @@ -174,6 +178,11 @@ public class DataProcessManager { return mIsDatabaseAppUsageLoaded; } + @VisibleForTesting + boolean getIsBatteryEventLoaded() { + return mIsBatteryEventLoaded; + } + @VisibleForTesting boolean getIsCurrentBatteryHistoryLoaded() { return mIsCurrentBatteryHistoryLoaded; @@ -290,7 +299,7 @@ public class DataProcessManager { return null; } final long startTime = System.currentTimeMillis(); - // Loads the current battery usage data from the battery stats service. + // Loads the app usage data from the database. final List appUsageEventList = DatabaseUtils.getAppUsageEventForUsers( mContext, Calendar.getInstance(), getCurrentUserIds(), @@ -314,6 +323,35 @@ public class DataProcessManager { }.execute(); } + private void loadBatteryEventList() { + new AsyncTask>() { + @Override + protected List doInBackground(Void... voids) { + final long startTime = System.currentTimeMillis(); + // Loads the battery event data from the database. + final List batteryEventList = + DatabaseUtils.getBatteryEvents( + mContext, Calendar.getInstance(), mRawStartTimestamp); + Log.d(TAG, String.format("execute loadBatteryEventList size=%d in %d/ms", + batteryEventList.size(), (System.currentTimeMillis() - startTime))); + return batteryEventList; + } + + @Override + protected void onPostExecute( + final List batteryEventList) { + if (batteryEventList == null || batteryEventList.isEmpty()) { + Log.d(TAG, "batteryEventList is null or empty"); + } else { + mBatteryEventList.clear(); + mBatteryEventList.addAll(batteryEventList); + } + mIsBatteryEventLoaded = true; + tryToProcessAppUsageData(); + } + }.execute(); + } + private void loadAndApplyBatteryMapFromServiceOnly() { new AsyncTask() { @Override @@ -344,9 +382,8 @@ public class DataProcessManager { } private void tryToProcessAppUsageData() { - // Only when all app usage events has been loaded, start processing app usage data to an - // intermediate result for further use. - if (!mIsCurrentAppUsageLoaded || !mIsDatabaseAppUsageLoaded) { + // Ignore processing the data if any required data is not loaded. + if (!mIsCurrentAppUsageLoaded || !mIsDatabaseAppUsageLoaded || !mIsBatteryEventLoaded) { return; } processAppUsageData(); @@ -360,16 +397,16 @@ public class DataProcessManager { } // Generates the indexed AppUsagePeriod list data for each corresponding time slot for // further use. - mAppUsagePeriodMap = DataProcessor.generateAppUsagePeriodMap( - mRawStartTimestamp, mHourlyBatteryLevelsPerDay, mAppUsageEventList); + mAppUsagePeriodMap = DataProcessor.generateAppUsagePeriodMap(mRawStartTimestamp, + mHourlyBatteryLevelsPerDay, mAppUsageEventList, mBatteryEventList); } private void tryToGenerateFinalDataAndApplyCallback() { - // Only when both battery history data and app usage events data has been loaded, start the - // final data processing. + // Ignore processing the data if any required data is not loaded. if (!mIsCurrentBatteryHistoryLoaded || !mIsCurrentAppUsageLoaded - || !mIsDatabaseAppUsageLoaded) { + || !mIsDatabaseAppUsageLoaded + || !mIsBatteryEventLoaded) { return; } generateFinalDataAndApplyCallback(); diff --git a/src/com/android/settings/fuelgauge/batteryusage/DataProcessor.java b/src/com/android/settings/fuelgauge/batteryusage/DataProcessor.java index 38d4e69c66e..6270eb3f8a1 100644 --- a/src/com/android/settings/fuelgauge/batteryusage/DataProcessor.java +++ b/src/com/android/settings/fuelgauge/batteryusage/DataProcessor.java @@ -94,6 +94,14 @@ public final class DataProcessor { @VisibleForTesting static final int SELECTED_INDEX_ALL = BatteryChartViewModel.SELECTED_INDEX_ALL; + @VisibleForTesting + static final Comparator APP_USAGE_EVENT_TIMESTAMP_COMPARATOR = + Comparator.comparing(AppUsageEvent::getTimestamp); + + @VisibleForTesting + static final Comparator BATTERY_EVENT_TIMESTAMP_COMPARATOR = + Comparator.comparing(BatteryEvent::getTimestamp); + @VisibleForTesting static boolean sDebug = false; @@ -110,8 +118,6 @@ public final class DataProcessor { public static final String CURRENT_TIME_BATTERY_HISTORY_PLACEHOLDER = "CURRENT_TIME_BATTERY_HISTORY_PLACEHOLDER"; - public static final Comparator TIMESTAMP_COMPARATOR = - Comparator.comparing(AppUsageEvent::getTimestamp); /** A callback listener when battery usage loading async task is executed. */ public interface UsageMapAsyncResponse { @@ -266,14 +272,16 @@ public final class DataProcessor { generateAppUsagePeriodMap( final long rawStartTimestamp, final List hourlyBatteryLevelsPerDay, - final List appUsageEventList) { + final List appUsageEventList, + final List batteryEventList) { if (appUsageEventList.isEmpty()) { Log.w(TAG, "appUsageEventList is empty"); return null; } - // Sorts the appUsageEventList in ascending order based on the timestamp before - // distribution. - Collections.sort(appUsageEventList, TIMESTAMP_COMPARATOR); + // Sorts the appUsageEventList and batteryEventList in ascending order based on the + // timestamp before distribution. + Collections.sort(appUsageEventList, APP_USAGE_EVENT_TIMESTAMP_COMPARATOR); + Collections.sort(batteryEventList, BATTERY_EVENT_TIMESTAMP_COMPARATOR); final Map>>>> resultMap = new ArrayMap<>(); @@ -309,8 +317,8 @@ public final class DataProcessor { // The value could be null when there is no data in the hourly slot. dailyMap.put( hourlyIndex, - buildAppUsagePeriodList( - hourlyAppUsageEventList, startTimestamp, endTimestamp)); + buildAppUsagePeriodList(hourlyAppUsageEventList, batteryEventList, + startTimestamp, endTimestamp)); } } return resultMap; @@ -717,7 +725,8 @@ public final class DataProcessor { @VisibleForTesting @Nullable static Map>> buildAppUsagePeriodList( - final List allAppUsageEvents, final long startTime, final long endTime) { + final List allAppUsageEvents, final List batteryEventList, + final long startTime, final long endTime) { if (allAppUsageEvents.isEmpty()) { return null; } @@ -763,12 +772,14 @@ public final class DataProcessor { usageEvents.addAll(deviceEvents); // Sorts the usageEvents in ascending order based on the timestamp before computing the // period. - Collections.sort(usageEvents, TIMESTAMP_COMPARATOR); + Collections.sort(usageEvents, APP_USAGE_EVENT_TIMESTAMP_COMPARATOR); // A package might have multiple instances. Computes the usage period per instance id // and then merges them into the same user-package map. final List usagePeriodList = - buildAppUsagePeriodListPerInstance(usageEvents, startTime, endTime); + excludePowerConnectedTimeFromAppUsagePeriodList( + buildAppUsagePeriodListPerInstance(usageEvents, startTime, endTime), + batteryEventList); if (!usagePeriodList.isEmpty()) { addToUsagePeriodMap(allUsagePeriods, usagePeriodList, eventUserId, packageName); } @@ -836,6 +847,53 @@ public final class DataProcessor { return usagePeriodList; } + @VisibleForTesting + static List excludePowerConnectedTimeFromAppUsagePeriodList( + final List usagePeriodList, + final List batteryEventList) { + final List resultList = new ArrayList<>(); + for (AppUsagePeriod inputPeriod : usagePeriodList) { + long lastStartTime = inputPeriod.getStartTime(); + for (BatteryEvent batteryEvent : batteryEventList) { + if (batteryEvent.getTimestamp() < inputPeriod.getStartTime()) { + // Because the batteryEventList has been sorted, here is to mark the power + // connection state when the usage period starts. If power is connected when + // the usage period starts, the starting period will be ignored; otherwise it + // will be added. + if (batteryEvent.getType() == BatteryEventType.POWER_CONNECTED) { + lastStartTime = 0; + } else if (batteryEvent.getType() == BatteryEventType.POWER_DISCONNECTED) { + lastStartTime = inputPeriod.getStartTime(); + } + continue; + } + if (batteryEvent.getTimestamp() > inputPeriod.getEndTime()) { + // Because the batteryEventList has been sorted, if any event is already after + // the end time, all the following events should be able to drop directly. + break; + } + + if (batteryEvent.getType() == BatteryEventType.POWER_CONNECTED + && lastStartTime != 0) { + resultList.add(AppUsagePeriod.newBuilder() + .setStartTime(lastStartTime) + .setEndTime(batteryEvent.getTimestamp()) + .build()); + lastStartTime = 0; + } else if (batteryEvent.getType() == BatteryEventType.POWER_DISCONNECTED) { + lastStartTime = batteryEvent.getTimestamp(); + } + } + if (lastStartTime != 0) { + resultList.add(AppUsagePeriod.newBuilder() + .setStartTime(lastStartTime) + .setEndTime(inputPeriod.getEndTime()) + .build()); + } + } + return resultList; + } + @VisibleForTesting static long getScreenOnTime( final Map>> appUsageMap, diff --git a/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtils.java b/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtils.java index ef1434cb4a0..84a39bb811f 100644 --- a/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtils.java +++ b/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtils.java @@ -64,10 +64,12 @@ public final class DatabaseUtils { /** 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 table name for app usage events. */ public static final String APP_USAGE_EVENT_TABLE = "AppUsageEvent"; + /** A table name for battery events. */ + public static final String BATTERY_EVENT_TABLE = "BatteryEvent"; + /** A table name for battery usage history. */ + public static final String BATTERY_STATE_TABLE = "BatteryState"; /** A path name for app usage latest timestamp query. */ public static final String APP_USAGE_LATEST_TIMESTAMP_PATH = "appUsageLatestTimestamp"; /** A class name for battery usage data provider. */ @@ -84,13 +86,6 @@ public final class DatabaseUtils { */ public static final long USAGE_QUERY_BUFFER_HOURS = Duration.ofHours(3).toMillis(); - /** 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(); /** A content URI to access app usage events data. */ public static final Uri APP_USAGE_EVENT_URI = new Uri.Builder() @@ -98,6 +93,20 @@ public final class DatabaseUtils { .authority(AUTHORITY) .appendPath(APP_USAGE_EVENT_TABLE) .build(); + /** A content URI to access battery events data. */ + public static final Uri BATTERY_EVENT_URI = + new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(AUTHORITY) + .appendPath(BATTERY_EVENT_TABLE) + .build(); + /** 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(); // For testing only. @VisibleForTesting @@ -106,6 +115,8 @@ public final class DatabaseUtils { static Supplier sFakeAppUsageEventSupplier; @VisibleForTesting static Supplier sFakeAppUsageLatestTimestampSupplier; + @VisibleForTesting + static Supplier sFakeBatteryEventSupplier; private DatabaseUtils() { } @@ -176,6 +187,32 @@ public final class DatabaseUtils { return appUsageEventList; } + /** Returns the battery event data since the query timestamp in battery event table. */ + public static List getBatteryEvents( + Context context, + final Calendar calendar, + final long rawStartTimestamp) { + final long startTime = System.currentTimeMillis(); + final long sixDaysAgoTimestamp = getTimestampSixDaysAgo(calendar); + final long queryTimestamp = Math.max(rawStartTimestamp, sixDaysAgoTimestamp); + Log.d(TAG, "getBatteryEvents for timestamp: " + queryTimestamp); + // Builds the content uri everytime to avoid cache. + final Uri batteryEventUri = + new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(AUTHORITY) + .appendPath(BATTERY_EVENT_TABLE) + .appendQueryParameter( + QUERY_KEY_TIMESTAMP, Long.toString(queryTimestamp)) + .build(); + + final List batteryEventList = + loadBatteryEventsFromContentProvider(context, batteryEventUri); + Log.d(TAG, String.format("getBatteryEvents size=%d in %d/ms", batteryEventList.size(), + (System.currentTimeMillis() - startTime))); + return batteryEventList; + } + /** Long: for timestamp and String: for BatteryHistEntry.getKey() */ public static Map> getHistoryMapSinceLastFullCharge( Context context, Calendar calendar) { @@ -210,8 +247,9 @@ public final class DatabaseUtils { try { final BatteryStateDatabase database = BatteryStateDatabase .getInstance(context.getApplicationContext()); - database.batteryStateDao().clearAll(); database.appUsageEventDao().clearAll(); + database.batteryEventDao().clearAll(); + database.batteryStateDao().clearAll(); } catch (RuntimeException e) { Log.e(TAG, "clearAll() failed", e); } @@ -226,8 +264,9 @@ public final class DatabaseUtils { .getInstance(context.getApplicationContext()); final long earliestTimestamp = Clock.systemUTC().millis() - Duration.ofDays(DATA_RETENTION_INTERVAL_DAY).toMillis(); - database.batteryStateDao().clearAllBefore(earliestTimestamp); database.appUsageEventDao().clearAllBefore(earliestTimestamp); + database.batteryEventDao().clearAllBefore(earliestTimestamp); + database.batteryStateDao().clearAllBefore(earliestTimestamp); } catch (RuntimeException e) { Log.e(TAG, "clearAllBefore() failed", e); } @@ -292,6 +331,23 @@ public final class DatabaseUtils { return valuesList; } + static ContentValues sendBatteryEventData( + final Context context, final BatteryEvent batteryEvent) { + final long startTime = System.currentTimeMillis(); + ContentValues contentValues = ConvertUtils.convertBatteryEventToContentValues(batteryEvent); + final ContentResolver resolver = context.getContentResolver(); + try { + resolver.insert(BATTERY_EVENT_URI, contentValues); + Log.d(TAG, "insert() battery event data into database: " + batteryEvent.toString()); + } catch (Exception e) { + Log.e(TAG, "insert() battery event data into database error:\n" + e); + } + Log.d(TAG, String.format("sendBatteryEventData() in %d/ms", + (System.currentTimeMillis() - startTime))); + clearMemory(); + return contentValues; + } + static List sendBatteryEntryData( final Context context, final List batteryEntryList, @@ -392,6 +448,8 @@ public final class DatabaseUtils { public static void dump(Context context, PrintWriter writer) { writeString(context, writer, "BatteryLevelChanged", Intent.ACTION_BATTERY_LEVEL_CHANGED); + writeString(context, writer, "BatteryPlugging", + BatteryUsageBroadcastReceiver.ACTION_BATTERY_PLUGGING); writeString(context, writer, "BatteryUnplugging", BatteryUsageBroadcastReceiver.ACTION_BATTERY_UNPLUGGING); writeString(context, writer, "ClearBatteryCacheData", @@ -475,6 +533,32 @@ public final class DatabaseUtils { return appUsageEventList; } + private static List loadBatteryEventsFromContentProvider( + Context context, Uri batteryEventUri) { + final List batteryEventList = new ArrayList<>(); + context = getParentContext(context); + if (context == null) { + return batteryEventList; + } + try (Cursor cursor = sFakeBatteryEventSupplier != null + ? sFakeBatteryEventSupplier.get() + : context.getContentResolver().query(batteryEventUri, null, null, null)) { + if (cursor == null || cursor.getCount() == 0) { + return batteryEventList; + } + // Loads and recovers all AppUsageEvent data from cursor. + while (cursor.moveToNext()) { + batteryEventList.add(ConvertUtils.convertToBatteryEventFromCursor(cursor)); + } + try { + cursor.close(); + } catch (Exception e) { + Log.e(TAG, "cursor.close() failed", e); + } + } + return batteryEventList; + } + private static Map> loadHistoryMapFromContentProvider( Context context, Uri batteryStateUri) { context = DatabaseUtils.getParentContext(context); diff --git a/src/com/android/settings/fuelgauge/batteryusage/db/BatteryEventDao.java b/src/com/android/settings/fuelgauge/batteryusage/db/BatteryEventDao.java new file mode 100644 index 00000000000..a638d094519 --- /dev/null +++ b/src/com/android/settings/fuelgauge/batteryusage/db/BatteryEventDao.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2023 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.db; + +import android.database.Cursor; + +import androidx.room.Dao; +import androidx.room.Insert; +import androidx.room.OnConflictStrategy; +import androidx.room.Query; + +import java.util.List; + +/** Data access object for accessing {@link BatteryEventEntity} in the database. */ +@Dao +public interface BatteryEventDao { + /** Inserts a {@link BatteryEventEntity} data into the database. */ + @Insert(onConflict = OnConflictStrategy.REPLACE) + void insert(BatteryEventEntity event); + + /** Gets all recorded data. */ + @Query("SELECT * FROM BatteryEventEntity ORDER BY timestamp DESC") + List getAll(); + + /** Gets the {@link Cursor} of all recorded data after a specific timestamp. */ + @Query("SELECT * FROM BatteryEventEntity WHERE timestamp > :timestamp ORDER BY timestamp DESC") + Cursor getAllAfter(long timestamp); + + /** Deletes all recorded data before a specific timestamp. */ + @Query("DELETE FROM BatteryEventEntity WHERE timestamp <= :timestamp") + void clearAllBefore(long timestamp); + + /** Clears all recorded data in the database. */ + @Query("DELETE FROM BatteryEventEntity") + void clearAll(); +} diff --git a/src/com/android/settings/fuelgauge/batteryusage/db/BatteryEventEntity.java b/src/com/android/settings/fuelgauge/batteryusage/db/BatteryEventEntity.java new file mode 100644 index 00000000000..66cadda4530 --- /dev/null +++ b/src/com/android/settings/fuelgauge/batteryusage/db/BatteryEventEntity.java @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2023 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.db; + +import android.content.ContentValues; + +import androidx.room.Entity; +import androidx.room.PrimaryKey; + +import com.android.settings.fuelgauge.batteryusage.ConvertUtils; + +import com.google.errorprone.annotations.CanIgnoreReturnValue; + +import java.util.Locale; + +/** A {@link Entity} class to save battery events into database. */ +@Entity +public class BatteryEventEntity { + /** Keys for accessing {@link ContentValues}. */ + public static final String KEY_TIMESTAMP = "timestamp"; + public static final String KEY_BATTERY_EVENT_TYPE = "batteryEventType"; + public static final String KEY_BATTERY_LEVEL = "batteryLevel"; + + @PrimaryKey(autoGenerate = true) + private long mId; + + public final long timestamp; + public final int batteryEventType; + public final int batteryLevel; + + public BatteryEventEntity( + final long timestamp, + final int batteryEventType, + final int batteryLevel) { + this.timestamp = timestamp; + this.batteryEventType = batteryEventType; + this.batteryLevel = batteryLevel; + } + + /** Sets the auto-generated content ID. */ + public void setId(long id) { + this.mId = id; + } + + /** Gets the auto-generated content ID. */ + public long getId() { + return mId; + } + + @Override + public String toString() { + final String recordAtDateTime = ConvertUtils.utcToLocalTimeForLogging(timestamp); + final StringBuilder builder = new StringBuilder() + .append("\nBatteryEvent{") + .append(String.format(Locale.US, + "\n\ttimestamp=%s|batteryEventType=%d|batteryLevel=%d", + recordAtDateTime, batteryEventType, batteryLevel)) + .append("\n}"); + return builder.toString(); + } + + /** Creates new {@link BatteryEventEntity} from {@link ContentValues}. */ + public static BatteryEventEntity create(ContentValues contentValues) { + Builder builder = BatteryEventEntity.newBuilder(); + if (contentValues.containsKey(KEY_TIMESTAMP)) { + builder.setTimestamp(contentValues.getAsLong(KEY_TIMESTAMP)); + } + if (contentValues.containsKey(KEY_BATTERY_EVENT_TYPE)) { + builder.setBatteryEventType(contentValues.getAsInteger(KEY_BATTERY_EVENT_TYPE)); + } + if (contentValues.containsKey(KEY_BATTERY_LEVEL)) { + builder.setBatteryLevel(contentValues.getAsInteger(KEY_BATTERY_LEVEL)); + } + return builder.build(); + } + + /** Creates a new {@link Builder} instance. */ + public static Builder newBuilder() { + return new Builder(); + } + + /** A convenience builder class to improve readability. */ + public static class Builder { + private long mTimestamp; + private int mBatteryEventType; + private int mBatteryLevel; + + /** Sets the timestamp. */ + @CanIgnoreReturnValue + public Builder setTimestamp(final long timestamp) { + mTimestamp = timestamp; + return this; + } + + /** Sets the battery event type. */ + @CanIgnoreReturnValue + public Builder setBatteryEventType(final int batteryEventType) { + mBatteryEventType = batteryEventType; + return this; + } + + /** Sets the battery level. */ + @CanIgnoreReturnValue + public Builder setBatteryLevel(final int batteryLevel) { + mBatteryLevel = batteryLevel; + return this; + } + + /** Builds the {@link BatteryEventEntity}. */ + public BatteryEventEntity build() { + return new BatteryEventEntity( + mTimestamp, + mBatteryEventType, + mBatteryLevel); + } + + private Builder() {} + } +} diff --git a/src/com/android/settings/fuelgauge/batteryusage/db/BatteryStateDatabase.java b/src/com/android/settings/fuelgauge/batteryusage/db/BatteryStateDatabase.java index 4c8df7e0ad5..466a7ca4740 100644 --- a/src/com/android/settings/fuelgauge/batteryusage/db/BatteryStateDatabase.java +++ b/src/com/android/settings/fuelgauge/batteryusage/db/BatteryStateDatabase.java @@ -25,24 +25,26 @@ import androidx.room.RoomDatabase; /** A {@link RoomDatabase} for battery usage states history. */ @Database( - entities = {BatteryState.class, AppUsageEventEntity.class}, + entities = {AppUsageEventEntity.class, BatteryEventEntity.class, BatteryState.class}, version = 1) public abstract class BatteryStateDatabase extends RoomDatabase { private static final String TAG = "BatteryStateDatabase"; private static BatteryStateDatabase sBatteryStateDatabase; - /** Provides DAO for battery state table. */ - public abstract BatteryStateDao batteryStateDao(); /** Provides DAO for app usage event table. */ public abstract AppUsageEventDao appUsageEventDao(); + /** Provides DAO for battery event table. */ + public abstract BatteryEventDao batteryEventDao(); + /** Provides DAO for battery state table. */ + public abstract BatteryStateDao batteryStateDao(); /** Gets or creates an instance of {@link RoomDatabase}. */ public static BatteryStateDatabase getInstance(Context context) { if (sBatteryStateDatabase == null) { sBatteryStateDatabase = Room.databaseBuilder( - context, BatteryStateDatabase.class, "battery-usage-db-v7") + context, BatteryStateDatabase.class, "battery-usage-db-v8") // Allows accessing data in the main thread for dumping bugreport. .allowMainThreadQueries() .fallbackToDestructiveMigration() diff --git a/src/com/android/settings/fuelgauge/protos/Android.bp b/src/com/android/settings/fuelgauge/protos/Android.bp index 2c63af40a09..3af2aef0796 100644 --- a/src/com/android/settings/fuelgauge/protos/Android.bp +++ b/src/com/android/settings/fuelgauge/protos/Android.bp @@ -7,6 +7,22 @@ package { default_applicable_licenses: ["packages_apps_Settings_license"], } +java_library { + name: "app-usage-event-protos-lite", + proto: { + type: "lite", + }, + srcs: ["app_usage_event.proto"], +} + +java_library { + name: "battery-event-protos-lite", + proto: { + type: "lite", + }, + srcs: ["battery_event.proto"], +} + java_library { name: "fuelgauge-usage-state-protos-lite", proto: { @@ -14,11 +30,3 @@ java_library { }, srcs: ["fuelgauge_usage_state.proto"], } - -java_library { - name: "app-usage-event-protos-lite", - proto: { - type: "lite", - }, - srcs: ["app_usage_event.proto"], -} \ No newline at end of file diff --git a/src/com/android/settings/fuelgauge/protos/battery_event.proto b/src/com/android/settings/fuelgauge/protos/battery_event.proto new file mode 100644 index 00000000000..80ccb3b24f4 --- /dev/null +++ b/src/com/android/settings/fuelgauge/protos/battery_event.proto @@ -0,0 +1,17 @@ +syntax = "proto2"; + +option java_multiple_files = true; +option java_package = "com.android.settings.fuelgauge.batteryusage"; +option java_outer_classname = "BatteryEventProto"; + +enum BatteryEventType { + UNKNOWN_EVENT = 0; + POWER_CONNECTED = 1; + POWER_DISCONNECTED = 2; +} + +message BatteryEvent { + optional int64 timestamp = 1; + optional BatteryEventType type = 2; + optional int32 battery_level = 3; +} 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 e5547c59b5c..05a6f2bc89c 100644 --- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageContentProviderTest.java +++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageContentProviderTest.java @@ -31,6 +31,7 @@ import android.net.Uri; import androidx.test.core.app.ApplicationProvider; import com.android.settings.fuelgauge.batteryusage.db.AppUsageEventEntity; +import com.android.settings.fuelgauge.batteryusage.db.BatteryEventEntity; import com.android.settings.fuelgauge.batteryusage.db.BatteryState; import com.android.settings.fuelgauge.batteryusage.db.BatteryStateDatabase; import com.android.settings.testutils.BatteryTestUtils; @@ -353,6 +354,28 @@ public final class BatteryUsageContentProviderTest { assertThat(entities.get(0).taskRootPackageName).isEqualTo("com.android.settings2"); } + @Test + public void insert_batteryEvent_returnsExpectedResult() { + mProvider.onCreate(); + ContentValues values = new ContentValues(); + values.put(BatteryEventEntity.KEY_TIMESTAMP, 10001L); + values.put(BatteryEventEntity.KEY_BATTERY_EVENT_TYPE, + BatteryEventType.POWER_CONNECTED.getNumber()); + values.put(BatteryEventEntity.KEY_BATTERY_LEVEL, 66); + + final Uri uri = mProvider.insert(DatabaseUtils.BATTERY_EVENT_URI, values); + + assertThat(uri).isEqualTo(DatabaseUtils.BATTERY_EVENT_URI); + // Verifies the AppUsageEventEntity content. + final List entities = + BatteryStateDatabase.getInstance(mContext).batteryEventDao().getAll(); + assertThat(entities).hasSize(1); + assertThat(entities.get(0).timestamp).isEqualTo(10001L); + assertThat(entities.get(0).batteryEventType).isEqualTo( + BatteryEventType.POWER_CONNECTED.getNumber()); + assertThat(entities.get(0).batteryLevel).isEqualTo(66); + } + @Test public void delete_throwsUnsupportedOperationException() { assertThrows( diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/ConvertUtilsTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/ConvertUtilsTest.java index fe1bff6ce00..6b8073b68ee 100644 --- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/ConvertUtilsTest.java +++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/ConvertUtilsTest.java @@ -39,6 +39,7 @@ import android.os.RemoteException; import android.os.UserHandle; import com.android.settings.fuelgauge.batteryusage.db.AppUsageEventEntity; +import com.android.settings.fuelgauge.batteryusage.db.BatteryEventEntity; import org.junit.Before; import org.junit.Test; @@ -194,6 +195,22 @@ public final class ConvertUtilsTest { .isEqualTo("com.android.settings2"); } + @Test + public void convertBatteryEventToContentValues_normalCase_returnsExpectedContentValues() { + final BatteryEvent batteryEvent = + BatteryEvent.newBuilder() + .setTimestamp(10001L) + .setType(BatteryEventType.POWER_CONNECTED) + .setBatteryLevel(66) + .build(); + final ContentValues values = + ConvertUtils.convertBatteryEventToContentValues(batteryEvent); + assertThat(values.getAsLong(BatteryEventEntity.KEY_TIMESTAMP)).isEqualTo(10001L); + assertThat(values.getAsInteger(BatteryEventEntity.KEY_BATTERY_EVENT_TYPE)).isEqualTo( + BatteryEventType.POWER_CONNECTED.getNumber()); + assertThat(values.getAsInteger(BatteryEventEntity.KEY_BATTERY_LEVEL)).isEqualTo(66); + } + @Test public void convertToBatteryHistEntry_returnsExpectedResult() { final int expectedType = 3; @@ -405,6 +422,15 @@ public final class ConvertUtilsTest { assertThat(appUsageEvent.getInstanceId()).isEqualTo(0); } + @Test + public void convertToBatteryEvent_normalCase_returnsExpectedResult() { + final BatteryEvent batteryEvent = ConvertUtils.convertToBatteryEvent( + 666L, BatteryEventType.POWER_DISCONNECTED, 88); + assertThat(batteryEvent.getTimestamp()).isEqualTo(666L); + assertThat(batteryEvent.getType()).isEqualTo(BatteryEventType.POWER_DISCONNECTED); + assertThat(batteryEvent.getBatteryLevel()).isEqualTo(88); + } + @Test public void getLocale_nullContext_returnDefaultLocale() { assertThat(ConvertUtils.getLocale(/*context=*/ null)) diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DataProcessManagerTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DataProcessManagerTest.java index aab91e0a590..753a7f7a1d0 100644 --- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DataProcessManagerTest.java +++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DataProcessManagerTest.java @@ -183,7 +183,7 @@ public final class DataProcessManagerTest { assertThat(dataProcessManager.getIsCurrentBatteryHistoryLoaded()).isTrue(); assertThat(dataProcessManager.getShowScreenOnTime()).isTrue(); final List appUsageEventList = dataProcessManager.getAppUsageEventList(); - Collections.sort(appUsageEventList, DataProcessor.TIMESTAMP_COMPARATOR); + Collections.sort(appUsageEventList, DataProcessor.APP_USAGE_EVENT_TIMESTAMP_COMPARATOR); assertThat(appUsageEventList.size()).isEqualTo(6); assertAppUsageEvent( appUsageEventList.get(0), AppUsageEventType.ACTIVITY_RESUMED, /*timestamp=*/ 1); diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DataProcessorTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DataProcessorTest.java index aa1547216d2..66e48c09246 100644 --- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DataProcessorTest.java +++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DataProcessorTest.java @@ -250,7 +250,7 @@ public final class DataProcessorTest { final Map>>>> periodMap = DataProcessor.generateAppUsagePeriodMap( - 14400000L, hourlyBatteryLevelsPerDay, appUsageEventList); + 14400000L, hourlyBatteryLevelsPerDay, appUsageEventList, new ArrayList<>()); assertThat(periodMap).hasSize(3); // Day 1 @@ -288,7 +288,7 @@ public final class DataProcessorTest { hourlyBatteryLevelsPerDay.add( new BatteryLevelData.PeriodBatteryLevelData(new ArrayList<>(), new ArrayList<>())); assertThat(DataProcessor.generateAppUsagePeriodMap( - 0L, hourlyBatteryLevelsPerDay, new ArrayList<>())).isNull(); + 0L, hourlyBatteryLevelsPerDay, new ArrayList<>(), new ArrayList<>())).isNull(); } @Test @@ -1669,7 +1669,8 @@ public final class DataProcessorTest { /*instanceId=*/ 4, packageName2)); final Map>> appUsagePeriodMap = - DataProcessor.buildAppUsagePeriodList(appUsageEvents, 0, 5); + DataProcessor.buildAppUsagePeriodList( + appUsageEvents, new ArrayList<>(), 0, 5); assertThat(appUsagePeriodMap).hasSize(2); final Map> userMap1 = appUsagePeriodMap.get(1L); @@ -1693,7 +1694,7 @@ public final class DataProcessorTest { @Test public void buildAppUsagePeriodList_emptyEventList_returnNull() { assertThat(DataProcessor.buildAppUsagePeriodList( - new ArrayList<>(), 0, 1)).isNull(); + new ArrayList<>(), new ArrayList<>(), 0, 1)).isNull(); } @Test @@ -1705,7 +1706,7 @@ public final class DataProcessorTest { AppUsageEventType.DEVICE_SHUTDOWN, /*timestamp=*/ 2)); assertThat(DataProcessor.buildAppUsagePeriodList( - appUsageEvents, 0, 3)).isNull(); + appUsageEvents, new ArrayList<>(), 0, 3)).isNull(); } @Test @@ -1765,6 +1766,89 @@ public final class DataProcessorTest { assertAppUsagePeriod(appUsagePeriodList.get(6), 1000000, 1100000); } + @Test + public void excludePowerConnectedTime_startEndNotCharging_returnExpectedResult() { + final List appUsagePeriodList = List.of( + AppUsagePeriod.newBuilder().setStartTime(100).setEndTime(200).build()); + final List batteryEventList = List.of( + BatteryEvent.newBuilder().setTimestamp(50).setType( + BatteryEventType.POWER_DISCONNECTED).build(), + BatteryEvent.newBuilder().setTimestamp(166).setType( + BatteryEventType.POWER_CONNECTED).build(), + BatteryEvent.newBuilder().setTimestamp(188).setType( + BatteryEventType.POWER_DISCONNECTED).build(), + BatteryEvent.newBuilder().setTimestamp(280).setType( + BatteryEventType.POWER_CONNECTED).build()); + + final List resultList = + DataProcessor.excludePowerConnectedTimeFromAppUsagePeriodList( + appUsagePeriodList, batteryEventList); + + assertThat(resultList).hasSize(2); + assertAppUsagePeriod(resultList.get(0), 100, 166); + assertAppUsagePeriod(resultList.get(1), 188, 200); + } + + @Test + public void excludePowerConnectedTime_startEndInCharging_returnExpectedResult() { + final List appUsagePeriodList = List.of( + AppUsagePeriod.newBuilder().setStartTime(100).setEndTime(200).build()); + final List batteryEventList = List.of( + BatteryEvent.newBuilder().setTimestamp(50).setType( + BatteryEventType.POWER_DISCONNECTED).build(), + BatteryEvent.newBuilder().setTimestamp(80).setType( + BatteryEventType.POWER_CONNECTED).build(), + BatteryEvent.newBuilder().setTimestamp(120).setType( + BatteryEventType.POWER_DISCONNECTED).build(), + BatteryEvent.newBuilder().setTimestamp(150).setType( + BatteryEventType.POWER_CONNECTED).build(), + BatteryEvent.newBuilder().setTimestamp(160).setType( + BatteryEventType.POWER_DISCONNECTED).build(), + BatteryEvent.newBuilder().setTimestamp(180).setType( + BatteryEventType.POWER_CONNECTED).build()); + + final List resultList = + DataProcessor.excludePowerConnectedTimeFromAppUsagePeriodList( + appUsagePeriodList, batteryEventList); + + assertThat(resultList).hasSize(2); + assertAppUsagePeriod(resultList.get(0), 120, 150); + assertAppUsagePeriod(resultList.get(1), 160, 180); + } + + @Test + public void excludePowerConnectedTime_wholePeriodNotCharging_returnExpectedResult() { + final List appUsagePeriodList = List.of( + AppUsagePeriod.newBuilder().setStartTime(100).setEndTime(200).build()); + final List batteryEventList = List.of( + BatteryEvent.newBuilder().setTimestamp(50).setType( + BatteryEventType.POWER_DISCONNECTED).build(), + BatteryEvent.newBuilder().setTimestamp(80).setType( + BatteryEventType.POWER_CONNECTED).build()); + + final List resultList = + DataProcessor.excludePowerConnectedTimeFromAppUsagePeriodList( + appUsagePeriodList, batteryEventList); + + assertThat(resultList).isEmpty(); + } + + @Test + public void excludePowerConnectedTime_wholePeriodInCharging_returnExpectedResult() { + final List appUsagePeriodList = List.of( + AppUsagePeriod.newBuilder().setStartTime(100).setEndTime(200).build()); + final List batteryEventList = List.of( + BatteryEvent.newBuilder().setTimestamp(50).setType( + BatteryEventType.POWER_DISCONNECTED).build()); + + final List resultList = + DataProcessor.excludePowerConnectedTimeFromAppUsagePeriodList( + appUsagePeriodList, batteryEventList); + + assertThat(resultList).hasSize(1); + assertAppUsagePeriod(resultList.get(0), 100, 200); + } + @Test public void getScreenOnTime_returnExpectedResult() { final long userId = 1; diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtilsTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtilsTest.java index 0cb3d1d50d4..20799d403d3 100644 --- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtilsTest.java +++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtilsTest.java @@ -36,6 +36,7 @@ import android.os.UserHandle; import android.os.UserManager; import com.android.settings.fuelgauge.batteryusage.db.AppUsageEventEntity; +import com.android.settings.fuelgauge.batteryusage.db.BatteryEventEntity; import com.android.settings.testutils.BatteryTestUtils; import org.junit.Before; @@ -136,6 +137,28 @@ public final class DatabaseUtilsTest { verifyNoMoreInteractions(mMockContentResolver); } + @Test + public void sendBatteryEventData_returnsExpectedList() { + final BatteryEvent batteryEvent = + BatteryEvent.newBuilder() + .setTimestamp(10001L) + .setType(BatteryEventType.POWER_CONNECTED) + .setBatteryLevel(66) + .build(); + + final ContentValues contentValues = + DatabaseUtils.sendBatteryEventData(mContext, batteryEvent); + + assertThat(contentValues.getAsInteger(BatteryEventEntity.KEY_TIMESTAMP)) + .isEqualTo(10001L); + assertThat(contentValues.getAsInteger(BatteryEventEntity.KEY_BATTERY_EVENT_TYPE)) + .isEqualTo(BatteryEventType.POWER_CONNECTED.getNumber()); + assertThat(contentValues.getAsInteger(BatteryEventEntity.KEY_BATTERY_LEVEL)) + .isEqualTo(66); + // Verifies the inserted ContentValues into content provider. + verify(mMockContentResolver).insert(DatabaseUtils.BATTERY_EVENT_URI, contentValues); + } + @Test public void sendBatteryEntryData_nullBatteryIntent_returnsNullValue() { doReturn(null).when(mContext).registerReceiver(any(), any()); diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/db/BatteryEventDaoTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/db/BatteryEventDaoTest.java new file mode 100644 index 00000000000..941f444e552 --- /dev/null +++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/db/BatteryEventDaoTest.java @@ -0,0 +1,80 @@ +/* + * 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.db; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.Context; +import android.database.Cursor; + +import androidx.test.core.app.ApplicationProvider; + +import com.android.settings.testutils.BatteryTestUtils; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +/** Tests for {@link BatteryEventDao}. */ +@RunWith(RobolectricTestRunner.class) +public final class BatteryEventDaoTest { + private Context mContext; + private BatteryStateDatabase mDatabase; + private BatteryEventDao mBatteryEventDao; + + @Before + public void setUp() { + mContext = ApplicationProvider.getApplicationContext(); + mDatabase = BatteryTestUtils.setUpBatteryStateDatabase(mContext); + mBatteryEventDao = mDatabase.batteryEventDao(); + } + + @After + public void closeDb() { + mDatabase.close(); + BatteryStateDatabase.setBatteryStateDatabase(/*database=*/ null); + } + + @Test + public void getAllAfter_returnExpectedResult() { + mBatteryEventDao.insert(BatteryEventEntity.newBuilder() + .setTimestamp(100L) + .setBatteryEventType(1) + .setBatteryLevel(66) + .build()); + mBatteryEventDao.insert(BatteryEventEntity.newBuilder() + .setTimestamp(200L) + .setBatteryEventType(2) + .setBatteryLevel(88) + .build()); + + final Cursor cursor = mBatteryEventDao.getAllAfter(160L); + assertThat(cursor.getCount()).isEqualTo(1); + cursor.moveToFirst(); + assertThat(cursor.getLong(cursor.getColumnIndex(BatteryEventEntity.KEY_TIMESTAMP))) + .isEqualTo(200L); + assertThat(cursor.getInt(cursor.getColumnIndex(BatteryEventEntity.KEY_BATTERY_EVENT_TYPE))) + .isEqualTo(2); + assertThat(cursor.getInt(cursor.getColumnIndex(BatteryEventEntity.KEY_BATTERY_LEVEL))) + .isEqualTo(88); + + mBatteryEventDao.clearAll(); + assertThat(mBatteryEventDao.getAll()).isEmpty(); + } +} diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/db/BatteryEventEntityTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/db/BatteryEventEntityTest.java new file mode 100644 index 00000000000..a34d6515982 --- /dev/null +++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/db/BatteryEventEntityTest.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2023 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.db; + +import static com.google.common.truth.Truth.assertThat; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +/** Tests for {@link BatteryEventEntity}. */ +@RunWith(RobolectricTestRunner.class) +public final class BatteryEventEntityTest { + + @Test + public void testBuilder_returnsExpectedResult() { + final long timestamp = 10001L; + final int batteryEventType = 1; + final int batteryLevel = 66; + + BatteryEventEntity entity = BatteryEventEntity + .newBuilder() + .setTimestamp(timestamp) + .setBatteryEventType(batteryEventType) + .setBatteryLevel(batteryLevel) + .build(); + + // Verifies the app relative information. + assertThat(entity.timestamp).isEqualTo(timestamp); + assertThat(entity.batteryEventType).isEqualTo(batteryEventType); + assertThat(entity.batteryLevel).isEqualTo(batteryLevel); + } +}