diff --git a/src/com/android/settings/fuelgauge/batteryusage/DataProcessManager.java b/src/com/android/settings/fuelgauge/batteryusage/DataProcessManager.java index dd6b9d9d4af..a1634bfb05f 100644 --- a/src/com/android/settings/fuelgauge/batteryusage/DataProcessManager.java +++ b/src/com/android/settings/fuelgauge/batteryusage/DataProcessManager.java @@ -33,7 +33,6 @@ import com.android.settings.Utils; import java.util.ArrayList; import java.util.Calendar; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -92,6 +91,13 @@ public class DataProcessManager { 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.

+ *

{@code String} stands for the packageName.

+ */ + private Map>>>> + mAppUsagePeriodMap; /** * Constructor when there exists battery level data. @@ -167,6 +173,12 @@ public class DataProcessManager { return mAppUsageEventList; } + @VisibleForTesting + Map>>>> + getAppUsagePeriodMap() { + return mAppUsagePeriodMap; + } + @VisibleForTesting boolean getIsCurrentAppUsageLoaded() { return mIsCurrentAppUsageLoaded; @@ -361,9 +373,10 @@ public class DataProcessManager { if (!mShowScreenOnTime) { return; } - // Sort the appUsageEventList in ascending order based on the timestamp. - Collections.sort(mAppUsageEventList, DataProcessor.TIMESTAMP_COMPARATOR); - // TODO: process app usage data to an intermediate result for further use. + // Generates the indexed AppUsagePeriod list data for each corresponding time slot for + // further use. + mAppUsagePeriodMap = DataProcessor.generateAppUsagePeriodMap( + mHourlyBatteryLevelsPerDay, mAppUsageEventList, mStartTimestampOfLevelData); } private void tryToGenerateFinalDataAndApplyCallback() { diff --git a/src/com/android/settings/fuelgauge/batteryusage/DataProcessor.java b/src/com/android/settings/fuelgauge/batteryusage/DataProcessor.java index feea8f8300b..6f99f5baef2 100644 --- a/src/com/android/settings/fuelgauge/batteryusage/DataProcessor.java +++ b/src/com/android/settings/fuelgauge/batteryusage/DataProcessor.java @@ -16,6 +16,7 @@ package com.android.settings.fuelgauge.batteryusage; +import static com.android.settings.fuelgauge.batteryusage.ConvertUtils.getEffectivePackageName; import static com.android.settings.fuelgauge.batteryusage.ConvertUtils.utcToLocalTime; import android.app.usage.IUsageStatsManager; @@ -41,6 +42,7 @@ import android.os.UserHandle; import android.os.UserManager; import android.text.TextUtils; import android.text.format.DateUtils; +import android.util.ArrayMap; import android.util.ArraySet; import android.util.Log; import android.util.SparseArray; @@ -86,6 +88,10 @@ public final class DataProcessor { private static final BatteryHistEntry EMPTY_BATTERY_HIST_ENTRY = new BatteryHistEntry(new ContentValues()); + @VisibleForTesting + static final long DEFAULT_USAGE_DURATION_FOR_INCOMPLETE_INTERVAL = + DateUtils.SECOND_IN_MILLIS * 30; + @VisibleForTesting static final int SELECTED_INDEX_ALL = BatteryChartViewModel.SELECTED_INDEX_ALL; @@ -222,6 +228,90 @@ public final class DataProcessor { } } + /** + * Generates the indexed {@link AppUsagePeriod} list data for each corresponding time slot. + * Attributes the list of {@link AppUsageEvent} into hourly time slots and reformat them into + * {@link AppUsagePeriod} for easier use in the following process. + * + *

There could be 2 cases of the returned value:

+ *
    + *
  • null: empty or invalid data.
  • + *
  • non-null: must be a 2d map and composed by: + *

    [0][0] ~ [maxDailyIndex][maxHourlyIndex]

  • + *
+ * + *

The structure is consistent with the battery usage map returned by + * {@code getBatteryUsageMap}.

+ * + *

{@code Long} stands for the userId.

+ *

{@code String} stands for the packageName.

+ */ + @Nullable + public static Map>>>> + generateAppUsagePeriodMap( + final List hourlyBatteryLevelsPerDay, + final List appUsageEventList, + final long startTimestampOfLevelData) { + 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); + final Map>>>> resultMap = + new HashMap<>(); + // Starts from the first index within expected time range. The events before the time range + // will be bypassed directly. + int currentAppUsageEventIndex = + getFirstUsageEventIndex(appUsageEventList, startTimestampOfLevelData); + final int appUsageEventSize = appUsageEventList.size(); + + for (int dailyIndex = 0; dailyIndex < hourlyBatteryLevelsPerDay.size(); dailyIndex++) { + final Map>>> dailyMap = + new HashMap<>(); + resultMap.put(dailyIndex, dailyMap); + if (hourlyBatteryLevelsPerDay.get(dailyIndex) == null) { + continue; + } + final List timestamps = hourlyBatteryLevelsPerDay.get(dailyIndex).getTimestamps(); + for (int hourlyIndex = 0; hourlyIndex < timestamps.size() - 1; hourlyIndex++) { + // The start and end timestamps of this slot should be the adjacent timestamps. + final long startTimestamp = timestamps.get(hourlyIndex); + final long endTimestamp = timestamps.get(hourlyIndex + 1); + + // Gets the app usage event list for this hourly slot first. + final List hourlyAppUsageEventList = new ArrayList<>(); + while (currentAppUsageEventIndex < appUsageEventSize) { + // If current event is null, go for next directly. + if (appUsageEventList.get(currentAppUsageEventIndex) == null) { + currentAppUsageEventIndex++; + continue; + } + final long timestamp = + appUsageEventList.get(currentAppUsageEventIndex).getTimestamp(); + // If the timestamp is already later than the end time, stop the loop. + if (timestamp >= endTimestamp) { + break; + } + // If timestamp is within the time range, add it into the list. + if (timestamp >= startTimestamp) { + hourlyAppUsageEventList.add( + appUsageEventList.get(currentAppUsageEventIndex)); + } + currentAppUsageEventIndex++; + } + + // The value could be null when there is no data in the hourly slot. + dailyMap.put( + hourlyIndex, + buildAppUsagePeriodList( + hourlyAppUsageEventList, startTimestamp, endTimestamp)); + } + } + return resultMap; + } + /** * Generates the list of {@link AppUsageEvent} from the supplied {@link UsageEvents}. */ @@ -498,12 +588,14 @@ public final class DataProcessor { /** * @return Returns the indexed battery usage data for each corresponding time slot. * - * There could be 2 cases of the returned value: - * 1) null: empty or invalid data. - * 2) non-null: must be a 2d map and composed by 3 parts: - * 1 - [SELECTED_INDEX_ALL][SELECTED_INDEX_ALL] - * 2 - [0][SELECTED_INDEX_ALL] ~ [maxDailyIndex][SELECTED_INDEX_ALL] - * 3 - [0][0] ~ [maxDailyIndex][maxHourlyIndex] + *

There could be 2 cases of the returned value:

+ *
    + *
  • null: empty or invalid data.
  • + *
  • non-null: must be a 2d map and composed by 3 parts:
  • + *

    1 - [SELECTED_INDEX_ALL][SELECTED_INDEX_ALL]

    + *

    2 - [0][SELECTED_INDEX_ALL] ~ [maxDailyIndex][SELECTED_INDEX_ALL]

    + *

    3 - [0][0] ~ [maxDailyIndex][maxHourlyIndex]

    + *
*/ @VisibleForTesting @Nullable @@ -579,6 +671,140 @@ public final class DataProcessor { return new BatteryDiffData(appEntries, systemEntries); } + /** + *

{@code Long} stands for the userId.

+ *

{@code String} stands for the packageName.

+ */ + @VisibleForTesting + @Nullable + static Map>> buildAppUsagePeriodList( + final List allAppUsageEvents, final long startTime, final long endTime) { + if (allAppUsageEvents.isEmpty()) { + return null; + } + + // Attributes the list of AppUsagePeriod into device events and instance events for further + // use. + final List deviceEvents = new ArrayList<>(); + final ArrayMap> usageEventsByInstanceId = new ArrayMap<>(); + for (final AppUsageEvent event : allAppUsageEvents) { + final AppUsageEventType eventType = event.getType(); + if (eventType == AppUsageEventType.ACTIVITY_RESUMED + || eventType == AppUsageEventType.ACTIVITY_STOPPED) { + final int instanceId = event.getInstanceId(); + if (usageEventsByInstanceId.get(instanceId) == null) { + usageEventsByInstanceId.put(instanceId, new ArrayList<>()); + } + usageEventsByInstanceId.get(instanceId).add(event); + } else if (eventType == AppUsageEventType.DEVICE_SHUTDOWN) { + // Track device-wide events in their own list as they affect any app. + deviceEvents.add(event); + } + } + if (usageEventsByInstanceId.isEmpty()) { + return null; + } + + final Map>> allUsagePeriods = new HashMap<>(); + + for (int i = 0; i < usageEventsByInstanceId.size(); i++) { + // The usage periods for an instance are determined by the usage events with its + // instance id and any device-wide events such as device shutdown. + final List usageEvents = usageEventsByInstanceId.valueAt(i); + if (usageEvents == null || usageEvents.isEmpty()) { + continue; + } + // The same instance must have same userId and packageName. + final AppUsageEvent firstEvent = usageEvents.get(0); + final long eventUserId = firstEvent.getUserId(); + final String packageName = getEffectivePackageName( + sUsageStatsManager, + firstEvent.getPackageName(), + firstEvent.getTaskRootPackageName()); + usageEvents.addAll(deviceEvents); + // Sorts the usageEvents in ascending order based on the timestamp before computing the + // period. + Collections.sort(usageEvents, 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); + if (!usagePeriodList.isEmpty()) { + addToUsagePeriodMap(allUsagePeriods, usagePeriodList, eventUserId, packageName); + } + } + + // Sorts all usage periods by start time. + for (final long userId : allUsagePeriods.keySet()) { + if (allUsagePeriods.get(userId) == null) { + continue; + } + for (final String packageName: allUsagePeriods.get(userId).keySet()) { + Collections.sort( + allUsagePeriods.get(userId).get(packageName), + Comparator.comparing(AppUsagePeriod::getStartTime)); + } + } + return allUsagePeriods.isEmpty() ? null : allUsagePeriods; + } + + @VisibleForTesting + static List buildAppUsagePeriodListPerInstance( + final List usageEvents, final long startTime, final long endTime) { + final List usagePeriodList = new ArrayList<>(); + AppUsagePeriod.Builder pendingUsagePeriod = AppUsagePeriod.newBuilder(); + boolean hasMetStartEvent = false; + + for (final AppUsageEvent event : usageEvents) { + final long eventTime = event.getTimestamp(); + + if (event.getType() == AppUsageEventType.ACTIVITY_RESUMED) { + hasMetStartEvent = true; + // If there is an existing start time, simply ignore this start event. + // If there was no start time, then start a new period. + if (!pendingUsagePeriod.hasStartTime()) { + pendingUsagePeriod.setStartTime(eventTime); + } + } else if (event.getType() == AppUsageEventType.ACTIVITY_STOPPED) { + pendingUsagePeriod.setEndTime(eventTime); + if (!pendingUsagePeriod.hasStartTime()) { + // If we haven't met start event in this list, the start event might happen + // in the previous time slot. use the startTime for this period. + // Otherwise, add one for the default duration. + if (!hasMetStartEvent) { + hasMetStartEvent = true; + pendingUsagePeriod.setStartTime(startTime); + } else { + pendingUsagePeriod.setStartTime( + getStartTimeForIncompleteUsagePeriod(pendingUsagePeriod)); + } + } + // If we already have start time, add it directly. + addToPeriodList(usagePeriodList, pendingUsagePeriod.build()); + pendingUsagePeriod.clear(); + } else if (event.getType() == AppUsageEventType.DEVICE_SHUTDOWN) { + hasMetStartEvent = true; + // The end event might be lost when device is shutdown. Use the estimated end + // time for the period. + if (pendingUsagePeriod.hasStartTime()) { + pendingUsagePeriod.setEndTime( + getEndTimeForIncompleteUsagePeriod(pendingUsagePeriod, eventTime)); + addToPeriodList(usagePeriodList, pendingUsagePeriod.build()); + pendingUsagePeriod.clear(); + } + } + } + // If there exists unclosed period, the stop event might happen in the next time + // slot. Use the endTime for the period. + if (pendingUsagePeriod.hasStartTime()) { + pendingUsagePeriod.setEndTime(endTime); + addToPeriodList(usagePeriodList, pendingUsagePeriod.build()); + pendingUsagePeriod.clear(); + } + return usagePeriodList; + } + /** * @return Returns the overall battery usage data from battery stats service directly. * @@ -613,6 +839,56 @@ public final class DataProcessor { } } + private static void addToPeriodList( + final List appUsagePeriodList, final AppUsagePeriod appUsagePeriod) { + // Only when the period is valid, add it into the list. + if (appUsagePeriod.getStartTime() < appUsagePeriod.getEndTime()) { + appUsagePeriodList.add(appUsagePeriod); + } + } + + private static void addToUsagePeriodMap( + final Map>> usagePeriodMap, + final List usagePeriodList, + final long userId, + final String packageName) { + usagePeriodMap.computeIfAbsent(userId, key -> new HashMap<>()); + final Map> packageNameMap = usagePeriodMap.get(userId); + packageNameMap.computeIfAbsent(packageName, key -> new ArrayList<>()); + packageNameMap.get(packageName).addAll(usagePeriodList); + } + + /** + * Returns the start time that gives {@code usagePeriod} the default usage duration. + */ + private static long getStartTimeForIncompleteUsagePeriod( + final AppUsagePeriodOrBuilder usagePeriod) { + return usagePeriod.getEndTime() - DEFAULT_USAGE_DURATION_FOR_INCOMPLETE_INTERVAL; + } + + /** + * Returns the end time that gives {@code usagePeriod} the default usage duration. + */ + private static long getEndTimeForIncompleteUsagePeriod( + final AppUsagePeriodOrBuilder usagePeriod, final long eventTime) { + return Math.min( + usagePeriod.getStartTime() + DEFAULT_USAGE_DURATION_FOR_INCOMPLETE_INTERVAL, + eventTime); + } + + private static int getFirstUsageEventIndex( + final List appUsageEventList, + final long startTimestampOfLevelData) { + int currentIndex = 0; + while (currentIndex < appUsageEventList.size() + && (appUsageEventList.get(currentIndex) == null + || appUsageEventList.get(currentIndex).getTimestamp() + < startTimestampOfLevelData)) { + currentIndex++; + } + return currentIndex; + } + @Nullable private static UsageEvents getAppUsageEventsForUser( Context context, final UserManager userManager, final int userID, diff --git a/src/com/android/settings/fuelgauge/protos/app_usage_event.proto b/src/com/android/settings/fuelgauge/protos/app_usage_event.proto index 921fb0a5747..c47fd1dbc65 100644 --- a/src/com/android/settings/fuelgauge/protos/app_usage_event.proto +++ b/src/com/android/settings/fuelgauge/protos/app_usage_event.proto @@ -32,3 +32,11 @@ message AppUsageEvent { optional int64 user_id = 6; optional int64 uid = 7; } + +// Represents a continuous period of time when an app is used. +message AppUsagePeriod { + // Start of the usage period. + optional int64 start_time = 1; + // End of the usage period. + optional int64 end_time = 2; +} 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 ee374692cae..7ee0fb9f4eb 100644 --- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DataProcessManagerTest.java +++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DataProcessManagerTest.java @@ -48,6 +48,7 @@ import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -110,15 +111,31 @@ public final class DataProcessManagerTest { assertThat(mDataProcessManager.getIsCurrentBatteryHistoryLoaded()).isTrue(); assertThat(mDataProcessManager.getShowScreenOnTime()).isTrue(); assertThat(mDataProcessManager.getAppUsageEventList()).isEmpty(); + assertThat(mDataProcessManager.getAppUsagePeriodMap()).isNull(); } @Test public void start_loadExpectedAppUsageData() throws RemoteException { + final List hourlyBatteryLevelsPerDay = + new ArrayList<>(); + final String packageName = "package"; + // Adds the day 1 data. + final List timestamps1 = List.of(2L, 3L, 4L); + final List levels1 = List.of(100, 100, 100); + hourlyBatteryLevelsPerDay.add( + new BatteryLevelData.PeriodBatteryLevelData(timestamps1, levels1)); + // Adds the day 2 data. + hourlyBatteryLevelsPerDay.add(null); + // Adds the day 3 data. + final List timestamps2 = List.of(5L, 6L); + final List levels2 = List.of(100, 100); + hourlyBatteryLevelsPerDay.add( + new BatteryLevelData.PeriodBatteryLevelData(timestamps2, levels2)); // Fake current usage data. final UsageEvents.Event event1 = - getUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED, /*timestamp=*/ 1); + getUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED, /*timestamp=*/ 1, packageName); final UsageEvents.Event event2 = - getUsageEvent(UsageEvents.Event.ACTIVITY_STOPPED, /*timestamp=*/ 2); + getUsageEvent(UsageEvents.Event.ACTIVITY_STOPPED, /*timestamp=*/ 2, packageName); final List events = new ArrayList<>(); events.add(event1); events.add(event2); @@ -126,30 +143,46 @@ public final class DataProcessManagerTest { .when(mUsageStatsManager) .queryEventsForUser(anyLong(), anyLong(), anyInt(), any()); doReturn(true).when(mUserManager).isUserUnlocked(anyInt()); + // Assign current user id. + doReturn(1).when(mContext).getUserId(); + // No work profile. + doReturn(new ArrayList<>()).when(mUserManager).getUserProfiles(); // Fake database usage data. final MatrixCursor cursor = new MatrixCursor( new String[]{ AppUsageEventEntity.KEY_APP_USAGE_EVENT_TYPE, - AppUsageEventEntity.KEY_TIMESTAMP}); + AppUsageEventEntity.KEY_TIMESTAMP, + AppUsageEventEntity.KEY_USER_ID, + AppUsageEventEntity.KEY_INSTANCE_ID, + AppUsageEventEntity.KEY_PACKAGE_NAME + }); // Adds fake data into the cursor. cursor.addRow(new Object[] { - AppUsageEventType.ACTIVITY_RESUMED.getNumber(), /*timestamp=*/ 3}); + AppUsageEventType.ACTIVITY_RESUMED.getNumber(), /*timestamp=*/ 3, /*userId=*/ 1, + /*instanceId=*/ 2, packageName}); cursor.addRow(new Object[] { - AppUsageEventType.ACTIVITY_RESUMED.getNumber(), /*timestamp=*/ 4}); + AppUsageEventType.ACTIVITY_STOPPED.getNumber(), /*timestamp=*/ 4, /*userId=*/ 1, + /*instanceId=*/ 2, packageName}); cursor.addRow(new Object[] { - AppUsageEventType.ACTIVITY_STOPPED.getNumber(), /*timestamp=*/ 5}); + AppUsageEventType.ACTIVITY_RESUMED.getNumber(), /*timestamp=*/ 5, /*userId=*/ 1, + /*instanceId=*/ 2, packageName}); cursor.addRow(new Object[] { - AppUsageEventType.ACTIVITY_STOPPED.getNumber(), /*timestamp=*/ 6}); + AppUsageEventType.ACTIVITY_STOPPED.getNumber(), /*timestamp=*/ 6, /*userId=*/ 1, + /*instanceId=*/ 2, packageName}); DatabaseUtils.sFakeAppUsageEventSupplier = () -> cursor; - mDataProcessManager.start(); + final DataProcessManager dataProcessManager = new DataProcessManager( + mContext, /*handler=*/ null, /*callbackFunction=*/ null, + hourlyBatteryLevelsPerDay, /*batteryHistoryMap=*/ null); + dataProcessManager.start(); - assertThat(mDataProcessManager.getIsCurrentAppUsageLoaded()).isTrue(); - assertThat(mDataProcessManager.getIsDatabaseAppUsageLoaded()).isTrue(); - assertThat(mDataProcessManager.getIsCurrentBatteryHistoryLoaded()).isTrue(); - assertThat(mDataProcessManager.getShowScreenOnTime()).isTrue(); - final List appUsageEventList = mDataProcessManager.getAppUsageEventList(); + assertThat(dataProcessManager.getIsCurrentAppUsageLoaded()).isTrue(); + assertThat(dataProcessManager.getIsDatabaseAppUsageLoaded()).isTrue(); + assertThat(dataProcessManager.getIsCurrentBatteryHistoryLoaded()).isTrue(); + assertThat(dataProcessManager.getShowScreenOnTime()).isTrue(); + final List appUsageEventList = dataProcessManager.getAppUsageEventList(); + Collections.sort(appUsageEventList, DataProcessor.TIMESTAMP_COMPARATOR); assertThat(appUsageEventList.size()).isEqualTo(6); assertAppUsageEvent( appUsageEventList.get(0), AppUsageEventType.ACTIVITY_RESUMED, /*timestamp=*/ 1); @@ -158,17 +191,41 @@ public final class DataProcessManagerTest { assertAppUsageEvent( appUsageEventList.get(2), AppUsageEventType.ACTIVITY_RESUMED, /*timestamp=*/ 3); assertAppUsageEvent( - appUsageEventList.get(3), AppUsageEventType.ACTIVITY_RESUMED, /*timestamp=*/ 4); + appUsageEventList.get(3), AppUsageEventType.ACTIVITY_STOPPED, /*timestamp=*/ 4); assertAppUsageEvent( - appUsageEventList.get(4), AppUsageEventType.ACTIVITY_STOPPED, /*timestamp=*/ 5); + appUsageEventList.get(4), AppUsageEventType.ACTIVITY_RESUMED, /*timestamp=*/ 5); assertAppUsageEvent( appUsageEventList.get(5), AppUsageEventType.ACTIVITY_STOPPED, /*timestamp=*/ 6); + + final Map>>>> + appUsagePeriodMap = dataProcessManager.getAppUsagePeriodMap(); + assertThat(appUsagePeriodMap.size()).isEqualTo(3); + // Day 1 + assertThat(appUsagePeriodMap.get(0).size()).isEqualTo(2); + Map>> hourlyMap = appUsagePeriodMap.get(0).get(0); + assertThat(hourlyMap).isNull(); + hourlyMap = appUsagePeriodMap.get(0).get(1); + assertThat(hourlyMap.size()).isEqualTo(1); + Map> userMap = hourlyMap.get(1L); + assertThat(userMap.size()).isEqualTo(1); + assertThat(userMap.get(packageName).size()).isEqualTo(1); + assertAppUsagePeriod(userMap.get(packageName).get(0), 3, 4); + // Day 2 + assertThat(appUsagePeriodMap.get(1).size()).isEqualTo(0); + // Day 3 + assertThat(appUsagePeriodMap.get(2).size()).isEqualTo(1); + hourlyMap = appUsagePeriodMap.get(2).get(0); + assertThat(hourlyMap.size()).isEqualTo(1); + userMap = hourlyMap.get(1L); + assertThat(userMap.size()).isEqualTo(1); + assertThat(userMap.get(packageName).size()).isEqualTo(1); + assertAppUsagePeriod(userMap.get(packageName).get(0), 5, 6); } @Test public void start_currentUserLocked_emptyAppUsageList() throws RemoteException { final UsageEvents.Event event = - getUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED, /*timestamp=*/ 1); + getUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED, /*timestamp=*/ 1, "package"); final List events = new ArrayList<>(); events.add(event); doReturn(getUsageEvents(events)) @@ -187,6 +244,7 @@ public final class DataProcessManagerTest { mDataProcessManager.start(); assertThat(mDataProcessManager.getAppUsageEventList()).isEmpty(); + assertThat(mDataProcessManager.getAppUsagePeriodMap()).isNull(); assertThat(mDataProcessManager.getShowScreenOnTime()).isFalse(); } @@ -296,10 +354,10 @@ public final class DataProcessManagerTest { } private UsageEvents.Event getUsageEvent( - final int eventType, final long timestamp) { + final int eventType, final long timestamp, final String packageName) { final UsageEvents.Event event = new UsageEvents.Event(); event.mEventType = eventType; - event.mPackage = "package"; + event.mPackage = packageName; event.mTimeStamp = timestamp; return event; } @@ -340,6 +398,12 @@ public final class DataProcessManagerTest { assertThat(event.getTimestamp()).isEqualTo(timestamp); } + private void assertAppUsagePeriod( + final AppUsagePeriod period, final long startTime, final long endTime) { + assertThat(period.getStartTime()).isEqualTo(startTime); + assertThat(period.getEndTime()).isEqualTo(endTime); + } + private static void verifyExpectedBatteryLevelData( final BatteryLevelData resultData, final List expectedDailyTimestamps, 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 aab3cb3a2f3..063934fea6e 100644 --- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DataProcessorTest.java +++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DataProcessorTest.java @@ -175,7 +175,108 @@ public final class DataProcessorTest { assertThat(DataProcessor.getAppUsageEventsForUser(mContext, userId, 0)).isNull(); } - @Test public void generateAppUsageEventListFromUsageEvents_returnExpectedResult() { + @Test + public void generateAppUsagePeriodMap_returnExpectedResult() { + final List hourlyBatteryLevelsPerDay = + new ArrayList<>(); + final String packageName = "com.android.settings"; + // Adds the day 1 data. + final List timestamps1 = List.of(10000L, 20000L, 30000L); + final List levels1 = List.of(100, 100, 100); + hourlyBatteryLevelsPerDay.add( + new BatteryLevelData.PeriodBatteryLevelData(timestamps1, levels1)); + // Adds the day 2 data. + hourlyBatteryLevelsPerDay.add(null); + // Adds the day 3 data. + final List timestamps2 = List.of(40000L, 50000L); + final List levels2 = List.of(100, 100); + hourlyBatteryLevelsPerDay.add( + new BatteryLevelData.PeriodBatteryLevelData(timestamps2, levels2)); + final long startTimestampOfLevelData = 10000L; + final List appUsageEventList = new ArrayList<>(); + // Adds some events before the start timestamp. + appUsageEventList.add(buildAppUsageEvent( + AppUsageEventType.ACTIVITY_RESUMED, /*timestamp=*/ 1, /*userId=*/ 1, + /*instanceId=*/ 2, packageName)); + appUsageEventList.add(buildAppUsageEvent( + AppUsageEventType.ACTIVITY_STOPPED, /*timestamp=*/ 2, /*userId=*/ 1, + /*instanceId=*/ 2, packageName)); + // Adds the valid app usage events. + appUsageEventList.add(buildAppUsageEvent( + AppUsageEventType.ACTIVITY_RESUMED, /*timestamp=*/ 10000L, /*userId=*/ 1, + /*instanceId=*/ 2, packageName)); + appUsageEventList.add(buildAppUsageEvent( + AppUsageEventType.ACTIVITY_STOPPED, /*timestamp=*/ 15000L, /*userId=*/ 1, + /*instanceId=*/ 2, packageName)); + appUsageEventList.add(buildAppUsageEvent( + AppUsageEventType.ACTIVITY_RESUMED, /*timestamp=*/ 12000L, /*userId=*/ 2, + /*instanceId=*/ 3, packageName)); + appUsageEventList.add(buildAppUsageEvent( + AppUsageEventType.ACTIVITY_STOPPED, /*timestamp=*/ 18000L, /*userId=*/ 2, + /*instanceId=*/ 3, packageName)); + appUsageEventList.add(buildAppUsageEvent( + AppUsageEventType.ACTIVITY_RESUMED, /*timestamp=*/ 35000L, /*userId=*/ 1, + /*instanceId=*/ 2, packageName)); + appUsageEventList.add(buildAppUsageEvent( + AppUsageEventType.ACTIVITY_STOPPED, /*timestamp=*/ 45000L, /*userId=*/ 1, + /*instanceId=*/ 2, packageName)); + appUsageEventList.add(buildAppUsageEvent( + AppUsageEventType.ACTIVITY_RESUMED, /*timestamp=*/ 42000L, /*userId=*/ 1, + /*instanceId=*/ 4, packageName)); + appUsageEventList.add(buildAppUsageEvent( + AppUsageEventType.ACTIVITY_STOPPED, /*timestamp=*/ 52000L, /*userId=*/ 1, + /*instanceId=*/ 4, packageName)); + appUsageEventList.add(buildAppUsageEvent( + AppUsageEventType.ACTIVITY_RESUMED, /*timestamp=*/ 55000L, /*userId=*/ 1, + /*instanceId=*/ 4, packageName)); + appUsageEventList.add(buildAppUsageEvent( + AppUsageEventType.ACTIVITY_STOPPED, /*timestamp=*/ 58000L, /*userId=*/ 1, + /*instanceId=*/ 4, packageName)); + + final Map>>>> periodMap = + DataProcessor.generateAppUsagePeriodMap( + hourlyBatteryLevelsPerDay, appUsageEventList, startTimestampOfLevelData); + + assertThat(periodMap.size()).isEqualTo(3); + // Day 1 + assertThat(periodMap.get(0).size()).isEqualTo(2); + Map>> hourlyMap = periodMap.get(0).get(0); + assertThat(hourlyMap.size()).isEqualTo(2); + Map> userMap = hourlyMap.get(1L); + assertThat(userMap.size()).isEqualTo(1); + assertThat(userMap.get(packageName).size()).isEqualTo(1); + assertAppUsagePeriod(userMap.get(packageName).get(0), 10000, 15000); + userMap = hourlyMap.get(2L); + assertThat(userMap.size()).isEqualTo(1); + assertThat(userMap.get(packageName).size()).isEqualTo(1); + assertAppUsagePeriod(userMap.get(packageName).get(0), 12000, 18000); + hourlyMap = periodMap.get(0).get(1); + assertThat(hourlyMap).isNull(); + // Day 2 + assertThat(periodMap.get(1).size()).isEqualTo(0); + // Day 3 + assertThat(periodMap.get(2).size()).isEqualTo(1); + hourlyMap = periodMap.get(2).get(0); + assertThat(hourlyMap.size()).isEqualTo(1); + userMap = hourlyMap.get(1L); + assertThat(userMap.size()).isEqualTo(1); + assertThat(userMap.get(packageName).size()).isEqualTo(2); + assertAppUsagePeriod(userMap.get(packageName).get(0), 40000, 45000); + assertAppUsagePeriod(userMap.get(packageName).get(1), 42000, 50000); + } + + @Test + public void generateAppUsagePeriodMap_emptyEventList_returnNull() { + final List hourlyBatteryLevelsPerDay = + new ArrayList<>(); + hourlyBatteryLevelsPerDay.add( + new BatteryLevelData.PeriodBatteryLevelData(new ArrayList<>(), new ArrayList<>())); + assertThat(DataProcessor.generateAppUsagePeriodMap( + hourlyBatteryLevelsPerDay, new ArrayList<>(), 0)).isNull(); + } + + @Test + public void generateAppUsageEventListFromUsageEvents_returnExpectedResult() { Event event1 = getUsageEvent(Event.NOTIFICATION_INTERRUPTION, /*timestamp=*/ 1); Event event2 = getUsageEvent(Event.ACTIVITY_RESUMED, /*timestamp=*/ 2); Event event3 = getUsageEvent(Event.ACTIVITY_STOPPED, /*timestamp=*/ 3); @@ -1214,6 +1315,150 @@ public final class DataProcessorTest { /*foregroundUsageTimeInMs=*/ 10, /*backgroundUsageTimeInMs=*/ 10); } + @Test + public void buildAppUsagePeriodList_returnExpectedResult() { + final List appUsageEvents = new ArrayList<>(); + final String packageName1 = "com.android.settings1"; + final String packageName2 = "com.android.settings2"; + // Fake multiple instances in one package. + appUsageEvents.add(buildAppUsageEvent( + AppUsageEventType.ACTIVITY_RESUMED, /*timestamp=*/ 1, /*userId=*/ 1, + /*instanceId=*/ 2, packageName1)); + appUsageEvents.add(buildAppUsageEvent( + AppUsageEventType.ACTIVITY_STOPPED, /*timestamp=*/ 2, /*userId=*/ 1, + /*instanceId=*/ 2, packageName1)); + appUsageEvents.add(buildAppUsageEvent( + AppUsageEventType.ACTIVITY_RESUMED, /*timestamp=*/ 3, /*userId=*/ 1, + /*instanceId=*/ 2, packageName1)); + appUsageEvents.add(buildAppUsageEvent( + AppUsageEventType.ACTIVITY_STOPPED, /*timestamp=*/ 4, /*userId=*/ 1, + /*instanceId=*/ 2, packageName1)); + appUsageEvents.add(buildAppUsageEvent( + AppUsageEventType.ACTIVITY_RESUMED, /*timestamp=*/ 2, /*userId=*/ 1, + /*instanceId=*/ 3, packageName1)); + appUsageEvents.add(buildAppUsageEvent( + AppUsageEventType.ACTIVITY_STOPPED, /*timestamp=*/ 4, /*userId=*/ 1, + /*instanceId=*/ 3, packageName1)); + appUsageEvents.add(buildAppUsageEvent( + AppUsageEventType.ACTIVITY_RESUMED, /*timestamp=*/ 2, /*userId=*/ 1, + /*instanceId=*/ 5, packageName2)); + appUsageEvents.add(buildAppUsageEvent( + AppUsageEventType.ACTIVITY_STOPPED, /*timestamp=*/ 4, /*userId=*/ 1, + /*instanceId=*/ 5, packageName2)); + // Fake one instance in one package. + appUsageEvents.add(buildAppUsageEvent( + AppUsageEventType.ACTIVITY_RESUMED, /*timestamp=*/ 1, /*userId=*/ 2, + /*instanceId=*/ 4, packageName2)); + appUsageEvents.add(buildAppUsageEvent( + AppUsageEventType.ACTIVITY_STOPPED, /*timestamp=*/ 2, /*userId=*/ 2, + /*instanceId=*/ 4, packageName2)); + appUsageEvents.add(buildAppUsageEvent( + AppUsageEventType.ACTIVITY_RESUMED, /*timestamp=*/ 3, /*userId=*/ 2, + /*instanceId=*/ 4, packageName2)); + appUsageEvents.add(buildAppUsageEvent( + AppUsageEventType.ACTIVITY_STOPPED, /*timestamp=*/ 4, /*userId=*/ 2, + /*instanceId=*/ 4, packageName2)); + + final Map>> appUsagePeriodMap = + DataProcessor.buildAppUsagePeriodList(appUsageEvents, 0, 5); + + assertThat(appUsagePeriodMap.size()).isEqualTo(2); + final Map> userMap1 = appUsagePeriodMap.get(1L); + assertThat(userMap1.size()).isEqualTo(2); + List appUsagePeriodList = userMap1.get(packageName1); + assertThat(appUsagePeriodList.size()).isEqualTo(3); + assertAppUsagePeriod(appUsagePeriodList.get(0), 1, 2); + assertAppUsagePeriod(appUsagePeriodList.get(1), 2, 4); + assertAppUsagePeriod(appUsagePeriodList.get(2), 3, 4); + appUsagePeriodList = userMap1.get(packageName2); + assertThat(appUsagePeriodList.size()).isEqualTo(1); + assertAppUsagePeriod(appUsagePeriodList.get(0), 2, 4); + final Map> userMap2 = appUsagePeriodMap.get(2L); + assertThat(userMap2.size()).isEqualTo(1); + appUsagePeriodList = userMap2.get(packageName2); + assertThat(appUsagePeriodList.size()).isEqualTo(2); + assertAppUsagePeriod(appUsagePeriodList.get(0), 1, 2); + assertAppUsagePeriod(appUsagePeriodList.get(1), 3, 4); + } + + @Test + public void buildAppUsagePeriodList_emptyEventList_returnNull() { + assertThat(DataProcessor.buildAppUsagePeriodList( + new ArrayList<>(), 0, 1)).isNull(); + } + + @Test + public void buildAppUsagePeriodList_emptyActivityList_returnNull() { + final List appUsageEvents = new ArrayList<>(); + appUsageEvents.add(buildAppUsageEvent( + AppUsageEventType.DEVICE_SHUTDOWN, /*timestamp=*/ 1)); + appUsageEvents.add(buildAppUsageEvent( + AppUsageEventType.DEVICE_SHUTDOWN, /*timestamp=*/ 2)); + + assertThat(DataProcessor.buildAppUsagePeriodList( + appUsageEvents, 0, 3)).isNull(); + } + + @Test + public void buildAppUsagePeriodListPerInstance_returnExpectedResult() { + final List appUsageEvents = new ArrayList<>(); + // Fake normal data. + appUsageEvents.add(buildAppUsageEvent( + AppUsageEventType.ACTIVITY_RESUMED, /*timestamp=*/ 100000)); + appUsageEvents.add(buildAppUsageEvent( + AppUsageEventType.ACTIVITY_STOPPED, /*timestamp=*/ 200000)); + // Fake two adjacent resume events. + appUsageEvents.add(buildAppUsageEvent( + AppUsageEventType.ACTIVITY_RESUMED, /*timestamp=*/ 300000)); + appUsageEvents.add(buildAppUsageEvent( + AppUsageEventType.ACTIVITY_RESUMED, /*timestamp=*/ 400000)); + appUsageEvents.add(buildAppUsageEvent( + AppUsageEventType.ACTIVITY_STOPPED, /*timestamp=*/ 500000)); + // Fake no start event when stop event happens. + appUsageEvents.add(buildAppUsageEvent( + AppUsageEventType.ACTIVITY_STOPPED, /*timestamp=*/ 600000)); + // There exists start event when device shutdown event happens. Shutdown is later than + // default complete time. + appUsageEvents.add(buildAppUsageEvent( + AppUsageEventType.ACTIVITY_RESUMED, /*timestamp=*/ 700000)); + appUsageEvents.add(buildAppUsageEvent( + AppUsageEventType.DEVICE_SHUTDOWN, /*timestamp=*/ 800000)); + // There exists start event when device shutdown event happens. Shutdown is earlier than + // default complete time. + appUsageEvents.add(buildAppUsageEvent( + AppUsageEventType.ACTIVITY_RESUMED, /*timestamp=*/ 900000)); + appUsageEvents.add(buildAppUsageEvent( + AppUsageEventType.DEVICE_SHUTDOWN, /*timestamp=*/ 910000)); + // There exists start event when the period ends. + appUsageEvents.add(buildAppUsageEvent( + AppUsageEventType.ACTIVITY_RESUMED, /*timestamp=*/ 1000000)); + + final List appUsagePeriodList = + DataProcessor.buildAppUsagePeriodListPerInstance(appUsageEvents, 0, 1100000); + + assertThat(appUsagePeriodList.size()).isEqualTo(6); + assertAppUsagePeriod(appUsagePeriodList.get(0), 100000, 200000); + assertAppUsagePeriod(appUsagePeriodList.get(1), 300000, 500000); + assertAppUsagePeriod(appUsagePeriodList.get(2), 570000, 600000); + assertAppUsagePeriod(appUsagePeriodList.get(3), 700000, 730000); + assertAppUsagePeriod(appUsagePeriodList.get(4), 900000, 910000); + assertAppUsagePeriod(appUsagePeriodList.get(5), 1000000, 1100000); + } + + @Test + public void buildAppUsagePeriodListPerInstance_notMetStart_returnExpectedResult() { + final List appUsageEvents = new ArrayList<>(); + // Start with stop event. + appUsageEvents.add(buildAppUsageEvent( + AppUsageEventType.ACTIVITY_STOPPED, /*timestamp=*/ 100000)); + + final List appUsagePeriodList = + DataProcessor.buildAppUsagePeriodListPerInstance(appUsageEvents, 0, 200000); + + assertThat(appUsageEvents.size()).isEqualTo(1); + assertAppUsagePeriod(appUsagePeriodList.get(0), 0, 100000); + } + private static Map> createHistoryMap( final long[] timestamps, final int[] levels) { final Map> batteryHistoryMap = new HashMap<>(); @@ -1292,12 +1537,39 @@ public final class DataProcessorTest { return event; } + private AppUsageEvent buildAppUsageEvent(final AppUsageEventType type, final long timestamp) { + return buildAppUsageEvent( + type, timestamp, /*userId=*/ 1, /*instanceId=*/ 2, + "com.android.settings"); + } + + private AppUsageEvent buildAppUsageEvent( + final AppUsageEventType type, + final long timestamp, + final long userId, + final int instanceId, + final String packageName) { + return AppUsageEvent.newBuilder() + .setType(type) + .setTimestamp(timestamp) + .setUserId(userId) + .setPackageName(packageName) + .setInstanceId(instanceId) + .build(); + } + private void assertAppUsageEvent( final AppUsageEvent event, final AppUsageEventType eventType, final long timestamp) { assertThat(event.getType()).isEqualTo(eventType); assertThat(event.getTimestamp()).isEqualTo(timestamp); } + private void assertAppUsagePeriod( + final AppUsagePeriod period, final long startTime, final long endTime) { + assertThat(period.getStartTime()).isEqualTo(startTime); + assertThat(period.getEndTime()).isEqualTo(endTime); + } + private static void verifyExpectedBatteryLevelData( final BatteryLevelData resultData, final List expectedDailyTimestamps,