diff --git a/src/com/android/settings/fuelgauge/batteryusage/DataProcessManager.java b/src/com/android/settings/fuelgauge/batteryusage/DataProcessManager.java new file mode 100644 index 00000000000..350bbcef0ba --- /dev/null +++ b/src/com/android/settings/fuelgauge/batteryusage/DataProcessManager.java @@ -0,0 +1,275 @@ +/* + * 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 android.app.usage.UsageEvents; +import android.content.Context; +import android.os.AsyncTask; +import android.os.Handler; +import android.os.UserHandle; +import android.os.UserManager; +import android.util.Log; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.settings.Utils; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Manages the async tasks to process battery and app usage data. + * + * For now, there exist 3 async tasks in this manager: + * + * + * The 3 async tasks will be started at the same time. + * + */ +public class DataProcessManager { + private static final String TAG = "DataProcessManager"; + + private final Handler mHandler; + private final DataProcessor.UsageMapAsyncResponse mCallbackFunction; + + private Context mContext; + private List mHourlyBatteryLevelsPerDay; + private Map> mBatteryHistoryMap; + + private boolean mIsCurrentBatteryHistoryLoaded = false; + private boolean mIsCurrentAppUsageLoaded = false; + private boolean mIsDatabaseAppUsageLoaded = false; + // Used to identify whether screen-on time data should be shown in the UI. + private boolean mShowScreenOnTime = true; + + private List mAppUsageEventList = new ArrayList<>(); + + /** + * Constructor when this exists battery level data. + */ + DataProcessManager( + Context context, + Handler handler, + final DataProcessor.UsageMapAsyncResponse callbackFunction, + final List hourlyBatteryLevelsPerDay, + final Map> batteryHistoryMap) { + mContext = context.getApplicationContext(); + mHandler = handler; + mCallbackFunction = callbackFunction; + mHourlyBatteryLevelsPerDay = hourlyBatteryLevelsPerDay; + mBatteryHistoryMap = batteryHistoryMap; + } + + /** + * Starts the async tasks to load battery history data and app usage data. + */ + public void start() { + // Load the latest battery history data from the service. + loadCurrentBatteryHistoryMap(); + // Load app usage list from database. + loadDatabaseAppUsageList(); + // Load the latest app usage list from the service. + loadCurrentAppUsageList(); + } + + @VisibleForTesting + List getAppUsageEventList() { + return mAppUsageEventList; + } + + @VisibleForTesting + boolean getIsCurrentAppUsageLoaded() { + return mIsCurrentAppUsageLoaded; + } + + @VisibleForTesting + boolean getIsDatabaseAppUsageLoaded() { + return mIsDatabaseAppUsageLoaded; + } + + @VisibleForTesting + boolean getIsCurrentBatteryHistoryLoaded() { + return mIsCurrentBatteryHistoryLoaded; + } + + @VisibleForTesting + boolean getShowScreenOnTime() { + return mShowScreenOnTime; + } + + private void loadCurrentBatteryHistoryMap() { + new AsyncTask>() { + @Override + protected Map doInBackground(Void... voids) { + final long startTime = System.currentTimeMillis(); + // Loads the current battery usage data from the battery stats service. + final Map currentBatteryHistoryMap = + DataProcessor.getCurrentBatteryHistoryMapFromStatsService( + mContext); + Log.d(TAG, String.format("execute loadCurrentBatteryHistoryMap size=%d in %d/ms", + currentBatteryHistoryMap.size(), (System.currentTimeMillis() - startTime))); + return currentBatteryHistoryMap; + } + + @Override + protected void onPostExecute( + final Map currentBatteryHistoryMap) { + if (mBatteryHistoryMap != null) { + // Replaces the placeholder in mBatteryHistoryMap. + for (Map.Entry> mapEntry + : mBatteryHistoryMap.entrySet()) { + if (mapEntry.getValue().containsKey( + DataProcessor.CURRENT_TIME_BATTERY_HISTORY_PLACEHOLDER)) { + mapEntry.setValue(currentBatteryHistoryMap); + } + } + } + mIsCurrentBatteryHistoryLoaded = true; + tryToGenerateFinalDataAndApplyCallback(); + } + }.execute(); + } + + private void loadCurrentAppUsageList() { + new AsyncTask>() { + @Override + protected List doInBackground(Void... voids) { + final long startTime = System.currentTimeMillis(); + // Loads the current battery usage data from the battery stats service. + final int currentUserId = getCurrentUserId(); + final int workProfileUserId = getWorkProfileUserId(); + final UsageEvents usageEventsForCurrentUser = + DataProcessor.getAppUsageEventsForUser(mContext, currentUserId); + // If fail to load usage events for current user, return null directly and screen-on + // time will not be shown in the UI. + if (usageEventsForCurrentUser == null) { + Log.w(TAG, "usageEventsForCurrentUser is null"); + return null; + } + UsageEvents usageEventsForWorkProfile = null; + if (workProfileUserId != Integer.MIN_VALUE) { + usageEventsForWorkProfile = + DataProcessor.getAppUsageEventsForUser( + mContext, workProfileUserId); + } else { + Log.d(TAG, "there is no work profile"); + } + + final Map usageEventsMap = new HashMap<>(); + usageEventsMap.put(Long.valueOf(currentUserId), usageEventsForCurrentUser); + if (usageEventsForWorkProfile != null) { + Log.d(TAG, "usageEventsForWorkProfile is null"); + usageEventsMap.put(Long.valueOf(workProfileUserId), usageEventsForWorkProfile); + } + + final List appUsageEventList = + DataProcessor.generateAppUsageEventListFromUsageEvents( + mContext, usageEventsMap); + Log.d(TAG, String.format("execute loadCurrentAppUsageList size=%d in %d/ms", + appUsageEventList.size(), (System.currentTimeMillis() - startTime))); + return appUsageEventList; + } + + @Override + protected void onPostExecute( + final List currentAppUsageList) { + final int currentUserId = getCurrentUserId(); + final UserManager userManager = mContext.getSystemService(UserManager.class); + // If current user is locked, don't show screen-on time data in the UI. + // Even if we have data in the database, we won't show screen-on time because we + // don't have the latest data. + if (userManager == null || !userManager.isUserUnlocked(currentUserId)) { + Log.d(TAG, "current user is locked"); + mShowScreenOnTime = false; + } else if (currentAppUsageList == null || currentAppUsageList.isEmpty()) { + Log.d(TAG, "usageEventsForWorkProfile is null or empty"); + } else { + mAppUsageEventList.addAll(currentAppUsageList); + } + mIsCurrentAppUsageLoaded = true; + tryToProcessAppUsageData(); + } + }.execute(); + } + + private void loadDatabaseAppUsageList() { + // TODO: load app usage data from database. + mIsDatabaseAppUsageLoaded = true; + tryToProcessAppUsageData(); + } + + 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) { + return; + } + processAppUsageData(); + tryToGenerateFinalDataAndApplyCallback(); + } + + private void processAppUsageData() { + // If there is no screen-on time data, no need to process. + if (!mShowScreenOnTime) { + return; + } + // TODO: process app usage data to an intermediate result for further use. + } + + private void tryToGenerateFinalDataAndApplyCallback() { + // Only when both battery history data and app usage events data has been loaded, start the + // final data processing. + if (!mIsCurrentBatteryHistoryLoaded + || !mIsCurrentAppUsageLoaded + || !mIsDatabaseAppUsageLoaded) { + return; + } + generateFinalDataAndApplyCallback(); + } + + private void generateFinalDataAndApplyCallback() { + // TODO: generate the final data including battery usage map and device screen-on time and + // then apply the callback function. + } + + private int getCurrentUserId() { + return mContext.getUserId(); + } + + private int getWorkProfileUserId() { + final UserHandle userHandle = + Utils.getManagedProfile( + mContext.getSystemService(UserManager.class)); + return userHandle != null ? userHandle.getIdentifier() : Integer.MIN_VALUE; + } +} diff --git a/src/com/android/settings/fuelgauge/batteryusage/DataProcessor.java b/src/com/android/settings/fuelgauge/batteryusage/DataProcessor.java index 2db6849a9d9..bc5c031d399 100644 --- a/src/com/android/settings/fuelgauge/batteryusage/DataProcessor.java +++ b/src/com/android/settings/fuelgauge/batteryusage/DataProcessor.java @@ -89,9 +89,6 @@ public final class DataProcessor { @VisibleForTesting static final int SELECTED_INDEX_ALL = BatteryChartViewModel.SELECTED_INDEX_ALL; - @VisibleForTesting - static final String CURRENT_TIME_BATTERY_HISTORY_PLACEHOLDER = - "CURRENT_TIME_BATTERY_HISTORY_PLACEHOLDER"; @VisibleForTesting static long sFakeCurrentTimeMillis = 0; @@ -101,6 +98,9 @@ public final class DataProcessor { IUsageStatsManager.Stub.asInterface( ServiceManager.getService(Context.USAGE_STATS_SERVICE)); + public static final String CURRENT_TIME_BATTERY_HISTORY_PLACEHOLDER = + "CURRENT_TIME_BATTERY_HISTORY_PLACEHOLDER"; + /** A callback listener when battery usage loading async task is executed. */ public interface UsageMapAsyncResponse { /** The callback function when batteryUsageMap is loaded. */ @@ -200,18 +200,9 @@ public final class DataProcessor { @Nullable public static Map getAppUsageEvents(Context context) { final long start = System.currentTimeMillis(); - final boolean isWorkProfileUser = DatabaseUtils.isWorkProfile(context); - Log.d(TAG, "getAppUsageEvents() 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; - } + context = DatabaseUtils.getOwnerContext(context); + if (context == null) { + return null; } final Map resultMap = new HashMap(); final UserManager userManager = context.getSystemService(UserManager.class); @@ -220,19 +211,9 @@ public final class DataProcessor { } final long sixDaysAgoTimestamp = DatabaseUtils.getTimestampSixDaysAgo(Calendar.getInstance()); - final String callingPackage = context.getPackageName(); - final long now = System.currentTimeMillis(); for (final UserInfo user : userManager.getAliveUsers()) { - // When the user is not unlocked, UsageStatsManager will return null, so bypass the - // following data loading logics directly. - if (!userManager.isUserUnlocked(user.id)) { - Log.w(TAG, "fail to load app usage event for user :" + user.id + " because locked"); - continue; - } - final long startTime = DatabaseUtils.getAppUsageStartTimestampOfUser( - context, user.id, sixDaysAgoTimestamp); final UsageEvents events = getAppUsageEventsForUser( - sUsageStatsManager, startTime, now, user.id, callingPackage); + context, userManager, user.id, sixDaysAgoTimestamp); if (events != null) { resultMap.put(Long.valueOf(user.id), events); } @@ -243,6 +224,30 @@ public final class DataProcessor { return resultMap.isEmpty() ? null : resultMap; } + /** + * Gets the {@link UsageEvents} from system service for the specific user. + */ + @Nullable + public static UsageEvents getAppUsageEventsForUser(Context context, final int userID) { + final long start = System.currentTimeMillis(); + context = DatabaseUtils.getOwnerContext(context); + if (context == null) { + return null; + } + final UserManager userManager = context.getSystemService(UserManager.class); + if (userManager == null) { + return null; + } + final long sixDaysAgoTimestamp = + DatabaseUtils.getTimestampSixDaysAgo(Calendar.getInstance()); + final UsageEvents events = getAppUsageEventsForUser( + context, userManager, userID, sixDaysAgoTimestamp); + final long elapsedTime = System.currentTimeMillis() - start; + Log.d(TAG, String.format("getAppUsageEventsForUser() for user %d in %d/ms", + userID, elapsedTime)); + return events; + } + /** * Closes the {@link BatteryUsageStats} after using it. */ @@ -335,6 +340,17 @@ public final class DataProcessor { return usageList; } + /** + * @return Returns the latest battery history map loaded from the battery stats service. + */ + public static Map getCurrentBatteryHistoryMapFromStatsService( + final Context context) { + final List batteryHistEntryList = + getBatteryHistListFromFromStatsService(context); + return batteryHistEntryList == null ? new HashMap<>() + : batteryHistEntryList.stream().collect(Collectors.toMap(e -> e.getKey(), e -> e)); + } + /** * @return Returns the processed history map which has interpolated to every hour data. * The start and end timestamp must be the even hours. @@ -621,6 +637,24 @@ public final class DataProcessor { @Nullable private static UsageEvents getAppUsageEventsForUser( + Context context, final UserManager userManager, final int userID, + final long sixDaysAgoTimestamp) { + final String callingPackage = context.getPackageName(); + final long now = System.currentTimeMillis(); + // When the user is not unlocked, UsageStatsManager will return null, so bypass the + // following data loading logics directly. + if (!userManager.isUserUnlocked(userID)) { + Log.w(TAG, "fail to load app usage event for user :" + userID + " because locked"); + return null; + } + final long startTime = DatabaseUtils.getAppUsageStartTimestampOfUser( + context, userID, sixDaysAgoTimestamp); + return loadAppUsageEventsForUserFromService( + sUsageStatsManager, startTime, now, userID, callingPackage); + } + + @Nullable + private static UsageEvents loadAppUsageEventsForUserFromService( final IUsageStatsManager usageStatsManager, final long startTime, final long endTime, final int userId, final String callingPackage) { final long start = System.currentTimeMillis(); @@ -672,14 +706,6 @@ public final class DataProcessor { return batteryHistEntryList; } - private static Map getCurrentBatteryHistoryMapFromStatsService( - final Context context) { - final List batteryHistEntryList = - getBatteryHistListFromFromStatsService(context); - return batteryHistEntryList == null ? new HashMap<>() - : batteryHistEntryList.stream().collect(Collectors.toMap(e -> e.getKey(), e -> e)); - } - @VisibleForTesting @Nullable static List convertToBatteryHistEntry( diff --git a/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtils.java b/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtils.java index d7c98a7f48d..f6b4e03d80e 100644 --- a/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtils.java +++ b/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtils.java @@ -194,6 +194,23 @@ public final class DatabaseUtils { return startCalendar.getTimeInMillis(); } + /** Returns the context with OWNER identity when current user is work profile. */ + public static Context getOwnerContext(Context context) { + final boolean isWorkProfileUser = isWorkProfile(context); + if (isWorkProfileUser) { + try { + return context.createPackageContextAsUser( + /*packageName=*/ context.getPackageName(), + /*flags=*/ 0, + /*user=*/ UserHandle.OWNER); + } catch (PackageManager.NameNotFoundException e) { + Log.e(TAG, "context.createPackageContextAsUser() fail:" + e); + return null; + } + } + return context; + } + static List sendAppUsageEventData( final Context context, final List appUsageEventList) { final long startTime = System.currentTimeMillis(); @@ -342,18 +359,9 @@ public final class DatabaseUtils { 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; - } + context = DatabaseUtils.getOwnerContext(context); + if (context == null) { + return null; } final Map> resultMap = new HashMap(); try (Cursor cursor = sFakeBatteryStateSupplier != null ? sFakeBatteryStateSupplier.get() : diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DataProcessManagerTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DataProcessManagerTest.java new file mode 100644 index 00000000000..a3578cb8f9d --- /dev/null +++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DataProcessManagerTest.java @@ -0,0 +1,139 @@ +/* + * 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.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; + +import android.app.usage.IUsageStatsManager; +import android.app.usage.UsageEvents; +import android.content.Context; +import android.os.Parcel; +import android.os.RemoteException; +import android.os.UserManager; + +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 java.util.ArrayList; +import java.util.List; + +@RunWith(RobolectricTestRunner.class) +public final class DataProcessManagerTest { + private Context mContext; + private DataProcessManager mDataProcessManager; + + @Mock + private IUsageStatsManager mUsageStatsManager; + @Mock + private UserManager mUserManager; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + mContext = spy(RuntimeEnvironment.application); + DataProcessor.sUsageStatsManager = mUsageStatsManager; + doReturn(mContext).when(mContext).getApplicationContext(); + doReturn(mUserManager) + .when(mContext) + .getSystemService(UserManager.class); + + mDataProcessManager = new DataProcessManager( + mContext, /*handler=*/ null, /*callbackFunction=*/ null, + /*hourlyBatteryLevelsPerDay=*/ null, /*batteryHistoryMap=*/ null); + } + + @Test + public void start_loadExpectedCurrentAppUsageData() throws RemoteException { + final UsageEvents.Event event1 = + getUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED, /*timestamp=*/ 1); + final UsageEvents.Event event2 = + getUsageEvent(UsageEvents.Event.ACTIVITY_STOPPED, /*timestamp=*/ 2); + final List events = new ArrayList<>(); + events.add(event1); + events.add(event2); + doReturn(getUsageEvents(events)) + .when(mUsageStatsManager) + .queryEventsForUser(anyLong(), anyLong(), anyInt(), any()); + doReturn(true).when(mUserManager).isUserUnlocked(anyInt()); + + mDataProcessManager.start(); + + assertThat(mDataProcessManager.getIsCurrentAppUsageLoaded()).isTrue(); + assertThat(mDataProcessManager.getIsDatabaseAppUsageLoaded()).isTrue(); + assertThat(mDataProcessManager.getIsCurrentBatteryHistoryLoaded()).isTrue(); + assertThat(mDataProcessManager.getShowScreenOnTime()).isTrue(); + final List appUsageEventList = mDataProcessManager.getAppUsageEventList(); + assertThat(appUsageEventList.size()).isEqualTo(2); + assertAppUsageEvent( + appUsageEventList.get(0), AppUsageEventType.ACTIVITY_RESUMED, /*timestamp=*/ 1); + assertAppUsageEvent( + appUsageEventList.get(1), AppUsageEventType.ACTIVITY_STOPPED, /*timestamp=*/ 2); + } + + @Test + public void start_currentUserLocked_emptyAppUsageList() throws RemoteException { + final UsageEvents.Event event = + getUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED, /*timestamp=*/ 1); + final List events = new ArrayList<>(); + events.add(event); + doReturn(getUsageEvents(events)) + .when(mUsageStatsManager) + .queryEventsForUser(anyLong(), anyLong(), anyInt(), any()); + doReturn(false).when(mUserManager).isUserUnlocked(anyInt()); + + mDataProcessManager.start(); + + assertThat(mDataProcessManager.getAppUsageEventList()).isEmpty(); + assertThat(mDataProcessManager.getShowScreenOnTime()).isFalse(); + } + + private UsageEvents getUsageEvents(final List events) { + UsageEvents usageEvents = new UsageEvents(events, new String[] {"package"}); + Parcel parcel = Parcel.obtain(); + parcel.setDataPosition(0); + usageEvents.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + return UsageEvents.CREATOR.createFromParcel(parcel); + } + + private UsageEvents.Event getUsageEvent( + final int eventType, final long timestamp) { + final UsageEvents.Event event = new UsageEvents.Event(); + event.mEventType = eventType; + event.mPackage = "package"; + event.mTimeStamp = timestamp; + return event; + } + + private void assertAppUsageEvent( + final AppUsageEvent event, final AppUsageEventType eventType, final long timestamp) { + assertThat(event.getType()).isEqualTo(eventType); + assertThat(event.getTimestamp()).isEqualTo(timestamp); + } +} 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 b1695ebf654..f0412dfa76e 100644 --- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DataProcessorTest.java +++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DataProcessorTest.java @@ -64,7 +64,7 @@ import java.util.Set; import java.util.TimeZone; @RunWith(RobolectricTestRunner.class) -public class DataProcessorTest { +public final class DataProcessorTest { private static final String FAKE_ENTRY_KEY = "fake_entry_key"; private Context mContext; @@ -177,7 +177,7 @@ public class DataProcessorTest { } @Test - public void getAppUsageEvents_lockedUser_returnNull() throws RemoteException { + public void getAppUsageEvents_lockedUser_returnNull() { UserInfo userInfo = new UserInfo(/*id=*/ 0, "user_0", /*flags=*/ 0); final List userInfoList = new ArrayList<>(); userInfoList.add(userInfo); @@ -205,6 +205,37 @@ public class DataProcessorTest { assertThat(resultMap).isNull(); } + @Test + public void getAppUsageEventsForUser_returnExpectedResult() throws RemoteException { + final int userId = 1; + doReturn(true).when(mUserManager).isUserUnlocked(userId); + doReturn(mUsageEvents1) + .when(mUsageStatsManager) + .queryEventsForUser(anyLong(), anyLong(), anyInt(), any()); + + assertThat(DataProcessor.getAppUsageEventsForUser(mContext, userId)) + .isEqualTo(mUsageEvents1); + } + + @Test + public void getAppUsageEventsForUser_lockedUser_returnNull() { + final int userId = 1; + // Test locked user. + doReturn(false).when(mUserManager).isUserUnlocked(userId); + + assertThat(DataProcessor.getAppUsageEventsForUser(mContext, userId)).isNull(); + } + + @Test + public void getAppUsageEventsForUser_nullUsageEvents_returnNull() throws RemoteException { + final int userId = 1; + doReturn(true).when(mUserManager).isUserUnlocked(userId); + doReturn(null) + .when(mUsageStatsManager).queryEventsForUser(anyLong(), anyLong(), anyInt(), any()); + + assertThat(DataProcessor.getAppUsageEventsForUser(mContext, userId)).isNull(); + } + @Test public void generateAppUsageEventListFromUsageEvents_returnExpectedResult() { Event event1 = getUsageEvent(Event.NOTIFICATION_INTERRUPTION, /*timestamp=*/ 1); Event event2 = getUsageEvent(Event.ACTIVITY_RESUMED, /*timestamp=*/ 2); @@ -231,11 +262,11 @@ public class DataProcessorTest { DataProcessor.generateAppUsageEventListFromUsageEvents(mContext, appUsageEvents); assertThat(appUsageEventList.size()).isEqualTo(3); - assetAppUsageEvent( + assertAppUsageEvent( appUsageEventList.get(0), AppUsageEventType.ACTIVITY_RESUMED, /*timestamp=*/ 2); - assetAppUsageEvent( + assertAppUsageEvent( appUsageEventList.get(1), AppUsageEventType.ACTIVITY_STOPPED, /*timestamp=*/ 3); - assetAppUsageEvent( + assertAppUsageEvent( appUsageEventList.get(2), AppUsageEventType.DEVICE_SHUTDOWN, /*timestamp=*/ 4); } @@ -1327,7 +1358,7 @@ public class DataProcessorTest { return event; } - private void assetAppUsageEvent( + private void assertAppUsageEvent( final AppUsageEvent event, final AppUsageEventType eventType, final long timestamp) { assertThat(event.getType()).isEqualTo(eventType); assertThat(event.getTimestamp()).isEqualTo(timestamp);