Implement the app usage data loading from database function.

Bug: 260964903
Test: make RunSettingsRoboTests + manual
Change-Id: I459dbdebe53e6b7421642955f36976b3e7c95fcb
This commit is contained in:
Kuan Wang
2022-12-19 19:59:13 +08:00
parent 092d07fa60
commit 97924455ff
12 changed files with 581 additions and 50 deletions

View File

@@ -37,6 +37,9 @@ import com.android.settings.fuelgauge.batteryusage.db.BatteryStateDatabase;
import java.time.Clock;
import java.time.Duration;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
/** {@link ContentProvider} class to fetch battery usage data. */
public class BatteryUsageContentProvider extends ContentProvider {
@@ -100,6 +103,8 @@ public class BatteryUsageContentProvider extends ContentProvider {
switch (sUriMatcher.match(uri)) {
case BATTERY_STATE_CODE:
return getBatteryStates(uri);
case APP_USAGE_EVENT_CODE:
return getAppUsageEvents(uri);
case APP_USAGE_LATEST_TIMESTAMP_CODE:
return getAppUsageLatestTimestamp(uri);
default:
@@ -153,8 +158,7 @@ public class BatteryUsageContentProvider extends ContentProvider {
}
private Cursor getBatteryStates(Uri uri) {
final long defaultTimestamp = mClock.millis() - QUERY_DURATION_HOURS.toMillis();
final long queryTimestamp = getQueryTimestamp(uri, defaultTimestamp);
final long queryTimestamp = getQueryTimestamp(uri);
return getBatteryStates(uri, queryTimestamp);
}
@@ -171,6 +175,24 @@ public class BatteryUsageContentProvider extends ContentProvider {
return cursor;
}
private Cursor getAppUsageEvents(Uri uri) {
final List<Long> queryUserIds = getQueryUserIds(uri);
if (queryUserIds == null || queryUserIds.isEmpty()) {
return null;
}
final long queryTimestamp = getQueryTimestamp(uri);
final long timestamp = mClock.millis();
Cursor cursor = null;
try {
cursor = mAppUsageEventDao.getAllForUsersAfter(queryUserIds, 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;
}
private Cursor getAppUsageLatestTimestamp(Uri uri) {
final long queryUserId = getQueryUserId(uri);
if (queryUserId == DatabaseUtils.INVALID_USER_ID) {
@@ -188,6 +210,26 @@ public class BatteryUsageContentProvider extends ContentProvider {
return cursor;
}
// If URI contains query parameter QUERY_KEY_USERID, use the value directly.
// Otherwise, return null.
private List<Long> getQueryUserIds(Uri uri) {
Log.d(TAG, "getQueryUserIds from uri: " + uri);
final String value = uri.getQueryParameter(DatabaseUtils.QUERY_KEY_USERID);
if (TextUtils.isEmpty(value)) {
Log.w(TAG, "empty query value");
return null;
}
try {
return Arrays.asList(value.split(","))
.stream()
.map(s -> Long.parseLong(s.trim()))
.collect(Collectors.toList());
} catch (NumberFormatException e) {
Log.e(TAG, "invalid query value: " + value, e);
return null;
}
}
// If URI contains query parameter QUERY_KEY_USERID, use the value directly.
// Otherwise, return INVALID_USER_ID.
private long getQueryUserId(Uri uri) {
@@ -198,8 +240,9 @@ public class BatteryUsageContentProvider extends ContentProvider {
// If URI contains query parameter QUERY_KEY_TIMESTAMP, use the value directly.
// Otherwise, load the data for QUERY_DURATION_HOURS by default.
private long getQueryTimestamp(Uri uri, long defaultTimestamp) {
private long getQueryTimestamp(Uri uri) {
Log.d(TAG, "getQueryTimestamp from uri: " + uri);
final long defaultTimestamp = mClock.millis() - QUERY_DURATION_HOURS.toMillis();
return getQueryValueFromUri(uri, DatabaseUtils.QUERY_KEY_TIMESTAMP, defaultTimestamp);
}

View File

@@ -209,6 +209,25 @@ public final class ConvertUtils {
return appUsageEventBuilder.build();
}
/** Converts to {@link AppUsageEvent} from {@link Cursor} */
public static AppUsageEvent convertToAppUsageEventFromCursor(final Cursor cursor) {
final AppUsageEvent.Builder eventBuilder = AppUsageEvent.newBuilder();
eventBuilder.setTimestamp(getLongFromCursor(cursor, AppUsageEventEntity.KEY_TIMESTAMP));
eventBuilder.setType(
AppUsageEventType.forNumber(
getIntegerFromCursor(
cursor, AppUsageEventEntity.KEY_APP_USAGE_EVENT_TYPE)));
eventBuilder.setPackageName(
getStringFromCursor(cursor, AppUsageEventEntity.KEY_PACKAGE_NAME));
eventBuilder.setInstanceId(
getIntegerFromCursor(cursor, AppUsageEventEntity.KEY_INSTANCE_ID));
eventBuilder.setTaskRootPackageName(
getStringFromCursor(cursor, AppUsageEventEntity.KEY_TASK_ROOT_PACKAGE_NAME));
eventBuilder.setUserId(getLongFromCursor(cursor, AppUsageEventEntity.KEY_USER_ID));
eventBuilder.setUid(getLongFromCursor(cursor, AppUsageEventEntity.KEY_UID));
return eventBuilder.build();
}
/** Converts UTC timestamp to human readable local time string. */
public static String utcToLocalTime(Context context, long timestamp) {
final Locale locale = getLocale(context);
@@ -331,4 +350,28 @@ public final class ConvertUtils {
return batteryInformationBuilder.build();
}
private static int getIntegerFromCursor(final Cursor cursor, final String key) {
final int columnIndex = cursor.getColumnIndex(key);
if (columnIndex >= 0) {
return cursor.getInt(columnIndex);
}
return 0;
}
private static long getLongFromCursor(final Cursor cursor, final String key) {
final int columnIndex = cursor.getColumnIndex(key);
if (columnIndex >= 0) {
return cursor.getLong(columnIndex);
}
return 0L;
}
private static String getStringFromCursor(final Cursor cursor, final String key) {
final int columnIndex = cursor.getColumnIndex(key);
if (columnIndex >= 0) {
return cursor.getString(columnIndex);
}
return "";
}
}

View File

@@ -24,10 +24,14 @@ import android.os.UserHandle;
import android.os.UserManager;
import android.util.Log;
import androidx.annotation.NonNull;
import com.android.internal.annotations.VisibleForTesting;
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;
@@ -63,9 +67,15 @@ public class DataProcessManager {
private final DataProcessor.UsageMapAsyncResponse mCallbackFunction;
private Context mContext;
private UserManager mUserManager;
private List<BatteryLevelData.PeriodBatteryLevelData> mHourlyBatteryLevelsPerDay;
private Map<Long, Map<String, BatteryHistEntry>> mBatteryHistoryMap;
// The start timestamp of battery level data. As we don't know when is the full charge cycle
// start time when loading app usage data, this value is used as the start time of querying app
// usage data.
private long mStartTimestampOfLevelData = 0;
private boolean mIsCurrentBatteryHistoryLoaded = false;
private boolean mIsCurrentAppUsageLoaded = false;
private boolean mIsDatabaseAppUsageLoaded = false;
@@ -81,13 +91,15 @@ public class DataProcessManager {
Context context,
Handler handler,
final DataProcessor.UsageMapAsyncResponse callbackFunction,
final List<BatteryLevelData.PeriodBatteryLevelData> hourlyBatteryLevelsPerDay,
final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap) {
@NonNull final List<BatteryLevelData.PeriodBatteryLevelData> hourlyBatteryLevelsPerDay,
@NonNull final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap) {
mContext = context.getApplicationContext();
mHandler = handler;
mUserManager = mContext.getSystemService(UserManager.class);
mCallbackFunction = callbackFunction;
mHourlyBatteryLevelsPerDay = hourlyBatteryLevelsPerDay;
mBatteryHistoryMap = batteryHistoryMap;
mStartTimestampOfLevelData = getStartTimestampOfBatteryLevelData();
}
/**
@@ -102,6 +114,21 @@ public class DataProcessManager {
loadCurrentAppUsageList();
}
@VisibleForTesting
long getStartTimestampOfBatteryLevelData() {
for (int dailyIndex = 0; dailyIndex < mHourlyBatteryLevelsPerDay.size(); dailyIndex++) {
if (mHourlyBatteryLevelsPerDay.get(dailyIndex) == null) {
continue;
}
final List<Long> timestamps =
mHourlyBatteryLevelsPerDay.get(dailyIndex).getTimestamps();
if (timestamps.size() > 0) {
return timestamps.get(0);
}
}
return 0;
}
@VisibleForTesting
List<AppUsageEvent> getAppUsageEventList() {
return mAppUsageEventList;
@@ -164,12 +191,17 @@ public class DataProcessManager {
new AsyncTask<Void, Void, List<AppUsageEvent>>() {
@Override
protected List<AppUsageEvent> doInBackground(Void... voids) {
if (!shouldLoadAppUsageData()) {
Log.d(TAG, "not loadCurrentAppUsageList");
return null;
}
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);
DataProcessor.getAppUsageEventsForUser(
mContext, currentUserId, mStartTimestampOfLevelData);
// 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) {
@@ -180,7 +212,7 @@ public class DataProcessManager {
if (workProfileUserId != Integer.MIN_VALUE) {
usageEventsForWorkProfile =
DataProcessor.getAppUsageEventsForUser(
mContext, workProfileUserId);
mContext, workProfileUserId, mStartTimestampOfLevelData);
} else {
Log.d(TAG, "there is no work profile");
}
@@ -203,16 +235,8 @@ public class DataProcessManager {
@Override
protected void onPostExecute(
final List<AppUsageEvent> 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");
if (currentAppUsageList == null || currentAppUsageList.isEmpty()) {
Log.d(TAG, "currentAppUsageList is null or empty");
} else {
mAppUsageEventList.addAll(currentAppUsageList);
}
@@ -223,9 +247,36 @@ public class DataProcessManager {
}
private void loadDatabaseAppUsageList() {
// TODO: load app usage data from database.
mIsDatabaseAppUsageLoaded = true;
tryToProcessAppUsageData();
new AsyncTask<Void, Void, List<AppUsageEvent>>() {
@Override
protected List<AppUsageEvent> doInBackground(Void... voids) {
if (!shouldLoadAppUsageData()) {
Log.d(TAG, "not loadDatabaseAppUsageList");
return null;
}
final long startTime = System.currentTimeMillis();
// Loads the current battery usage data from the battery stats service.
final List<AppUsageEvent> appUsageEventList =
DatabaseUtils.getAppUsageEventForUsers(
mContext, Calendar.getInstance(), getCurrentUserIds(),
mStartTimestampOfLevelData);
Log.d(TAG, String.format("execute loadDatabaseAppUsageList size=%d in %d/ms",
appUsageEventList.size(), (System.currentTimeMillis() - startTime)));
return appUsageEventList;
}
@Override
protected void onPostExecute(
final List<AppUsageEvent> databaseAppUsageList) {
if (databaseAppUsageList == null || databaseAppUsageList.isEmpty()) {
Log.d(TAG, "databaseAppUsageList is null or empty");
} else {
mAppUsageEventList.addAll(databaseAppUsageList);
}
mIsDatabaseAppUsageLoaded = true;
tryToProcessAppUsageData();
}
}.execute();
}
private void tryToProcessAppUsageData() {
@@ -243,6 +294,8 @@ 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.
}
@@ -262,14 +315,39 @@ public class DataProcessManager {
// then apply the callback function.
}
// Whether we should load app usage data from service or database.
private boolean shouldLoadAppUsageData() {
if (!mShowScreenOnTime) {
return false;
}
final int currentUserId = getCurrentUserId();
// If current user is locked, no need to load app usage data from service or database.
if (mUserManager == null || !mUserManager.isUserUnlocked(currentUserId)) {
Log.d(TAG, "shouldLoadAppUsageData: false, current user is locked");
mShowScreenOnTime = false;
return false;
}
return true;
}
// Returns the list of current user id and work profile id if exists.
private List<Integer> getCurrentUserIds() {
final List<Integer> userIds = new ArrayList<>();
userIds.add(getCurrentUserId());
final int workProfileUserId = getWorkProfileUserId();
if (workProfileUserId != Integer.MIN_VALUE) {
userIds.add(workProfileUserId);
}
return userIds;
}
private int getCurrentUserId() {
return mContext.getUserId();
}
private int getWorkProfileUserId() {
final UserHandle userHandle =
Utils.getManagedProfile(
mContext.getSystemService(UserManager.class));
Utils.getManagedProfile(mUserManager);
return userHandle != null ? userHandle.getIdentifier() : Integer.MIN_VALUE;
}
}

View File

@@ -100,6 +100,8 @@ public final class DataProcessor {
public static final String CURRENT_TIME_BATTERY_HISTORY_PLACEHOLDER =
"CURRENT_TIME_BATTERY_HISTORY_PLACEHOLDER";
public static final Comparator<AppUsageEvent> TIMESTAMP_COMPARATOR =
Comparator.comparing(AppUsageEvent::getTimestamp);
/** A callback listener when battery usage loading async task is executed. */
public interface UsageMapAsyncResponse {
@@ -228,7 +230,8 @@ public final class DataProcessor {
* Gets the {@link UsageEvents} from system service for the specific user.
*/
@Nullable
public static UsageEvents getAppUsageEventsForUser(Context context, final int userID) {
public static UsageEvents getAppUsageEventsForUser(
Context context, final int userID, final long startTimestampOfLevelData) {
final long start = System.currentTimeMillis();
context = DatabaseUtils.getOwnerContext(context);
if (context == null) {
@@ -240,8 +243,9 @@ public final class DataProcessor {
}
final long sixDaysAgoTimestamp =
DatabaseUtils.getTimestampSixDaysAgo(Calendar.getInstance());
final long earliestTimestamp = Math.max(sixDaysAgoTimestamp, startTimestampOfLevelData);
final UsageEvents events = getAppUsageEventsForUser(
context, userManager, userID, sixDaysAgoTimestamp);
context, userManager, userID, earliestTimestamp);
final long elapsedTime = System.currentTimeMillis() - start;
Log.d(TAG, String.format("getAppUsageEventsForUser() for user %d in %d/ms",
userID, elapsedTime));
@@ -638,7 +642,7 @@ public final class DataProcessor {
@Nullable
private static UsageEvents getAppUsageEventsForUser(
Context context, final UserManager userManager, final int userID,
final long sixDaysAgoTimestamp) {
final long earliestTimestamp) {
final String callingPackage = context.getPackageName();
final long now = System.currentTimeMillis();
// When the user is not unlocked, UsageStatsManager will return null, so bypass the
@@ -648,7 +652,7 @@ public final class DataProcessor {
return null;
}
final long startTime = DatabaseUtils.getAppUsageStartTimestampOfUser(
context, userID, sixDaysAgoTimestamp);
context, userID, earliestTimestamp);
return loadAppUsageEventsForUserFromService(
sUsageStatsManager, startTime, now, userID, callingPackage);
}

View File

@@ -46,6 +46,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
import java.util.stream.Collectors;
/** A utility class to operate battery usage database. */
public final class DatabaseUtils {
@@ -93,6 +94,8 @@ public final class DatabaseUtils {
@VisibleForTesting
static Supplier<Cursor> sFakeBatteryStateSupplier;
@VisibleForTesting
static Supplier<Cursor> sFakeAppUsageEventSupplier;
@VisibleForTesting
static Supplier<Cursor> sFakeAppUsageLatestTimestampSupplier;
private DatabaseUtils() {
@@ -125,6 +128,38 @@ public final class DatabaseUtils {
return Math.max(latestTimestamp, earliestTimestamp);
}
/** Returns the current user data in app usage event table. */
public static List<AppUsageEvent> getAppUsageEventForUsers(
Context context,
final Calendar calendar,
final List<Integer> userIds,
final long startTimestampOfLevelData) {
final long startTime = System.currentTimeMillis();
final long sixDaysAgoTimestamp = getTimestampSixDaysAgo(calendar);
final long queryTimestamp = Math.max(startTimestampOfLevelData, sixDaysAgoTimestamp);
Log.d(TAG, "sixDayAgoTimestamp: " + sixDaysAgoTimestamp);
final String queryUserIdString = userIds.stream()
.map(userId -> String.valueOf(userId))
.collect(Collectors.joining(","));
// Builds the content uri everytime to avoid cache.
final Uri appUsageEventUri =
new Uri.Builder()
.scheme(ContentResolver.SCHEME_CONTENT)
.authority(AUTHORITY)
.appendPath(APP_USAGE_EVENT_TABLE)
.appendQueryParameter(
QUERY_KEY_TIMESTAMP, Long.toString(queryTimestamp))
.appendQueryParameter(QUERY_KEY_USERID, queryUserIdString)
.build();
final List<AppUsageEvent> appUsageEventList =
loadAppUsageEventsFromContentProvider(context, appUsageEventUri);
Log.d(TAG, String.format("getAppUsageEventForUser userId=%s size=%d in %d/ms",
queryUserIdString, appUsageEventList.size(),
(System.currentTimeMillis() - startTime)));
return appUsageEventList;
}
/** Long: for timestamp and String: for BatteryHistEntry.getKey() */
public static Map<Long, Map<String, BatteryHistEntry>> getHistoryMapSinceLastFullCharge(
Context context, Calendar calendar) {
@@ -357,6 +392,32 @@ public final class DatabaseUtils {
}
}
private static List<AppUsageEvent> loadAppUsageEventsFromContentProvider(
Context context, Uri appUsageEventUri) {
final List<AppUsageEvent> appUsageEventList = new ArrayList<>();
context = getOwnerContext(context);
if (context == null) {
return appUsageEventList;
}
try (Cursor cursor = sFakeAppUsageEventSupplier != null
? sFakeAppUsageEventSupplier.get()
: context.getContentResolver().query(appUsageEventUri, null, null, null)) {
if (cursor == null || cursor.getCount() == 0) {
return appUsageEventList;
}
// Loads and recovers all AppUsageEvent data from cursor.
while (cursor.moveToNext()) {
appUsageEventList.add(ConvertUtils.convertToAppUsageEventFromCursor(cursor));
}
try {
cursor.close();
} catch (Exception e) {
Log.e(TAG, "cursor.close() failed", e);
}
}
return appUsageEventList;
}
private static Map<Long, Map<String, BatteryHistEntry>> loadHistoryMapFromContentProvider(
Context context, Uri batteryStateUri) {
context = DatabaseUtils.getOwnerContext(context);

View File

@@ -41,6 +41,11 @@ public interface AppUsageEventDao {
@Query("SELECT * FROM AppUsageEventEntity WHERE timestamp > :timestamp ORDER BY timestamp DESC")
List<AppUsageEventEntity> getAllAfter(long timestamp);
/** Gets the {@link Cursor} of all recorded data after a specific timestamp of the users. */
@Query("SELECT * FROM AppUsageEventEntity WHERE timestamp >= :timestamp"
+ " AND userId IN (:userIds) ORDER BY timestamp ASC")
Cursor getAllForUsersAfter(List<Long> userIds, long timestamp);
/** Gets the {@link Cursor} of the latest timestamp of the specific user. */
@Query("SELECT MAX(timestamp) as timestamp FROM AppUsageEventEntity WHERE userId = :userId")
Cursor getLatestTimestampOfUser(long userId);

View File

@@ -43,13 +43,18 @@ import org.robolectric.RobolectricTestRunner;
import org.robolectric.Shadows;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/** Tests for {@link BatteryUsageContentProvider}. */
@RunWith(RobolectricTestRunner.class)
public final class BatteryUsageContentProviderTest {
private static final Uri VALID_BATTERY_STATE_CONTENT_URI = DatabaseUtils.BATTERY_CONTENT_URI;
private static final long TIMESTAMP1 = System.currentTimeMillis();
private static final long TIMESTAMP2 = System.currentTimeMillis() + 2;
private static final long TIMESTAMP3 = System.currentTimeMillis() + 4;
private static final String PACKAGE_NAME1 = "com.android.settings1";
private static final String PACKAGE_NAME2 = "com.android.settings2";
private static final String PACKAGE_NAME3 = "com.android.settings3";
@@ -180,29 +185,49 @@ public final class BatteryUsageContentProviderTest {
BootBroadcastReceiver.ACTION_PERIODIC_JOB_RECHECK);
}
@Test
public void query_appUsageEvent_returnsExpectedResult() {
insertAppUsageEvent();
final List<Long> userIds1 = new ArrayList<>();
final long notExistingUserId = 3;
userIds1.add(USER_ID1);
userIds1.add(USER_ID2);
userIds1.add(notExistingUserId);
final Cursor cursor1 = getCursorOfAppUsage(userIds1, TIMESTAMP1);
assertThat(cursor1.getCount()).isEqualTo(3);
// Verifies the queried first battery state.
cursor1.moveToFirst();
assertThat(cursor1.getString(5 /*packageName*/)).isEqualTo(PACKAGE_NAME1);
// Verifies the queried second battery state.
cursor1.moveToNext();
assertThat(cursor1.getString(5 /*packageName*/)).isEqualTo(PACKAGE_NAME2);
// Verifies the queried third battery state.
cursor1.moveToNext();
assertThat(cursor1.getString(5 /*packageName*/)).isEqualTo(PACKAGE_NAME3);
final List<Long> userIds2 = new ArrayList<>();
userIds2.add(USER_ID1);
final Cursor cursor2 = getCursorOfAppUsage(userIds2, TIMESTAMP3);
assertThat(cursor2.getCount()).isEqualTo(1);
// Verifies the queried first battery state.
cursor2.moveToFirst();
assertThat(cursor2.getString(5 /*packageName*/)).isEqualTo(PACKAGE_NAME3);
}
@Test
public void query_appUsageTimestamp_returnsExpectedResult() throws Exception {
mProvider.onCreate();
final long timestamp1 = System.currentTimeMillis();
final long timestamp2 = timestamp1 + 2;
final long timestamp3 = timestamp1 + 4;
// Inserts some valid testing data.
BatteryTestUtils.insertDataToAppUsageEventTable(
mContext, USER_ID1, timestamp1, PACKAGE_NAME1);
BatteryTestUtils.insertDataToAppUsageEventTable(
mContext, USER_ID2, timestamp2, PACKAGE_NAME2);
BatteryTestUtils.insertDataToAppUsageEventTable(
mContext, USER_ID1, timestamp3, PACKAGE_NAME3);
insertAppUsageEvent();
final Cursor cursor1 = getCursorOfLatestTimestamp(USER_ID1);
assertThat(cursor1.getCount()).isEqualTo(1);
cursor1.moveToFirst();
assertThat(cursor1.getLong(0)).isEqualTo(timestamp3);
assertThat(cursor1.getLong(0)).isEqualTo(TIMESTAMP3);
final Cursor cursor2 = getCursorOfLatestTimestamp(USER_ID2);
assertThat(cursor2.getCount()).isEqualTo(1);
cursor2.moveToFirst();
assertThat(cursor2.getLong(0)).isEqualTo(timestamp2);
assertThat(cursor2.getLong(0)).isEqualTo(TIMESTAMP2);
final long notExistingUserId = 3;
final Cursor cursor3 = getCursorOfLatestTimestamp(notExistingUserId);
@@ -383,6 +408,17 @@ public final class BatteryUsageContentProviderTest {
return cursor;
}
private void insertAppUsageEvent() {
mProvider.onCreate();
// Inserts some valid testing data.
BatteryTestUtils.insertDataToAppUsageEventTable(
mContext, USER_ID1, TIMESTAMP1, PACKAGE_NAME1);
BatteryTestUtils.insertDataToAppUsageEventTable(
mContext, USER_ID2, TIMESTAMP2, PACKAGE_NAME2);
BatteryTestUtils.insertDataToAppUsageEventTable(
mContext, USER_ID1, TIMESTAMP3, PACKAGE_NAME3);
}
private Cursor getCursorOfLatestTimestamp(final long userId) {
final Uri appUsageLatestTimestampQueryContentUri =
new Uri.Builder()
@@ -394,10 +430,28 @@ public final class BatteryUsageContentProviderTest {
.build();
return mProvider.query(
appUsageLatestTimestampQueryContentUri,
/*strings=*/ null,
/*s=*/ null,
/*strings1=*/ null,
/*s1=*/ null);
appUsageLatestTimestampQueryContentUri,
/*strings=*/ null,
/*s=*/ null,
/*strings1=*/ null,
/*s1=*/ null);
}
private Cursor getCursorOfAppUsage(final List<Long> userIds, final long queryTimestamp) {
final String queryUserIdString = userIds.stream()
.map(userId -> String.valueOf(userId))
.collect(Collectors.joining(","));
final Uri appUsageEventUri =
new Uri.Builder()
.scheme(ContentResolver.SCHEME_CONTENT)
.authority(DatabaseUtils.AUTHORITY)
.appendPath(DatabaseUtils.APP_USAGE_EVENT_TABLE)
.appendQueryParameter(
DatabaseUtils.QUERY_KEY_TIMESTAMP, Long.toString(queryTimestamp))
.appendQueryParameter(DatabaseUtils.QUERY_KEY_USERID, queryUserIdString)
.build();
return mProvider.query(
appUsageEventUri, /*strings=*/ null, /*s=*/ null, /*strings1=*/ null, /*s1=*/ null);
}
}

View File

@@ -27,6 +27,7 @@ import android.app.usage.UsageEvents.Event;
import android.content.ContentValues;
import android.content.Context;
import android.content.pm.PackageManager;
import android.database.MatrixCursor;
import android.os.BatteryManager;
import android.os.BatteryUsageStats;
import android.os.LocaleList;
@@ -335,6 +336,68 @@ public final class ConvertUtilsTest {
assertThat(appUsageEvent).isNull();
}
@Test
public void convertToAppUsageEventFromCursor_returnExpectedResult() {
final MatrixCursor cursor = new MatrixCursor(
new String[]{
AppUsageEventEntity.KEY_UID,
AppUsageEventEntity.KEY_USER_ID,
AppUsageEventEntity.KEY_PACKAGE_NAME,
AppUsageEventEntity.KEY_TIMESTAMP,
AppUsageEventEntity.KEY_APP_USAGE_EVENT_TYPE,
AppUsageEventEntity.KEY_TASK_ROOT_PACKAGE_NAME,
AppUsageEventEntity.KEY_INSTANCE_ID});
cursor.addRow(
new Object[]{
101L,
1001L,
"com.android.settings1",
10001L,
AppUsageEventType.DEVICE_SHUTDOWN.getNumber(),
"com.android.settings2",
100001L});
cursor.moveToFirst();
final AppUsageEvent appUsageEvent = ConvertUtils.convertToAppUsageEventFromCursor(cursor);
assertThat(appUsageEvent.getUid()).isEqualTo(101L);
assertThat(appUsageEvent.getUserId()).isEqualTo(1001L);
assertThat(appUsageEvent.getPackageName()).isEqualTo("com.android.settings1");
assertThat(appUsageEvent.getTimestamp()).isEqualTo(10001L);
assertThat(appUsageEvent.getType()).isEqualTo(AppUsageEventType.DEVICE_SHUTDOWN);
assertThat(appUsageEvent.getTaskRootPackageName()).isEqualTo("com.android.settings2");
assertThat(appUsageEvent.getInstanceId()).isEqualTo(100001L);
}
@Test
public void convertToAppUsageEventFromCursor_emptyInstanceIdAndRootName_returnExpectedResult() {
final MatrixCursor cursor = new MatrixCursor(
new String[]{
AppUsageEventEntity.KEY_UID,
AppUsageEventEntity.KEY_USER_ID,
AppUsageEventEntity.KEY_PACKAGE_NAME,
AppUsageEventEntity.KEY_TIMESTAMP,
AppUsageEventEntity.KEY_APP_USAGE_EVENT_TYPE});
cursor.addRow(
new Object[]{
101L,
1001L,
"com.android.settings1",
10001L,
AppUsageEventType.DEVICE_SHUTDOWN.getNumber()});
cursor.moveToFirst();
final AppUsageEvent appUsageEvent = ConvertUtils.convertToAppUsageEventFromCursor(cursor);
assertThat(appUsageEvent.getUid()).isEqualTo(101L);
assertThat(appUsageEvent.getUserId()).isEqualTo(1001L);
assertThat(appUsageEvent.getPackageName()).isEqualTo("com.android.settings1");
assertThat(appUsageEvent.getTimestamp()).isEqualTo(10001L);
assertThat(appUsageEvent.getType()).isEqualTo(AppUsageEventType.DEVICE_SHUTDOWN);
assertThat(appUsageEvent.getTaskRootPackageName()).isEqualTo("");
assertThat(appUsageEvent.getInstanceId()).isEqualTo(0);
}
@Test
public void getLocale_nullContext_returnDefaultLocale() {
assertThat(ConvertUtils.getLocale(/*context=*/ null))

View File

@@ -27,10 +27,13 @@ import static org.mockito.Mockito.spy;
import android.app.usage.IUsageStatsManager;
import android.app.usage.UsageEvents;
import android.content.Context;
import android.database.MatrixCursor;
import android.os.Parcel;
import android.os.RemoteException;
import android.os.UserManager;
import com.android.settings.fuelgauge.batteryusage.db.AppUsageEventEntity;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -65,11 +68,31 @@ public final class DataProcessManagerTest {
mDataProcessManager = new DataProcessManager(
mContext, /*handler=*/ null, /*callbackFunction=*/ null,
/*hourlyBatteryLevelsPerDay=*/ null, /*batteryHistoryMap=*/ null);
/*hourlyBatteryLevelsPerDay=*/ new ArrayList<>(), /*batteryHistoryMap=*/ null);
}
@Test
public void start_loadExpectedCurrentAppUsageData() throws RemoteException {
public void start_loadEmptyDatabaseAppUsageData() {
final MatrixCursor cursor = new MatrixCursor(
new String[]{
AppUsageEventEntity.KEY_UID,
AppUsageEventEntity.KEY_PACKAGE_NAME,
AppUsageEventEntity.KEY_TIMESTAMP});
DatabaseUtils.sFakeAppUsageEventSupplier = () -> cursor;
doReturn(true).when(mUserManager).isUserUnlocked(anyInt());
mDataProcessManager.start();
assertThat(mDataProcessManager.getIsCurrentAppUsageLoaded()).isTrue();
assertThat(mDataProcessManager.getIsDatabaseAppUsageLoaded()).isTrue();
assertThat(mDataProcessManager.getIsCurrentBatteryHistoryLoaded()).isTrue();
assertThat(mDataProcessManager.getShowScreenOnTime()).isTrue();
assertThat(mDataProcessManager.getAppUsageEventList()).isEmpty();
}
@Test
public void start_loadExpectedAppUsageData() throws RemoteException {
// Fake current usage data.
final UsageEvents.Event event1 =
getUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED, /*timestamp=*/ 1);
final UsageEvents.Event event2 =
@@ -82,6 +105,22 @@ public final class DataProcessManagerTest {
.queryEventsForUser(anyLong(), anyLong(), anyInt(), any());
doReturn(true).when(mUserManager).isUserUnlocked(anyInt());
// Fake database usage data.
final MatrixCursor cursor = new MatrixCursor(
new String[]{
AppUsageEventEntity.KEY_APP_USAGE_EVENT_TYPE,
AppUsageEventEntity.KEY_TIMESTAMP});
// Adds fake data into the cursor.
cursor.addRow(new Object[] {
AppUsageEventType.ACTIVITY_RESUMED.getNumber(), /*timestamp=*/ 3});
cursor.addRow(new Object[] {
AppUsageEventType.ACTIVITY_RESUMED.getNumber(), /*timestamp=*/ 4});
cursor.addRow(new Object[] {
AppUsageEventType.ACTIVITY_STOPPED.getNumber(), /*timestamp=*/ 5});
cursor.addRow(new Object[] {
AppUsageEventType.ACTIVITY_STOPPED.getNumber(), /*timestamp=*/ 6});
DatabaseUtils.sFakeAppUsageEventSupplier = () -> cursor;
mDataProcessManager.start();
assertThat(mDataProcessManager.getIsCurrentAppUsageLoaded()).isTrue();
@@ -89,11 +128,19 @@ public final class DataProcessManagerTest {
assertThat(mDataProcessManager.getIsCurrentBatteryHistoryLoaded()).isTrue();
assertThat(mDataProcessManager.getShowScreenOnTime()).isTrue();
final List<AppUsageEvent> appUsageEventList = mDataProcessManager.getAppUsageEventList();
assertThat(appUsageEventList.size()).isEqualTo(2);
assertThat(appUsageEventList.size()).isEqualTo(6);
assertAppUsageEvent(
appUsageEventList.get(0), AppUsageEventType.ACTIVITY_RESUMED, /*timestamp=*/ 1);
assertAppUsageEvent(
appUsageEventList.get(1), AppUsageEventType.ACTIVITY_STOPPED, /*timestamp=*/ 2);
assertAppUsageEvent(
appUsageEventList.get(2), AppUsageEventType.ACTIVITY_RESUMED, /*timestamp=*/ 3);
assertAppUsageEvent(
appUsageEventList.get(3), AppUsageEventType.ACTIVITY_RESUMED, /*timestamp=*/ 4);
assertAppUsageEvent(
appUsageEventList.get(4), AppUsageEventType.ACTIVITY_STOPPED, /*timestamp=*/ 5);
assertAppUsageEvent(
appUsageEventList.get(5), AppUsageEventType.ACTIVITY_STOPPED, /*timestamp=*/ 6);
}
@Test
@@ -106,6 +153,14 @@ public final class DataProcessManagerTest {
.when(mUsageStatsManager)
.queryEventsForUser(anyLong(), anyLong(), anyInt(), any());
doReturn(false).when(mUserManager).isUserUnlocked(anyInt());
final MatrixCursor cursor = new MatrixCursor(
new String[]{
AppUsageEventEntity.KEY_UID,
AppUsageEventEntity.KEY_PACKAGE_NAME,
AppUsageEventEntity.KEY_TIMESTAMP});
// Adds fake data into the cursor.
cursor.addRow(new Object[] {101L, "app name1", 1001L});
DatabaseUtils.sFakeAppUsageEventSupplier = () -> cursor;
mDataProcessManager.start();
@@ -113,6 +168,42 @@ public final class DataProcessManagerTest {
assertThat(mDataProcessManager.getShowScreenOnTime()).isFalse();
}
@Test
public void getStartTimestampOfBatteryLevelData_returnExpectedResult() {
final List<BatteryLevelData.PeriodBatteryLevelData> hourlyBatteryLevelsPerDay =
new ArrayList<>();
final List<Long> timestamps = new ArrayList<>();
timestamps.add(101L);
timestamps.add(1001L);
final List<Integer> levels = new ArrayList<>();
levels.add(1);
levels.add(2);
hourlyBatteryLevelsPerDay.add(null);
hourlyBatteryLevelsPerDay.add(
new BatteryLevelData.PeriodBatteryLevelData(timestamps, levels));
final DataProcessManager dataProcessManager = new DataProcessManager(
mContext, /*handler=*/ null, /*callbackFunction=*/ null,
hourlyBatteryLevelsPerDay, /*batteryHistoryMap=*/ null);
assertThat(dataProcessManager.getStartTimestampOfBatteryLevelData()).isEqualTo(101);
}
@Test
public void getStartTimestampOfBatteryLevelData_emptyLevels_returnZero() {
final List<BatteryLevelData.PeriodBatteryLevelData> hourlyBatteryLevelsPerDay =
new ArrayList<>();
hourlyBatteryLevelsPerDay.add(null);
hourlyBatteryLevelsPerDay.add(
new BatteryLevelData.PeriodBatteryLevelData(new ArrayList<>(), new ArrayList<>()));
final DataProcessManager dataProcessManager = new DataProcessManager(
mContext, /*handler=*/ null, /*callbackFunction=*/ null,
hourlyBatteryLevelsPerDay, /*batteryHistoryMap=*/ null);
assertThat(dataProcessManager.getStartTimestampOfBatteryLevelData()).isEqualTo(0);
}
private UsageEvents getUsageEvents(final List<UsageEvents.Event> events) {
UsageEvents usageEvents = new UsageEvents(events, new String[] {"package"});
Parcel parcel = Parcel.obtain();

View File

@@ -213,7 +213,7 @@ public final class DataProcessorTest {
.when(mUsageStatsManager)
.queryEventsForUser(anyLong(), anyLong(), anyInt(), any());
assertThat(DataProcessor.getAppUsageEventsForUser(mContext, userId))
assertThat(DataProcessor.getAppUsageEventsForUser(mContext, userId, 0))
.isEqualTo(mUsageEvents1);
}
@@ -223,7 +223,7 @@ public final class DataProcessorTest {
// Test locked user.
doReturn(false).when(mUserManager).isUserUnlocked(userId);
assertThat(DataProcessor.getAppUsageEventsForUser(mContext, userId)).isNull();
assertThat(DataProcessor.getAppUsageEventsForUser(mContext, userId, 0)).isNull();
}
@Test
@@ -233,7 +233,7 @@ public final class DataProcessorTest {
doReturn(null)
.when(mUsageStatsManager).queryEventsForUser(anyLong(), anyLong(), anyInt(), any());
assertThat(DataProcessor.getAppUsageEventsForUser(mContext, userId)).isNull();
assertThat(DataProcessor.getAppUsageEventsForUser(mContext, userId, 0)).isNull();
}
@Test public void generateAppUsageEventListFromUsageEvents_returnExpectedResult() {
@@ -647,6 +647,7 @@ public final class DataProcessorTest {
public void getBatteryUsageMap_emptyHistoryMap_returnNull() {
final List<BatteryLevelData.PeriodBatteryLevelData> hourlyBatteryLevelsPerDay =
new ArrayList<>();
hourlyBatteryLevelsPerDay.add(
new BatteryLevelData.PeriodBatteryLevelData(new ArrayList<>(), new ArrayList<>()));

View File

@@ -276,6 +276,62 @@ public final class DatabaseUtilsTest {
mContext, /*userId=*/ 0, earliestTimestamp2)).isEqualTo(earliestTimestamp2);
}
@Test
public void getAppUsageEventForUsers_emptyCursorContent_returnEmptyMap() {
final MatrixCursor cursor = new MatrixCursor(
new String[]{
AppUsageEventEntity.KEY_UID,
AppUsageEventEntity.KEY_USER_ID,
AppUsageEventEntity.KEY_PACKAGE_NAME,
AppUsageEventEntity.KEY_TIMESTAMP,
AppUsageEventEntity.KEY_APP_USAGE_EVENT_TYPE});
DatabaseUtils.sFakeAppUsageEventSupplier = () -> cursor;
assertThat(DatabaseUtils.getAppUsageEventForUsers(
mContext,
/*calendar=*/ null,
/*userIds=*/ new ArrayList<>(),
/*startTimestampOfLevelData=*/ 0)).isEmpty();
}
@Test
public void getAppUsageEventForUsers_nullCursor_returnEmptyMap() {
DatabaseUtils.sFakeAppUsageEventSupplier = () -> null;
assertThat(DatabaseUtils.getAppUsageEventForUsers(
mContext,
/*calendar=*/ null,
/*userIds=*/ new ArrayList<>(),
/*startTimestampOfLevelData=*/ 0)).isEmpty();
}
@Test
public void getAppUsageEventForUsers_returnExpectedMap() {
final Long timestamp1 = 1001L;
final Long timestamp2 = 1002L;
final MatrixCursor cursor = new MatrixCursor(
new String[]{
AppUsageEventEntity.KEY_UID,
AppUsageEventEntity.KEY_PACKAGE_NAME,
AppUsageEventEntity.KEY_TIMESTAMP});
// Adds fake data into the cursor.
cursor.addRow(new Object[] {101L, "app name1", timestamp1});
cursor.addRow(new Object[] {101L, "app name2", timestamp2});
cursor.addRow(new Object[] {101L, "app name3", timestamp2});
cursor.addRow(new Object[] {101L, "app name4", timestamp2});
DatabaseUtils.sFakeAppUsageEventSupplier = () -> cursor;
final List<AppUsageEvent> appUsageEventList = DatabaseUtils.getAppUsageEventForUsers(
mContext,
/*calendar=*/ null,
/*userIds=*/ new ArrayList<>(),
/*startTimestampOfLevelData=*/ 0);
assertThat(appUsageEventList.get(0).getPackageName()).isEqualTo("app name1");
assertThat(appUsageEventList.get(1).getPackageName()).isEqualTo("app name2");
assertThat(appUsageEventList.get(2).getPackageName()).isEqualTo("app name3");
assertThat(appUsageEventList.get(3).getPackageName()).isEqualTo("app name4");
}
@Test
public void getHistoryMapSinceLastFullCharge_emptyCursorContent_returnEmptyMap() {
final MatrixCursor cursor = new MatrixCursor(

View File

@@ -31,11 +31,13 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import java.util.ArrayList;
import java.util.List;
/** Tests for {@link AppUsageEventDao}. */
@RunWith(RobolectricTestRunner.class)
public final class AppUsageEventDaoTest {
private static final int CURSOR_COLUMN_SIZE = 8;
private static final long TIMESTAMP1 = System.currentTimeMillis();
private static final long TIMESTAMP2 = System.currentTimeMillis() + 2;
private static final long TIMESTAMP3 = System.currentTimeMillis() + 4;
@@ -77,6 +79,36 @@ public final class AppUsageEventDaoTest {
assertAppUsageEvent(entities.get(1), TIMESTAMP2, PACKAGE_NAME2);
}
@Test
public void appUsageEventDao_getAllForUsersAfter() {
final List<Long> userIds1 = new ArrayList<>();
final long notExistingUserId = 3;
userIds1.add(USER_ID1);
userIds1.add(USER_ID2);
userIds1.add(notExistingUserId);
final Cursor cursor1 = mAppUsageEventDao.getAllForUsersAfter(userIds1, TIMESTAMP1);
assertThat(cursor1.getCount()).isEqualTo(3);
assertThat(cursor1.getColumnCount()).isEqualTo(CURSOR_COLUMN_SIZE);
// Verifies the queried first battery state.
cursor1.moveToFirst();
assertThat(cursor1.getString(5 /*packageName*/)).isEqualTo(PACKAGE_NAME1);
// Verifies the queried second battery state.
cursor1.moveToNext();
assertThat(cursor1.getString(5 /*packageName*/)).isEqualTo(PACKAGE_NAME2);
// Verifies the queried third battery state.
cursor1.moveToNext();
assertThat(cursor1.getString(5 /*packageName*/)).isEqualTo(PACKAGE_NAME3);
final List<Long> userIds2 = new ArrayList<>();
userIds2.add(USER_ID1);
final Cursor cursor2 = mAppUsageEventDao.getAllForUsersAfter(userIds2, TIMESTAMP3);
assertThat(cursor2.getCount()).isEqualTo(1);
assertThat(cursor2.getColumnCount()).isEqualTo(CURSOR_COLUMN_SIZE);
// Verifies the queried first battery state.
cursor2.moveToFirst();
assertThat(cursor2.getString(5 /*packageName*/)).isEqualTo(PACKAGE_NAME3);
}
@Test
public void appUsageEventDao_getLatestTimestampOfUser() throws Exception {
final Cursor cursor1 = mAppUsageEventDao.getLatestTimestampOfUser(USER_ID1);