Load app usage events data in the hourly job.
Test: make RunSettingsRoboTests + manual
Bug: 260964679
Change-Id: Iaccaa77bd52fb7356cdcb786c64523f21040b128
diff --git a/Android.bp b/Android.bp
index b5dfe79..dc7270e 100644
--- a/Android.bp
+++ b/Android.bp
@@ -80,6 +80,7 @@
"guava",
"jsr305",
"net-utils-framework-common",
+ "app-usage-event-protos-lite",
"settings-contextual-card-protos-lite",
"settings-log-bridge-protos-lite",
"settings-telephony-protos-lite",
diff --git a/src/com/android/settings/fuelgauge/PowerUsageFeatureProvider.java b/src/com/android/settings/fuelgauge/PowerUsageFeatureProvider.java
index 71d56ae..8029194 100644
--- a/src/com/android/settings/fuelgauge/PowerUsageFeatureProvider.java
+++ b/src/com/android/settings/fuelgauge/PowerUsageFeatureProvider.java
@@ -141,12 +141,17 @@
Intent getResumeChargeIntent(boolean isDockDefender);
/**
- * Returns {@link Set} for hidding applications background usage time.
+ * Returns {@link Set} for hiding applications background usage time.
*/
Set<CharSequence> getHideBackgroundUsageTimeSet(Context context);
/**
- * Returns package names for hidding application in the usage screen.
+ * Returns package names for hiding application in the usage screen.
*/
CharSequence[] getHideApplicationEntries(Context context);
+
+ /**
+ * Returns {@link Set} for ignoring task root class names for screen on time.
+ */
+ Set<CharSequence> getIgnoreScreenOnTimeTaskRootSet(Context context);
}
diff --git a/src/com/android/settings/fuelgauge/PowerUsageFeatureProviderImpl.java b/src/com/android/settings/fuelgauge/PowerUsageFeatureProviderImpl.java
index 932b35d..53d8701 100644
--- a/src/com/android/settings/fuelgauge/PowerUsageFeatureProviderImpl.java
+++ b/src/com/android/settings/fuelgauge/PowerUsageFeatureProviderImpl.java
@@ -165,4 +165,9 @@
public CharSequence[] getHideApplicationEntries(Context context) {
return new CharSequence[0];
}
+
+ @Override
+ public Set<CharSequence> getIgnoreScreenOnTimeTaskRootSet(Context context) {
+ return new ArraySet<>();
+ }
}
diff --git a/src/com/android/settings/fuelgauge/batteryusage/AppUsageDataLoader.java b/src/com/android/settings/fuelgauge/batteryusage/AppUsageDataLoader.java
new file mode 100644
index 0000000..c336fcd
--- /dev/null
+++ b/src/com/android/settings/fuelgauge/batteryusage/AppUsageDataLoader.java
@@ -0,0 +1,83 @@
+/*
+ * 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.util.Log;
+
+import androidx.annotation.VisibleForTesting;
+
+import java.util.List;
+import java.util.Map;
+import java.util.function.Supplier;
+
+/** Load app usage events data in the background. */
+public final class AppUsageDataLoader {
+ private static final String TAG = "AppUsageDataLoader";
+
+ // For testing only.
+ @VisibleForTesting
+ static Supplier<Map<Long, UsageEvents>> sFakeAppUsageEventsSupplier;
+ @VisibleForTesting
+ static Supplier<List<AppUsageEvent>> sFakeUsageEventsListSupplier;
+
+ private AppUsageDataLoader() {}
+
+ static void enqueueWork(final Context context) {
+ AsyncTask.execute(() -> {
+ Log.d(TAG, "loadAppUsageDataSafely() in the AsyncTask");
+ loadAppUsageDataSafely(context.getApplicationContext());
+ });
+ }
+
+ @VisibleForTesting
+ static void loadAppUsageData(final Context context) {
+ final long start = System.currentTimeMillis();
+ final Map<Long, UsageEvents> appUsageEvents =
+ sFakeAppUsageEventsSupplier != null
+ ? sFakeAppUsageEventsSupplier.get()
+ : DataProcessor.getAppUsageEvents(context);
+ if (appUsageEvents == null) {
+ Log.w(TAG, "loadAppUsageData() returns null");
+ return;
+ }
+ final List<AppUsageEvent> appUsageEventList =
+ sFakeUsageEventsListSupplier != null
+ ? sFakeUsageEventsListSupplier.get()
+ : DataProcessor.generateAppUsageEventListFromUsageEvents(
+ context, appUsageEvents);
+ if (appUsageEventList == null || appUsageEventList.isEmpty()) {
+ Log.w(TAG, "loadAppUsageData() returns null or empty content");
+ return;
+ }
+ final long elapsedTime = System.currentTimeMillis() - start;
+ Log.d(TAG, String.format("loadAppUsageData() size=%d in %d/ms", appUsageEventList.size(),
+ elapsedTime));
+ // Uploads the AppUsageEvent data into database.
+ DatabaseUtils.sendAppUsageEventData(context, appUsageEventList);
+ }
+
+ private static void loadAppUsageDataSafely(final Context context) {
+ try {
+ loadAppUsageData(context);
+ } catch (RuntimeException e) {
+ Log.e(TAG, "loadAppUsageData:" + e);
+ }
+ }
+}
diff --git a/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageContentProvider.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageContentProvider.java
index 4827f8f..4abcdc3 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageContentProvider.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageContentProvider.java
@@ -29,6 +29,8 @@
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
+import com.android.settings.fuelgauge.batteryusage.db.AppUsageEventDao;
+import com.android.settings.fuelgauge.batteryusage.db.AppUsageEventEntity;
import com.android.settings.fuelgauge.batteryusage.db.BatteryState;
import com.android.settings.fuelgauge.batteryusage.db.BatteryStateDao;
import com.android.settings.fuelgauge.batteryusage.db.BatteryStateDatabase;
@@ -43,11 +45,11 @@
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
public static final Duration QUERY_DURATION_HOURS = Duration.ofDays(6);
- @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
- public static final String QUERY_KEY_TIMESTAMP = "timestamp";
-
/** Codes */
private static final int BATTERY_STATE_CODE = 1;
+ private static final int APP_USAGE_LATEST_TIMESTAMP_CODE = 2;
+ private static final int APP_USAGE_EVENT_CODE = 3;
+
private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
static {
@@ -55,10 +57,19 @@
DatabaseUtils.AUTHORITY,
/*path=*/ DatabaseUtils.BATTERY_STATE_TABLE,
/*code=*/ BATTERY_STATE_CODE);
+ sUriMatcher.addURI(
+ DatabaseUtils.AUTHORITY,
+ /*path=*/ DatabaseUtils.APP_USAGE_LATEST_TIMESTAMP_PATH,
+ /*code=*/ APP_USAGE_LATEST_TIMESTAMP_CODE);
+ sUriMatcher.addURI(
+ DatabaseUtils.AUTHORITY,
+ /*path=*/ DatabaseUtils.APP_USAGE_EVENT_TABLE,
+ /*code=*/ APP_USAGE_EVENT_CODE);
}
private Clock mClock;
private BatteryStateDao mBatteryStateDao;
+ private AppUsageEventDao mAppUsageEventDao;
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
public void setClock(Clock clock) {
@@ -73,6 +84,7 @@
}
mClock = Clock.systemUTC();
mBatteryStateDao = BatteryStateDatabase.getInstance(getContext()).batteryStateDao();
+ mAppUsageEventDao = BatteryStateDatabase.getInstance(getContext()).appUsageEventDao();
Log.w(TAG, "create content provider from " + getCallingPackage());
return true;
}
@@ -88,6 +100,8 @@
switch (sUriMatcher.match(uri)) {
case BATTERY_STATE_CODE:
return getBatteryStates(uri);
+ case APP_USAGE_LATEST_TIMESTAMP_CODE:
+ return getAppUsageLatestTimestamp(uri);
default:
throw new IllegalArgumentException("unknown URI: " + uri);
}
@@ -111,6 +125,14 @@
Log.e(TAG, "insert() from:" + uri + " error:" + e);
return null;
}
+ case APP_USAGE_EVENT_CODE:
+ try {
+ mAppUsageEventDao.insert(AppUsageEventEntity.create(contentValues));
+ return uri;
+ } catch (RuntimeException e) {
+ Log.e(TAG, "insert() from:" + uri + " error:" + e);
+ return null;
+ }
default:
throw new IllegalArgumentException("unknown URI: " + uri);
}
@@ -145,24 +167,54 @@
Log.e(TAG, "query() from:" + uri + " error:" + e);
}
AsyncTask.execute(() -> BootBroadcastReceiver.invokeJobRecheck(getContext()));
- Log.w(TAG, "query battery states in " + (mClock.millis() - timestamp) + "/ms");
+ Log.d(TAG, "query battery states in " + (mClock.millis() - timestamp) + "/ms");
return cursor;
}
+ private Cursor getAppUsageLatestTimestamp(Uri uri) {
+ final long queryUserId = getQueryUserId(uri);
+ if (queryUserId == DatabaseUtils.INVALID_USER_ID) {
+ return null;
+ }
+ final long timestamp = mClock.millis();
+ Cursor cursor = null;
+ try {
+ cursor = mAppUsageEventDao.getLatestTimestampOfUser(queryUserId);
+ } catch (RuntimeException e) {
+ Log.e(TAG, "query() from:" + uri + " error:" + e);
+ }
+ Log.d(TAG, String.format("query app usage latest timestamp %d for user %d in %d/ms",
+ timestamp, queryUserId, (mClock.millis() - timestamp)));
+ return cursor;
+ }
+
+ // If URI contains query parameter QUERY_KEY_USERID, use the value directly.
+ // Otherwise, return INVALID_USER_ID.
+ private long getQueryUserId(Uri uri) {
+ Log.d(TAG, "getQueryUserId from uri: " + uri);
+ return getQueryValueFromUri(
+ uri, DatabaseUtils.QUERY_KEY_USERID, DatabaseUtils.INVALID_USER_ID);
+ }
+
// 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) {
- final String firstTimestampString = uri.getQueryParameter(QUERY_KEY_TIMESTAMP);
- if (TextUtils.isEmpty(firstTimestampString)) {
- Log.w(TAG, "empty query timestamp");
- return defaultTimestamp;
+ Log.d(TAG, "getQueryTimestamp from uri: " + uri);
+ return getQueryValueFromUri(uri, DatabaseUtils.QUERY_KEY_TIMESTAMP, defaultTimestamp);
+ }
+
+ private long getQueryValueFromUri(Uri uri, String key, long defaultValue) {
+ final String value = uri.getQueryParameter(key);
+ if (TextUtils.isEmpty(value)) {
+ Log.w(TAG, "empty query value");
+ return defaultValue;
}
try {
- return Long.parseLong(firstTimestampString);
+ return Long.parseLong(value);
} catch (NumberFormatException e) {
- Log.e(TAG, "invalid query timestamp: " + firstTimestampString, e);
- return defaultTimestamp;
+ Log.e(TAG, "invalid query value: " + value, e);
+ return defaultValue;
}
}
}
diff --git a/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageDataLoader.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageDataLoader.java
index 3cb5465..d446bb2 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageDataLoader.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageDataLoader.java
@@ -58,7 +58,7 @@
final long elapsedTime = System.currentTimeMillis() - start;
Log.d(TAG, String.format("getBatteryUsageStats() in %d/ms", elapsedTime));
- // Uploads the BatteryEntry data into SettingsIntelligence.
+ // Uploads the BatteryEntry data into database.
DatabaseUtils.sendBatteryEntryData(
context, batteryEntryList, batteryUsageStats, isFullChargeStart);
DataProcessor.closeBatteryUsageStats(batteryUsageStats);
diff --git a/src/com/android/settings/fuelgauge/batteryusage/ConvertUtils.java b/src/com/android/settings/fuelgauge/batteryusage/ConvertUtils.java
index c5c0522..38879d9 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/ConvertUtils.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/ConvertUtils.java
@@ -16,8 +16,11 @@
package com.android.settings.fuelgauge.batteryusage;
import android.annotation.IntDef;
+import android.annotation.Nullable;
+import android.app.usage.UsageEvents.Event;
import android.content.ContentValues;
import android.content.Context;
+import android.content.pm.PackageManager;
import android.database.Cursor;
import android.os.BatteryUsageStats;
import android.os.Build;
@@ -25,10 +28,12 @@
import android.os.UserHandle;
import android.text.format.DateFormat;
import android.util.Base64;
+import android.util.Log;
import androidx.annotation.VisibleForTesting;
import com.android.settings.fuelgauge.BatteryUtils;
+import com.android.settings.fuelgauge.batteryusage.db.AppUsageEventEntity;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -49,7 +54,7 @@
CONSUMER_TYPE_SYSTEM_BATTERY,
})
@Retention(RetentionPolicy.SOURCE)
- public static @interface ConsumerType {
+ public @interface ConsumerType {
}
public static final int CONSUMER_TYPE_UNKNOWN = 0;
@@ -60,8 +65,8 @@
private ConvertUtils() {
}
- /** Converts to content values */
- public static ContentValues convertToContentValues(
+ /** Converts {@link BatteryEntry} to content values */
+ public static ContentValues convertBatteryEntryToContentValues(
final BatteryEntry entry,
final BatteryUsageStats batteryUsageStats,
final int batteryLevel,
@@ -103,6 +108,19 @@
return values;
}
+ /** Converts {@link AppUsageEvent} to content values */
+ public static ContentValues convertAppUsageEventToContentValues(final AppUsageEvent event) {
+ final ContentValues values = new ContentValues();
+ values.put(AppUsageEventEntity.KEY_UID, event.getUid());
+ values.put(AppUsageEventEntity.KEY_USER_ID, event.getUserId());
+ values.put(AppUsageEventEntity.KEY_TIMESTAMP, event.getTimestamp());
+ values.put(AppUsageEventEntity.KEY_APP_USAGE_EVENT_TYPE, event.getType().getNumber());
+ values.put(AppUsageEventEntity.KEY_PACKAGE_NAME, event.getPackageName());
+ values.put(AppUsageEventEntity.KEY_INSTANCE_ID, event.getInstanceId());
+ values.put(AppUsageEventEntity.KEY_TASK_ROOT_PACKAGE_NAME, event.getTaskRootPackageName());
+ return values;
+ }
+
/** Gets the encoded string from {@link BatteryInformation} instance. */
public static String convertBatteryInformationToString(
final BatteryInformation batteryInformation) {
@@ -135,7 +153,7 @@
BatteryEntry entry,
BatteryUsageStats batteryUsageStats) {
return new BatteryHistEntry(
- convertToContentValues(
+ convertBatteryEntryToContentValues(
entry,
batteryUsageStats,
/*batteryLevel=*/ 0,
@@ -146,6 +164,51 @@
/*isFullChargeStart=*/ false));
}
+ /** Converts to {@link AppUsageEvent} from {@link Event} */
+ @Nullable
+ public static AppUsageEvent convertToAppUsageEvent(
+ Context context, final Event event, final long userId) {
+ if (event.getPackageName() == null) {
+ // See b/190609174: Event package names should never be null, but sometimes they are.
+ // Note that system events like device shutting down should still come with the android
+ // package name.
+ Log.w(TAG, String.format(
+ "Ignoring a usage event with null package name (timestamp=%d, type=%d)",
+ event.getTimeStamp(), event.getEventType()));
+ return null;
+ }
+
+ final AppUsageEvent.Builder appUsageEventBuilder = AppUsageEvent.newBuilder();
+ appUsageEventBuilder
+ .setTimestamp(event.getTimeStamp())
+ .setType(getAppUsageEventType(event.getEventType()))
+ .setPackageName(event.getPackageName())
+ .setUserId(userId);
+
+ try {
+ final long uid = context
+ .getPackageManager()
+ .getPackageUidAsUser(event.getPackageName(), (int) userId);
+ appUsageEventBuilder.setUid(uid);
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.w(TAG, String.format(
+ "Fail to get uid for package %s of user %d)", event.getPackageName(), userId));
+ return null;
+ }
+
+ try {
+ appUsageEventBuilder.setInstanceId(event.getInstanceId());
+ } catch (NoClassDefFoundError | NoSuchMethodError e) {
+ Log.w(TAG, "UsageEvent instance ID API error");
+ }
+ String taskRootPackageName = getTaskRootPackageName(event);
+ if (taskRootPackageName != null) {
+ appUsageEventBuilder.setTaskRootPackageName(taskRootPackageName);
+ }
+
+ return appUsageEventBuilder.build();
+ }
+
/** Converts UTC timestamp to human readable local time string. */
public static String utcToLocalTime(Context context, long timestamp) {
final Locale locale = getLocale(context);
@@ -185,6 +248,50 @@
: Locale.getDefault();
}
+ /**
+ * Returns the package name of the task root when this event was reported when {@code event} is
+ * one of:
+ *
+ * <ul>
+ * <li>{@link Event#ACTIVITY_RESUMED}
+ * <li>{@link Event#ACTIVITY_STOPPED}
+ * </ul>
+ */
+ @Nullable
+ private static String getTaskRootPackageName(Event event) {
+ int eventType = event.getEventType();
+ if (eventType != Event.ACTIVITY_RESUMED && eventType != Event.ACTIVITY_STOPPED) {
+ // Task root is only relevant for ACTIVITY_* events.
+ return null;
+ }
+
+ try {
+ String taskRootPackageName = event.getTaskRootPackageName();
+ if (taskRootPackageName == null) {
+ Log.w(TAG, String.format(
+ "Null task root in event with timestamp %d, type=%d, package %s",
+ event.getTimeStamp(), event.getEventType(), event.getPackageName()));
+ }
+ return taskRootPackageName;
+ } catch (NoSuchMethodError e) {
+ Log.w(TAG, "Failed to call Event#getTaskRootPackageName()");
+ return null;
+ }
+ }
+
+ private static AppUsageEventType getAppUsageEventType(final int eventType) {
+ switch (eventType) {
+ case Event.ACTIVITY_RESUMED:
+ return AppUsageEventType.ACTIVITY_RESUMED;
+ case Event.ACTIVITY_STOPPED:
+ return AppUsageEventType.ACTIVITY_STOPPED;
+ case Event.DEVICE_SHUTDOWN:
+ return AppUsageEventType.DEVICE_SHUTDOWN;
+ default:
+ return AppUsageEventType.UNKNOWN;
+ }
+ }
+
private static BatteryInformation constructBatteryInformation(
final BatteryEntry entry,
final BatteryUsageStats batteryUsageStats,
diff --git a/src/com/android/settings/fuelgauge/batteryusage/DataProcessor.java b/src/com/android/settings/fuelgauge/batteryusage/DataProcessor.java
index 9d2774e..e3e1912 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/DataProcessor.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/DataProcessor.java
@@ -18,10 +18,14 @@
import static com.android.settings.fuelgauge.batteryusage.ConvertUtils.utcToLocalTime;
+import android.app.usage.IUsageStatsManager;
+import android.app.usage.UsageEvents;
+import android.app.usage.UsageEvents.Event;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
+import android.content.pm.UserInfo;
import android.os.AsyncTask;
import android.os.BatteryConsumer;
import android.os.BatteryStatsManager;
@@ -30,6 +34,8 @@
import android.os.Handler;
import android.os.Looper;
import android.os.Process;
+import android.os.RemoteException;
+import android.os.ServiceManager;
import android.os.UidBatteryConsumer;
import android.os.UserBatteryConsumer;
import android.os.UserHandle;
@@ -90,6 +96,11 @@
@VisibleForTesting
static long sFakeCurrentTimeMillis = 0;
+ @VisibleForTesting
+ static IUsageStatsManager sUsageStatsManager =
+ IUsageStatsManager.Stub.asInterface(
+ ServiceManager.getService(Context.USAGE_STATS_SERVICE));
+
/** A callback listener when battery usage loading async task is executed. */
public interface UsageMapAsyncResponse {
/** The callback function when batteryUsageMap is loaded. */
@@ -184,6 +195,55 @@
}
/**
+ * Gets the {@link UsageEvents} from system service for all unlocked users.
+ */
+ @Nullable
+ public static Map<Long, UsageEvents> 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;
+ }
+ }
+ final Map<Long, UsageEvents> resultMap = new HashMap();
+ final UserManager userManager = context.getSystemService(UserManager.class);
+ if (userManager == null) {
+ return null;
+ }
+ 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);
+ if (events != null) {
+ resultMap.put(Long.valueOf(user.id), events);
+ }
+ }
+ final long elapsedTime = System.currentTimeMillis() - start;
+ Log.d(TAG, String.format("getAppUsageEvents() for all unlocked users in %d/ms",
+ elapsedTime));
+ return resultMap.isEmpty() ? null : resultMap;
+ }
+
+ /**
* Closes the {@link BatteryUsageStats} after using it.
*/
public static void closeBatteryUsageStats(BatteryUsageStats batteryUsageStats) {
@@ -197,6 +257,59 @@
}
/**
+ * Generates the list of {@link AppUsageEvent} from the supplied {@link UsageEvents}.
+ */
+ public static List<AppUsageEvent> generateAppUsageEventListFromUsageEvents(
+ Context context, Map<Long, UsageEvents> usageEventsMap) {
+ final List<AppUsageEvent> appUsageEventList = new ArrayList<>();
+ long numEventsFetched = 0;
+ long numAllEventsFetched = 0;
+ final Set<CharSequence> ignoreScreenOnTimeTaskRootSet =
+ FeatureFactory.getFactory(context)
+ .getPowerUsageFeatureProvider(context)
+ .getIgnoreScreenOnTimeTaskRootSet(context);
+ for (final long userId : usageEventsMap.keySet()) {
+ final UsageEvents usageEvents = usageEventsMap.get(userId);
+ while (usageEvents.hasNextEvent()) {
+ final Event event = new Event();
+ usageEvents.getNextEvent(event);
+ numAllEventsFetched++;
+ switch (event.getEventType()) {
+ case Event.ACTIVITY_RESUMED:
+ case Event.ACTIVITY_STOPPED:
+ case Event.DEVICE_SHUTDOWN:
+ final String taskRootClassName = event.getTaskRootClassName();
+ if (!TextUtils.isEmpty(taskRootClassName)
+ && !ignoreScreenOnTimeTaskRootSet.isEmpty()
+ && contains(
+ taskRootClassName, ignoreScreenOnTimeTaskRootSet)) {
+ Log.w(TAG, String.format(
+ "Ignoring a usage event with task root class name %s, "
+ + "(timestamp=%d, type=%d)",
+ taskRootClassName,
+ event.getTimeStamp(),
+ event.getEventType()));
+ break;
+ }
+ final AppUsageEvent appUsageEvent =
+ ConvertUtils.convertToAppUsageEvent(context, event, userId);
+ if (appUsageEvent != null) {
+ numEventsFetched++;
+ appUsageEventList.add(appUsageEvent);
+ }
+ break;
+ default:
+ break;
+ }
+ }
+ }
+ Log.w(TAG, String.format(
+ "Read %d relevant events (%d total) from UsageStatsManager", numEventsFetched,
+ numAllEventsFetched));
+ return appUsageEventList;
+ }
+
+ /**
* Generates the list of {@link BatteryEntry} from the supplied {@link BatteryUsageStats}.
*/
@Nullable
@@ -508,13 +621,30 @@
asyncResponseDelegate).execute();
}
+ @Nullable
+ private static UsageEvents getAppUsageEventsForUser(
+ final IUsageStatsManager usageStatsManager, final long startTime, final long endTime,
+ final int userId, final String callingPackage) {
+ final long start = System.currentTimeMillis();
+ UsageEvents events = null;
+ try {
+ events = usageStatsManager.queryEventsForUser(
+ startTime, endTime, userId, callingPackage);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error fetching usage events: ", e);
+ }
+ final long elapsedTime = System.currentTimeMillis() - start;
+ Log.d(TAG, String.format("getAppUsageEventsForUser(): %d from %d to %d in %d/ms", userId,
+ startTime, endTime, elapsedTime));
+ return events;
+ }
+
/**
* @return Returns the overall battery usage data from battery stats service directly.
*
* The returned value should be always a 2d map and composed by only 1 part:
* - [SELECTED_INDEX_ALL][SELECTED_INDEX_ALL]
*/
- @Nullable
private static Map<Integer, Map<Integer, BatteryDiffData>> getBatteryUsageMapFromStatsService(
final Context context) {
final Map<Integer, Map<Integer, BatteryDiffData>> resultMap = new HashMap<>();
@@ -1335,6 +1465,7 @@
return calendar.getTimeInMillis();
}
+ /** Whether the Set contains the target. */
private static boolean contains(String target, Set<CharSequence> packageNames) {
if (target != null && packageNames != null) {
for (CharSequence packageName : packageNames) {
diff --git a/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtils.java b/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtils.java
index 8ff802d..d7c98a7 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtils.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtils.java
@@ -45,12 +45,11 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.function.Supplier;
/** A utility class to operate battery usage database. */
public final class DatabaseUtils {
private static final String TAG = "DatabaseUtils";
- /** Key for query parameter timestamp used in BATTERY_CONTENT_URI **/
- private static final String QUERY_KEY_TIMESTAMP = "timestamp";
/** Clear memory threshold for device booting phase. **/
private static final long CLEAR_MEMORY_THRESHOLD_MS = Duration.ofMinutes(5).toMillis();
private static final long CLEAR_MEMORY_DELAYED_MS = Duration.ofSeconds(2).toMillis();
@@ -62,8 +61,18 @@
public static final String AUTHORITY = "com.android.settings.battery.usage.provider";
/** A table name for battery usage history. */
public static final String BATTERY_STATE_TABLE = "BatteryState";
+ /** A table name for app usage events. */
+ public static final String APP_USAGE_EVENT_TABLE = "AppUsageEvent";
+ /** A path name for app usage latest timestamp query. */
+ public static final String APP_USAGE_LATEST_TIMESTAMP_PATH = "appUsageLatestTimestamp";
/** A class name for battery usage data provider. */
public static final String SETTINGS_PACKAGE_PATH = "com.android.settings";
+ /** Key for query parameter timestamp used in BATTERY_CONTENT_URI **/
+ public static final String QUERY_KEY_TIMESTAMP = "timestamp";
+ /** Key for query parameter userid used in APP_USAGE_EVENT_URI **/
+ public static final String QUERY_KEY_USERID = "userid";
+
+ public static final long INVALID_USER_ID = Integer.MIN_VALUE;
/** A content URI to access battery usage states data. */
public static final Uri BATTERY_CONTENT_URI =
@@ -72,6 +81,19 @@
.authority(AUTHORITY)
.appendPath(BATTERY_STATE_TABLE)
.build();
+ /** A content URI to access app usage events data. */
+ public static final Uri APP_USAGE_EVENT_URI =
+ new Uri.Builder()
+ .scheme(ContentResolver.SCHEME_CONTENT)
+ .authority(AUTHORITY)
+ .appendPath(APP_USAGE_EVENT_TABLE)
+ .build();
+
+ // For testing only.
+ @VisibleForTesting
+ static Supplier<Cursor> sFakeBatteryStateSupplier;
+ @VisibleForTesting
+ static Supplier<Cursor> sFakeAppUsageLatestTimestampSupplier;
private DatabaseUtils() {
}
@@ -82,6 +104,27 @@
return userManager.isManagedProfile() && !userManager.isSystemUser();
}
+ /** Returns the latest timestamp current user data in app usage event table. */
+ public static long getAppUsageStartTimestampOfUser(
+ Context context, final long userId, final long earliestTimestamp) {
+ final long startTime = System.currentTimeMillis();
+ // Builds the content uri everytime to avoid cache.
+ final Uri appUsageLatestTimestampUri =
+ new Uri.Builder()
+ .scheme(ContentResolver.SCHEME_CONTENT)
+ .authority(AUTHORITY)
+ .appendPath(APP_USAGE_LATEST_TIMESTAMP_PATH)
+ .appendQueryParameter(
+ QUERY_KEY_USERID, Long.toString(userId))
+ .build();
+ final long latestTimestamp =
+ loadAppUsageLatestTimestampFromContentProvider(context, appUsageLatestTimestampUri);
+ Log.d(TAG, String.format(
+ "getAppUsageStartTimestampOfUser() userId=%d latestTimestamp=%d in %d/ms",
+ userId, latestTimestamp, (System.currentTimeMillis() - startTime)));
+ return Math.max(latestTimestamp, earliestTimestamp);
+ }
+
/** Long: for timestamp and String: for BatteryHistEntry.getKey() */
public static Map<Long, Map<String, BatteryHistEntry>> getHistoryMapSinceLastFullCharge(
Context context, Calendar calendar) {
@@ -113,10 +156,10 @@
public static void clearAll(Context context) {
AsyncTask.execute(() -> {
try {
- BatteryStateDatabase
- .getInstance(context.getApplicationContext())
- .batteryStateDao()
- .clearAll();
+ final BatteryStateDatabase database = BatteryStateDatabase
+ .getInstance(context.getApplicationContext());
+ database.batteryStateDao().clearAll();
+ database.appUsageEventDao().clearAll();
} catch (RuntimeException e) {
Log.e(TAG, "clearAll() failed", e);
}
@@ -127,17 +170,59 @@
public static void clearExpiredDataIfNeeded(Context context) {
AsyncTask.execute(() -> {
try {
- BatteryStateDatabase
- .getInstance(context.getApplicationContext())
- .batteryStateDao()
- .clearAllBefore(Clock.systemUTC().millis()
- - Duration.ofDays(DATA_RETENTION_INTERVAL_DAY).toMillis());
+ final BatteryStateDatabase database = BatteryStateDatabase
+ .getInstance(context.getApplicationContext());
+ final long earliestTimestamp = Clock.systemUTC().millis()
+ - Duration.ofDays(DATA_RETENTION_INTERVAL_DAY).toMillis();
+ database.batteryStateDao().clearAllBefore(earliestTimestamp);
+ database.appUsageEventDao().clearAllBefore(earliestTimestamp);
} catch (RuntimeException e) {
Log.e(TAG, "clearAllBefore() failed", e);
}
});
}
+ /** Returns the timestamp for 00:00 6 days before the calendar date. */
+ public static long getTimestampSixDaysAgo(Calendar calendar) {
+ Calendar startCalendar =
+ calendar == null ? Calendar.getInstance() : (Calendar) calendar.clone();
+ startCalendar.add(Calendar.DAY_OF_YEAR, -6);
+ startCalendar.set(Calendar.HOUR_OF_DAY, 0);
+ startCalendar.set(Calendar.MINUTE, 0);
+ startCalendar.set(Calendar.SECOND, 0);
+ startCalendar.set(Calendar.MILLISECOND, 0);
+ return startCalendar.getTimeInMillis();
+ }
+
+ static List<ContentValues> sendAppUsageEventData(
+ final Context context, final List<AppUsageEvent> appUsageEventList) {
+ final long startTime = System.currentTimeMillis();
+ // Creates the ContentValues list to insert them into provider.
+ final List<ContentValues> valuesList = new ArrayList<>();
+ appUsageEventList.stream()
+ .filter(appUsageEvent -> appUsageEvent.hasUid())
+ .forEach(appUsageEvent -> valuesList.add(
+ ConvertUtils.convertAppUsageEventToContentValues(appUsageEvent)));
+ int size = 0;
+ final ContentResolver resolver = context.getContentResolver();
+ // Inserts all ContentValues into battery provider.
+ if (!valuesList.isEmpty()) {
+ final ContentValues[] valuesArray = new ContentValues[valuesList.size()];
+ valuesList.toArray(valuesArray);
+ try {
+ size = resolver.bulkInsert(APP_USAGE_EVENT_URI, valuesArray);
+ resolver.notifyChange(APP_USAGE_EVENT_URI, /*observer=*/ null);
+ Log.d(TAG, "insert() app usage events data into database");
+ } catch (Exception e) {
+ Log.e(TAG, "bulkInsert() app usage data into database error:\n" + e);
+ }
+ }
+ Log.d(TAG, String.format("sendAppUsageEventData() size=%d in %d/ms",
+ size, (System.currentTimeMillis() - startTime)));
+ clearMemory();
+ return valuesList;
+ }
+
static List<ContentValues> sendBatteryEntryData(
final Context context,
final List<BatteryEntry> batteryEntryList,
@@ -178,7 +263,7 @@
|| backgroundMs != 0;
})
.forEach(entry -> valuesList.add(
- ConvertUtils.convertToContentValues(
+ ConvertUtils.convertBatteryEntryToContentValues(
entry,
batteryUsageStats,
batteryLevel,
@@ -197,15 +282,15 @@
valuesList.toArray(valuesArray);
try {
size = resolver.bulkInsert(BATTERY_CONTENT_URI, valuesArray);
- Log.d(TAG, "insert() data into database with isFullChargeStart:"
+ Log.d(TAG, "insert() battery states data into database with isFullChargeStart:"
+ isFullChargeStart);
} catch (Exception e) {
- Log.e(TAG, "bulkInsert() data into database error:\n" + e);
+ Log.e(TAG, "bulkInsert() battery states data into database error:\n" + e);
}
} else {
// Inserts one fake data into battery provider.
final ContentValues contentValues =
- ConvertUtils.convertToContentValues(
+ ConvertUtils.convertBatteryEntryToContentValues(
/*entry=*/ null,
/*batteryUsageStats=*/ null,
batteryLevel,
@@ -231,6 +316,30 @@
return valuesList;
}
+ private static long loadAppUsageLatestTimestampFromContentProvider(
+ Context context, final Uri appUsageLatestTimestampUri) {
+ // We have already make sure the context here is with OWNER user identity. Don't need to
+ // check whether current user is work profile.
+ try (Cursor cursor = sFakeAppUsageLatestTimestampSupplier != null
+ ? sFakeAppUsageLatestTimestampSupplier.get()
+ : context.getContentResolver().query(
+ appUsageLatestTimestampUri, null, null, null)) {
+ if (cursor == null || cursor.getCount() == 0) {
+ return INVALID_USER_ID;
+ }
+ cursor.moveToFirst();
+ // There is only one column returned so use the index 0 directly.
+ final long latestTimestamp = cursor.getLong(/*columnIndex=*/ 0);
+ try {
+ cursor.close();
+ } catch (Exception e) {
+ Log.e(TAG, "cursor.close() failed", e);
+ }
+ // If there is no data for this user, 0 will be returned from the database.
+ return latestTimestamp == 0 ? INVALID_USER_ID : latestTimestamp;
+ }
+ }
+
private static Map<Long, Map<String, BatteryHistEntry>> loadHistoryMapFromContentProvider(
Context context, Uri batteryStateUri) {
final boolean isWorkProfileUser = isWorkProfile(context);
@@ -247,7 +356,7 @@
}
}
final Map<Long, Map<String, BatteryHistEntry>> resultMap = new HashMap();
- try (Cursor cursor =
+ try (Cursor cursor = sFakeBatteryStateSupplier != null ? sFakeBatteryStateSupplier.get() :
context.getContentResolver().query(batteryStateUri, null, null, null)) {
if (cursor == null || cursor.getCount() == 0) {
return resultMap;
@@ -286,17 +395,4 @@
Log.w(TAG, "invoke clearMemory()");
}, CLEAR_MEMORY_DELAYED_MS);
}
-
- /** Returns the timestamp for 00:00 6 days before the calendar date. */
- private static long getTimestampSixDaysAgo(Calendar calendar) {
- Calendar startCalendar =
- calendar == null ? Calendar.getInstance() : (Calendar) calendar.clone();
- startCalendar.add(Calendar.DAY_OF_YEAR, -6);
- startCalendar.set(Calendar.HOUR_OF_DAY, 0);
- startCalendar.set(Calendar.MINUTE, 0);
- startCalendar.set(Calendar.SECOND, 0);
- startCalendar.set(Calendar.MILLISECOND, 0);
- return startCalendar.getTimeInMillis();
- }
-
}
diff --git a/src/com/android/settings/fuelgauge/batteryusage/PeriodicJobReceiver.java b/src/com/android/settings/fuelgauge/batteryusage/PeriodicJobReceiver.java
index fddf01b..d6a2f62 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/PeriodicJobReceiver.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/PeriodicJobReceiver.java
@@ -40,6 +40,7 @@
return;
}
BatteryUsageDataLoader.enqueueWork(context, /*isFullChargeStart=*/ false);
+ AppUsageDataLoader.enqueueWork(context);
Log.d(TAG, "refresh periodic job from action=" + action);
PeriodicJobManager.getInstance(context).refreshJob();
DatabaseUtils.clearExpiredDataIfNeeded(context);
diff --git a/src/com/android/settings/fuelgauge/batteryusage/db/AppUsageEventDao.java b/src/com/android/settings/fuelgauge/batteryusage/db/AppUsageEventDao.java
new file mode 100644
index 0000000..578a1ff
--- /dev/null
+++ b/src/com/android/settings/fuelgauge/batteryusage/db/AppUsageEventDao.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.fuelgauge.batteryusage.db;
+
+import android.database.Cursor;
+
+import androidx.room.Dao;
+import androidx.room.Insert;
+import androidx.room.OnConflictStrategy;
+import androidx.room.Query;
+
+import java.util.List;
+
+/** Data access object for accessing {@link AppUsageEventEntity} in the database. */
+@Dao
+public interface AppUsageEventDao {
+
+ /** Inserts a {@link AppUsageEventEntity} data into the database. */
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ void insert(AppUsageEventEntity event);
+
+ /** Inserts {@link AppUsageEventEntity} data into the database. */
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ void insertAll(List<AppUsageEventEntity> events);
+
+ /** Lists all recorded data after a specific timestamp. */
+ @Query("SELECT * FROM AppUsageEventEntity WHERE timestamp > :timestamp ORDER BY timestamp DESC")
+ List<AppUsageEventEntity> getAllAfter(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);
+
+ /** Deletes all recorded data before a specific timestamp. */
+ @Query("DELETE FROM AppUsageEventEntity WHERE timestamp <= :timestamp")
+ void clearAllBefore(long timestamp);
+
+ /** Clears all recorded data in the database. */
+ @Query("DELETE FROM AppUsageEventEntity")
+ void clearAll();
+}
diff --git a/src/com/android/settings/fuelgauge/batteryusage/db/AppUsageEventEntity.java b/src/com/android/settings/fuelgauge/batteryusage/db/AppUsageEventEntity.java
new file mode 100644
index 0000000..9d62d07
--- /dev/null
+++ b/src/com/android/settings/fuelgauge/batteryusage/db/AppUsageEventEntity.java
@@ -0,0 +1,213 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.fuelgauge.batteryusage.db;
+
+import android.content.ContentValues;
+
+import androidx.room.Entity;
+import androidx.room.PrimaryKey;
+
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+import java.util.TimeZone;
+
+/** A {@link Entity} class to save app usage events into database. */
+@Entity
+public class AppUsageEventEntity {
+ private static String sCacheZoneId;
+ private static SimpleDateFormat sCacheSimpleDateFormat;
+
+ /** Keys for accessing {@link ContentValues}. */
+ public static final String KEY_UID = "uid";
+ public static final String KEY_USER_ID = "userId";
+ public static final String KEY_TIMESTAMP = "timestamp";
+ public static final String KEY_APP_USAGE_EVENT_TYPE = "appUsageEventType";
+ public static final String KEY_PACKAGE_NAME = "packageName";
+ public static final String KEY_INSTANCE_ID = "instanceId";
+ public static final String KEY_TASK_ROOT_PACKAGE_NAME = "taskRootPackageName";
+
+ @PrimaryKey(autoGenerate = true)
+ private long mId;
+
+ // Records the app relative information.
+ public final long uid;
+ public final long userId;
+ public final long timestamp;
+ public final int appUsageEventType;
+ public final String packageName;
+ public final int instanceId;
+ public final String taskRootPackageName;
+
+ public AppUsageEventEntity(
+ final long uid,
+ final long userId,
+ final long timestamp,
+ final int appUsageEventType,
+ final String packageName,
+ final int instanceId,
+ final String taskRootPackageName) {
+ this.uid = uid;
+ this.userId = userId;
+ this.timestamp = timestamp;
+ this.appUsageEventType = appUsageEventType;
+ this.packageName = packageName;
+ this.instanceId = instanceId;
+ this.taskRootPackageName = taskRootPackageName;
+ }
+
+ /** Sets the auto-generated content ID. */
+ public void setId(long id) {
+ this.mId = id;
+ }
+
+ /** Gets the auto-generated content ID. */
+ public long getId() {
+ return mId;
+ }
+
+ @Override
+ @SuppressWarnings("JavaUtilDate")
+ public String toString() {
+ final String currentZoneId = TimeZone.getDefault().getID();
+ if (!currentZoneId.equals(sCacheZoneId) || sCacheSimpleDateFormat == null) {
+ sCacheZoneId = currentZoneId;
+ sCacheSimpleDateFormat = new SimpleDateFormat("MMM dd,yyyy HH:mm:ss", Locale.US);
+ }
+ final String recordAtDateTime = sCacheSimpleDateFormat.format(new Date(timestamp));
+ final StringBuilder builder = new StringBuilder()
+ .append("\nAppUsageEvent{")
+ .append(String.format(Locale.US,
+ "\n\tpackage=%s|uid=%d|userId=%d", packageName, uid, userId))
+ .append(String.format(Locale.US, "\n\ttimestamp=%s|eventType=%d|instanceId=%d",
+ recordAtDateTime, appUsageEventType, instanceId))
+ .append(String.format(Locale.US, "\n\ttaskRootPackageName=%s",
+ taskRootPackageName));
+ return builder.toString();
+ }
+
+ /** Creates new {@link AppUsageEventEntity} from {@link ContentValues}. */
+ public static AppUsageEventEntity create(ContentValues contentValues) {
+ Builder builder = AppUsageEventEntity.newBuilder();
+ if (contentValues.containsKey(KEY_UID)) {
+ builder.setUid(contentValues.getAsLong(KEY_UID));
+ }
+ if (contentValues.containsKey(KEY_USER_ID)) {
+ builder.setUserId(contentValues.getAsLong(KEY_USER_ID));
+ }
+ if (contentValues.containsKey(KEY_TIMESTAMP)) {
+ builder.setTimestamp(contentValues.getAsLong(KEY_TIMESTAMP));
+ }
+ if (contentValues.containsKey(KEY_APP_USAGE_EVENT_TYPE)) {
+ builder.setAppUsageEventType(contentValues.getAsInteger(KEY_APP_USAGE_EVENT_TYPE));
+ }
+ if (contentValues.containsKey(KEY_PACKAGE_NAME)) {
+ builder.setPackageName(contentValues.getAsString(KEY_PACKAGE_NAME));
+ }
+ if (contentValues.containsKey(KEY_INSTANCE_ID)) {
+ builder.setInstanceId(
+ contentValues.getAsInteger(KEY_INSTANCE_ID));
+ }
+ if (contentValues.containsKey(KEY_TASK_ROOT_PACKAGE_NAME)) {
+ builder.setTaskRootPackageName(contentValues.getAsString(KEY_TASK_ROOT_PACKAGE_NAME));
+ }
+ return builder.build();
+ }
+
+ /** Creates a new {@link Builder} instance. */
+ public static Builder newBuilder() {
+ return new Builder();
+ }
+
+ /** A convenience builder class to improve readability. */
+ public static class Builder {
+ private long mUid;
+ private long mUserId;
+ private long mTimestamp;
+ private int mAppUsageEventType;
+ private String mPackageName;
+ private int mInstanceId;
+ private String mTaskRootPackageName;
+
+ /** Sets the uid. */
+ @CanIgnoreReturnValue
+ public Builder setUid(final long uid) {
+ this.mUid = uid;
+ return this;
+ }
+
+ /** Sets the user ID. */
+ @CanIgnoreReturnValue
+ public Builder setUserId(final long userId) {
+ this.mUserId = userId;
+ return this;
+ }
+
+ /** Sets the timestamp. */
+ @CanIgnoreReturnValue
+ public Builder setTimestamp(final long timestamp) {
+ this.mTimestamp = timestamp;
+ return this;
+ }
+
+ /** Sets the app usage event type. */
+ @CanIgnoreReturnValue
+ public Builder setAppUsageEventType(final int appUsageEventType) {
+ this.mAppUsageEventType = appUsageEventType;
+ return this;
+ }
+
+ /** Sets the package name. */
+ @CanIgnoreReturnValue
+ public Builder setPackageName(final String packageName) {
+ this.mPackageName = packageName;
+ return this;
+ }
+
+ /** Sets the instance ID. */
+ @CanIgnoreReturnValue
+ public Builder setInstanceId(final int instanceId) {
+ this.mInstanceId = instanceId;
+ return this;
+ }
+
+ /** Sets the task root package name. */
+ @CanIgnoreReturnValue
+ public Builder setTaskRootPackageName(final String taskRootPackageName) {
+ this.mTaskRootPackageName = taskRootPackageName;
+ return this;
+ }
+
+ /** Builds the AppUsageEvent. */
+ public AppUsageEventEntity build() {
+ return new AppUsageEventEntity(
+ mUid,
+ mUserId,
+ mTimestamp,
+ mAppUsageEventType,
+ mPackageName,
+ mInstanceId,
+ mTaskRootPackageName);
+ }
+
+ private Builder() {}
+ }
+
+
+}
diff --git a/src/com/android/settings/fuelgauge/batteryusage/db/BatteryState.java b/src/com/android/settings/fuelgauge/batteryusage/db/BatteryState.java
index a50578b..9139c10 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/db/BatteryState.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/db/BatteryState.java
@@ -204,14 +204,14 @@
return this;
}
- /** Sets the consumer type. */
+ /** Sets the battery information. */
@CanIgnoreReturnValue
public Builder setBatteryInformation(String batteryInformation) {
this.mBatteryInformation = batteryInformation;
return this;
}
- /** Sets the consumer type. */
+ /** Sets the battery information debug string. */
@CanIgnoreReturnValue
public Builder setBatteryInformationDebug(String batteryInformationDebug) {
this.mBatteryInformationDebug = batteryInformationDebug;
diff --git a/src/com/android/settings/fuelgauge/batteryusage/db/BatteryStateDatabase.java b/src/com/android/settings/fuelgauge/batteryusage/db/BatteryStateDatabase.java
index 64c629c..4c8df7e 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/db/BatteryStateDatabase.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/db/BatteryStateDatabase.java
@@ -25,7 +25,7 @@
/** A {@link RoomDatabase} for battery usage states history. */
@Database(
- entities = {BatteryState.class},
+ entities = {BatteryState.class, AppUsageEventEntity.class},
version = 1)
public abstract class BatteryStateDatabase extends RoomDatabase {
private static final String TAG = "BatteryStateDatabase";
@@ -34,13 +34,15 @@
/** Provides DAO for battery state table. */
public abstract BatteryStateDao batteryStateDao();
+ /** Provides DAO for app usage event table. */
+ public abstract AppUsageEventDao appUsageEventDao();
/** Gets or creates an instance of {@link RoomDatabase}. */
public static BatteryStateDatabase getInstance(Context context) {
if (sBatteryStateDatabase == null) {
sBatteryStateDatabase =
Room.databaseBuilder(
- context, BatteryStateDatabase.class, "battery-usage-db-v6")
+ context, BatteryStateDatabase.class, "battery-usage-db-v7")
// Allows accessing data in the main thread for dumping bugreport.
.allowMainThreadQueries()
.fallbackToDestructiveMigration()
diff --git a/src/com/android/settings/fuelgauge/protos/Android.bp b/src/com/android/settings/fuelgauge/protos/Android.bp
index ab5e36b..2c63af4 100644
--- a/src/com/android/settings/fuelgauge/protos/Android.bp
+++ b/src/com/android/settings/fuelgauge/protos/Android.bp
@@ -13,4 +13,12 @@
type: "lite",
},
srcs: ["fuelgauge_usage_state.proto"],
+}
+
+java_library {
+ name: "app-usage-event-protos-lite",
+ proto: {
+ type: "lite",
+ },
+ srcs: ["app_usage_event.proto"],
}
\ No newline at end of file
diff --git a/src/com/android/settings/fuelgauge/protos/app_usage_event.proto b/src/com/android/settings/fuelgauge/protos/app_usage_event.proto
new file mode 100644
index 0000000..921fb0a
--- /dev/null
+++ b/src/com/android/settings/fuelgauge/protos/app_usage_event.proto
@@ -0,0 +1,34 @@
+syntax = "proto2";
+
+option java_multiple_files = true;
+option java_package = "com.android.settings.fuelgauge.batteryusage";
+option java_outer_classname = "AppUsageEventProto";
+
+enum AppUsageEventType {
+ UNKNOWN = 0;
+ ACTIVITY_RESUMED = 1;
+ ACTIVITY_STOPPED = 2;
+ DEVICE_SHUTDOWN = 3;
+}
+
+message AppUsageEvent {
+ // Timestamp of the usage event.
+ optional int64 timestamp = 1;
+ // Type of the usage event.
+ optional AppUsageEventType type = 2;
+ // Package name of the app.
+ optional string package_name = 3;
+ // Instance ID for the activity. This is important for matching events of
+ // different event types for the same instance because an activity can be
+ // instantiated multiple times. Only available on Q builds after Dec 13 2018.
+ optional int32 instance_id = 4;
+ // Package name of the task root. For example, if a Twitter activity starts a
+ // Chrome activity within the same task, then while package_name is Chrome,
+ // task_root_package_name will be Twitter.
+ // Note: Activities that are task roots themselves (most activities) will have
+ // this field is populated as package_name.
+ // Note: The task root might be missing due to b/123404490.
+ optional string task_root_package_name = 5;
+ optional int64 user_id = 6;
+ optional int64 uid = 7;
+}
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/AppUsageDataLoaderTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/AppUsageDataLoaderTest.java
new file mode 100644
index 0000000..4b250a3
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/AppUsageDataLoaderTest.java
@@ -0,0 +1,102 @@
+/*
+ * 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 org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+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.HashMap;
+import java.util.List;
+
+@RunWith(RobolectricTestRunner.class)
+public final class AppUsageDataLoaderTest {
+ private Context mContext;
+ @Mock
+ private ContentResolver mMockContentResolver;
+ @Mock
+ private UserManager mUserManager;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mContext = spy(RuntimeEnvironment.application);
+ doReturn(mContext).when(mContext).getApplicationContext();
+ doReturn(mMockContentResolver).when(mContext).getContentResolver();
+ doReturn(mUserManager).when(mContext).getSystemService(UserManager.class);
+ doReturn(new Intent()).when(mContext).registerReceiver(any(), any());
+ }
+
+ @Test
+ public void loadAppUsageData_withData_insertFakeDataIntoProvider() {
+ final List<AppUsageEvent> AppUsageEventList = new ArrayList<>();
+ final AppUsageEvent appUsageEvent = AppUsageEvent.newBuilder().setUid(0).build();
+ AppUsageEventList.add(appUsageEvent);
+ AppUsageDataLoader.sFakeAppUsageEventsSupplier = () -> new HashMap<>();
+ AppUsageDataLoader.sFakeUsageEventsListSupplier = () -> AppUsageEventList;
+
+ AppUsageDataLoader.loadAppUsageData(mContext);
+
+ verify(mMockContentResolver).bulkInsert(any(), any());
+ verify(mMockContentResolver).notifyChange(any(), any());
+ }
+
+ @Test
+ public void loadAppUsageData_nullAppUsageEvents_notInsertDataIntoProvider() {
+ AppUsageDataLoader.sFakeAppUsageEventsSupplier = () -> null;
+
+ AppUsageDataLoader.loadAppUsageData(mContext);
+
+ verifyNoMoreInteractions(mMockContentResolver);
+ }
+
+ @Test
+ public void loadAppUsageData_nullUsageEventsList_notInsertDataIntoProvider() {
+ AppUsageDataLoader.sFakeAppUsageEventsSupplier = () -> new HashMap<>();
+ AppUsageDataLoader.sFakeUsageEventsListSupplier = () -> null;
+
+ AppUsageDataLoader.loadAppUsageData(mContext);
+
+ verifyNoMoreInteractions(mMockContentResolver);
+ }
+
+ @Test
+ public void loadAppUsageData_emptyUsageEventsList_notInsertDataIntoProvider() {
+ AppUsageDataLoader.sFakeAppUsageEventsSupplier = () -> new HashMap<>();
+ AppUsageDataLoader.sFakeUsageEventsListSupplier = () -> new ArrayList<>();
+
+ AppUsageDataLoader.loadAppUsageData(mContext);
+
+ verifyNoMoreInteractions(mMockContentResolver);
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryHistEntryTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryHistEntryTest.java
index 5c143b1..9667760 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryHistEntryTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryHistEntryTest.java
@@ -68,7 +68,7 @@
when(mMockBatteryEntry.getConsumerType())
.thenReturn(ConvertUtils.CONSUMER_TYPE_SYSTEM_BATTERY);
final ContentValues values =
- ConvertUtils.convertToContentValues(
+ ConvertUtils.convertBatteryEntryToContentValues(
mMockBatteryEntry,
mBatteryUsageStats,
/*batteryLevel=*/ 12,
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageContentProviderTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageContentProviderTest.java
index d578b89..b43727d 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageContentProviderTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageContentProviderTest.java
@@ -30,6 +30,7 @@
import androidx.test.core.app.ApplicationProvider;
+import com.android.settings.fuelgauge.batteryusage.db.AppUsageEventEntity;
import com.android.settings.fuelgauge.batteryusage.db.BatteryState;
import com.android.settings.fuelgauge.batteryusage.db.BatteryStateDatabase;
import com.android.settings.testutils.BatteryTestUtils;
@@ -52,6 +53,8 @@
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";
+ private static final long USER_ID1 = 1;
+ private static final long USER_ID2 = 2;
private Context mContext;
private BatteryUsageContentProvider mProvider;
@@ -178,6 +181,37 @@
}
@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);
+
+ final Cursor cursor1 = getCursorOfLatestTimestamp(USER_ID1);
+ assertThat(cursor1.getCount()).isEqualTo(1);
+ cursor1.moveToFirst();
+ 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);
+
+ final long notExistingUserId = 3;
+ final Cursor cursor3 = getCursorOfLatestTimestamp(notExistingUserId);
+ assertThat(cursor3.getCount()).isEqualTo(1);
+ cursor3.moveToFirst();
+ assertThat(cursor3.getLong(0)).isEqualTo(0);
+ }
+
+ @Test
public void insert_batteryState_returnsExpectedResult() {
mProvider.onCreate();
final DeviceBatteryState deviceBatteryState =
@@ -267,6 +301,34 @@
}
@Test
+ public void insert_appUsageEvent_returnsExpectedResult() {
+ mProvider.onCreate();
+ ContentValues values = new ContentValues();
+ values.put(AppUsageEventEntity.KEY_UID, 101L);
+ values.put(AppUsageEventEntity.KEY_USER_ID, 1001L);
+ values.put(AppUsageEventEntity.KEY_TIMESTAMP, 10001L);
+ values.put(AppUsageEventEntity.KEY_APP_USAGE_EVENT_TYPE, 1);
+ values.put(AppUsageEventEntity.KEY_PACKAGE_NAME, "com.android.settings1");
+ values.put(AppUsageEventEntity.KEY_INSTANCE_ID, 100001L);
+ values.put(AppUsageEventEntity.KEY_TASK_ROOT_PACKAGE_NAME, "com.android.settings2");
+
+ final Uri uri = mProvider.insert(DatabaseUtils.APP_USAGE_EVENT_URI, values);
+
+ assertThat(uri).isEqualTo(DatabaseUtils.APP_USAGE_EVENT_URI);
+ // Verifies the AppUsageEventEntity content.
+ final List<AppUsageEventEntity> entities =
+ BatteryStateDatabase.getInstance(mContext).appUsageEventDao().getAllAfter(0);
+ assertThat(entities).hasSize(1);
+ assertThat(entities.get(0).uid).isEqualTo(101L);
+ assertThat(entities.get(0).userId).isEqualTo(1001L);
+ assertThat(entities.get(0).timestamp).isEqualTo(10001L);
+ assertThat(entities.get(0).appUsageEventType).isEqualTo(1);
+ assertThat(entities.get(0).packageName).isEqualTo("com.android.settings1");
+ assertThat(entities.get(0).instanceId).isEqualTo(100001L);
+ assertThat(entities.get(0).taskRootPackageName).isEqualTo("com.android.settings2");
+ }
+
+ @Test
public void delete_throwsUnsupportedOperationException() {
assertThrows(
UnsupportedOperationException.class,
@@ -293,12 +355,12 @@
mProvider.setClock(fakeClock);
final long currentTimestamp = currentTime.toMillis();
// Inserts some valid testing data.
- BatteryTestUtils.insertDataToBatteryStateDatabase(
+ BatteryTestUtils.insertDataToBatteryStateTable(
mContext, currentTimestamp - 2, PACKAGE_NAME1,
/*isFullChargeStart=*/ true);
- BatteryTestUtils.insertDataToBatteryStateDatabase(
+ BatteryTestUtils.insertDataToBatteryStateTable(
mContext, currentTimestamp - 1, PACKAGE_NAME2);
- BatteryTestUtils.insertDataToBatteryStateDatabase(
+ BatteryTestUtils.insertDataToBatteryStateTable(
mContext, currentTimestamp, PACKAGE_NAME3);
final Uri batteryStateQueryContentUri =
@@ -307,7 +369,7 @@
.authority(DatabaseUtils.AUTHORITY)
.appendPath(DatabaseUtils.BATTERY_STATE_TABLE)
.appendQueryParameter(
- BatteryUsageContentProvider.QUERY_KEY_TIMESTAMP, queryTimestamp)
+ DatabaseUtils.QUERY_KEY_TIMESTAMP, queryTimestamp)
.build();
final Cursor cursor =
@@ -320,4 +382,22 @@
return cursor;
}
+
+ private Cursor getCursorOfLatestTimestamp(final long userId) {
+ final Uri appUsageLatestTimestampQueryContentUri =
+ new Uri.Builder()
+ .scheme(ContentResolver.SCHEME_CONTENT)
+ .authority(DatabaseUtils.AUTHORITY)
+ .appendPath(DatabaseUtils.APP_USAGE_LATEST_TIMESTAMP_PATH)
+ .appendQueryParameter(
+ DatabaseUtils.QUERY_KEY_USERID, Long.toString(userId))
+ .build();
+
+ return mProvider.query(
+ appUsageLatestTimestampQueryContentUri,
+ /*strings=*/ null,
+ /*s=*/ null,
+ /*strings1=*/ null,
+ /*s1=*/ null);
+ }
}
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BootBroadcastReceiverTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BootBroadcastReceiverTest.java
index b67066d..514ac63 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BootBroadcastReceiverTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BootBroadcastReceiverTest.java
@@ -62,7 +62,7 @@
// Inserts fake data into database for testing.
final BatteryStateDatabase database = BatteryTestUtils.setUpBatteryStateDatabase(mContext);
- BatteryTestUtils.insertDataToBatteryStateDatabase(
+ BatteryTestUtils.insertDataToBatteryStateTable(
mContext, Clock.systemUTC().millis(), "com.android.systemui");
mDao = database.batteryStateDao();
}
@@ -170,9 +170,9 @@
private void insertExpiredData(int shiftDay) {
final long expiredTimeInMs =
Clock.systemUTC().millis() - Duration.ofDays(shiftDay).toMillis();
- BatteryTestUtils.insertDataToBatteryStateDatabase(
+ BatteryTestUtils.insertDataToBatteryStateTable(
mContext, expiredTimeInMs - 1, "com.android.systemui");
- BatteryTestUtils.insertDataToBatteryStateDatabase(
+ BatteryTestUtils.insertDataToBatteryStateTable(
mContext, expiredTimeInMs, "com.android.systemui");
// Ensures the testing environment is correct.
assertThat(mDao.getAllAfter(0)).hasSize(3);
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/ConvertUtilsTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/ConvertUtilsTest.java
index 978930a..42cd7ef 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/ConvertUtilsTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/ConvertUtilsTest.java
@@ -17,16 +17,23 @@
import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
+import android.app.usage.UsageEvents;
+import android.app.usage.UsageEvents.Event;
import android.content.ContentValues;
import android.content.Context;
+import android.content.pm.PackageManager;
import android.os.BatteryManager;
import android.os.BatteryUsageStats;
import android.os.LocaleList;
import android.os.UserHandle;
+import com.android.settings.fuelgauge.batteryusage.db.AppUsageEventEntity;
+
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -44,6 +51,8 @@
private Context mContext;
@Mock
+ private PackageManager mMockPackageManager;
+ @Mock
private BatteryUsageStats mBatteryUsageStats;
@Mock
private BatteryEntry mMockBatteryEntry;
@@ -52,10 +61,11 @@
public void setUp() {
MockitoAnnotations.initMocks(this);
mContext = spy(RuntimeEnvironment.application);
+ when(mContext.getPackageManager()).thenReturn(mMockPackageManager);
}
@Test
- public void convertToContentValues_returnsExpectedContentValues() {
+ public void convertBatteryEntryToContentValues_returnsExpectedContentValues() {
final int expectedType = 3;
when(mMockBatteryEntry.getUid()).thenReturn(1001);
when(mMockBatteryEntry.getLabel()).thenReturn("Settings");
@@ -76,7 +86,7 @@
.thenReturn(ConvertUtils.CONSUMER_TYPE_SYSTEM_BATTERY);
final ContentValues values =
- ConvertUtils.convertToContentValues(
+ ConvertUtils.convertBatteryEntryToContentValues(
mMockBatteryEntry,
mBatteryUsageStats,
/*batteryLevel=*/ 12,
@@ -121,9 +131,9 @@
}
@Test
- public void convertToContentValues_nullBatteryEntry_returnsExpectedContentValues() {
+ public void convertBatteryEntryToContentValues_nullBatteryEntry_returnsExpectedContentValues() {
final ContentValues values =
- ConvertUtils.convertToContentValues(
+ ConvertUtils.convertBatteryEntryToContentValues(
/*entry=*/ null,
/*batteryUsageStats=*/ null,
/*batteryLevel=*/ 12,
@@ -152,6 +162,31 @@
}
@Test
+ public void convertAppUsageEventToContentValues_returnsExpectedContentValues() {
+ final AppUsageEvent appUsageEvent =
+ AppUsageEvent.newBuilder()
+ .setUid(101L)
+ .setUserId(1001L)
+ .setTimestamp(10001L)
+ .setType(AppUsageEventType.ACTIVITY_RESUMED)
+ .setPackageName("com.android.settings1")
+ .setInstanceId(100001)
+ .setTaskRootPackageName("com.android.settings2")
+ .build();
+ final ContentValues values =
+ ConvertUtils.convertAppUsageEventToContentValues(appUsageEvent);
+ assertThat(values.getAsLong(AppUsageEventEntity.KEY_UID)).isEqualTo(101L);
+ assertThat(values.getAsLong(AppUsageEventEntity.KEY_USER_ID)).isEqualTo(1001L);
+ assertThat(values.getAsLong(AppUsageEventEntity.KEY_TIMESTAMP)).isEqualTo(10001L);
+ assertThat(values.getAsInteger(AppUsageEventEntity.KEY_APP_USAGE_EVENT_TYPE)).isEqualTo(1);
+ assertThat(values.getAsString(AppUsageEventEntity.KEY_PACKAGE_NAME))
+ .isEqualTo("com.android.settings1");
+ assertThat(values.getAsInteger(AppUsageEventEntity.KEY_INSTANCE_ID)).isEqualTo(100001);
+ assertThat(values.getAsString(AppUsageEventEntity.KEY_TASK_ROOT_PACKAGE_NAME))
+ .isEqualTo("com.android.settings2");
+ }
+
+ @Test
public void convertToBatteryHistEntry_returnsExpectedResult() {
final int expectedType = 3;
when(mMockBatteryEntry.getUid()).thenReturn(1001);
@@ -230,6 +265,77 @@
}
@Test
+ public void convertToAppUsageEvent_returnsExpectedResult()
+ throws PackageManager.NameNotFoundException {
+ final Event event = new Event();
+ event.mEventType = UsageEvents.Event.ACTIVITY_RESUMED;
+ event.mPackage = "com.android.settings1";
+ event.mTimeStamp = 101L;
+ event.mInstanceId = 100001;
+ event.mTaskRootPackage = "com.android.settings2";
+ when(mMockPackageManager.getPackageUidAsUser(any(), anyInt())).thenReturn(1001);
+
+ final long userId = 2;
+ final AppUsageEvent appUsageEvent = ConvertUtils.convertToAppUsageEvent(
+ mContext, event, userId);
+ assertThat(appUsageEvent.getTimestamp()).isEqualTo(101L);
+ assertThat(appUsageEvent.getType()).isEqualTo(AppUsageEventType.ACTIVITY_RESUMED);
+ assertThat(appUsageEvent.getPackageName()).isEqualTo("com.android.settings1");
+ assertThat(appUsageEvent.getInstanceId()).isEqualTo(100001);
+ assertThat(appUsageEvent.getTaskRootPackageName()).isEqualTo("com.android.settings2");
+ assertThat(appUsageEvent.getUid()).isEqualTo(1001L);
+ assertThat(appUsageEvent.getUserId()).isEqualTo(userId);
+ }
+
+ @Test
+ public void convertToAppUsageEvent_emptyInstanceIdAndRootName_returnsExpectedResult()
+ throws PackageManager.NameNotFoundException {
+ final Event event = new Event();
+ event.mEventType = UsageEvents.Event.DEVICE_SHUTDOWN;
+ event.mPackage = "com.android.settings1";
+ event.mTimeStamp = 101L;
+ when(mMockPackageManager.getPackageUidAsUser(any(), anyInt())).thenReturn(1001);
+
+ final long userId = 1;
+ final AppUsageEvent appUsageEvent = ConvertUtils.convertToAppUsageEvent(
+ mContext, event, userId);
+ assertThat(appUsageEvent.getTimestamp()).isEqualTo(101L);
+ assertThat(appUsageEvent.getType()).isEqualTo(AppUsageEventType.DEVICE_SHUTDOWN);
+ assertThat(appUsageEvent.getPackageName()).isEqualTo("com.android.settings1");
+ assertThat(appUsageEvent.getInstanceId()).isEqualTo(0);
+ assertThat(appUsageEvent.getTaskRootPackageName()).isEqualTo("");
+ assertThat(appUsageEvent.getUid()).isEqualTo(1001L);
+ assertThat(appUsageEvent.getUserId()).isEqualTo(userId);
+ }
+
+ @Test
+ public void convertToAppUsageEvent_emptyPackageName_returnsNull() {
+ final Event event = new Event();
+ event.mPackage = null;
+
+ final AppUsageEvent appUsageEvent = ConvertUtils.convertToAppUsageEvent(
+ mContext, event, /*userId=*/ 0);
+
+ assertThat(appUsageEvent).isNull();
+ }
+
+ @Test
+ public void convertToAppUsageEvent_failToGetUid_returnsNull()
+ throws PackageManager.NameNotFoundException {
+ final Event event = new Event();
+ event.mEventType = UsageEvents.Event.DEVICE_SHUTDOWN;
+ event.mPackage = "com.android.settings1";
+ when(mMockPackageManager.getPackageUidAsUser(any(), anyInt()))
+ .thenThrow(new PackageManager.NameNotFoundException());
+
+ final long userId = 1;
+ final AppUsageEvent appUsageEvent = ConvertUtils.convertToAppUsageEvent(
+ mContext, event, userId);
+
+ assertThat(appUsageEvent).isNull();
+ }
+
+ @Test
public void getLocale_nullContext_returnDefaultLocale() {
assertThat(ConvertUtils.getLocale(/*context=*/ null))
.isEqualTo(Locale.getDefault());
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 210a21b..ce958c9 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DataProcessorTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DataProcessorTest.java
@@ -18,6 +18,7 @@
import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.doReturn;
@@ -25,12 +26,19 @@
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
+import android.app.usage.IUsageStatsManager;
+import android.app.usage.UsageEvents;
+import android.app.usage.UsageEvents.Event;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
+import android.content.pm.UserInfo;
import android.os.BatteryConsumer;
import android.os.BatteryManager;
import android.os.BatteryUsageStats;
+import android.os.Parcel;
+import android.os.RemoteException;
+import android.os.UserManager;
import android.text.format.DateUtils;
import com.android.settings.fuelgauge.BatteryUtils;
@@ -65,10 +73,13 @@
@Mock private Intent mIntent;
@Mock private BatteryUsageStats mBatteryUsageStats;
+ @Mock private UserManager mUserManager;
+ @Mock private IUsageStatsManager mUsageStatsManager;
@Mock private BatteryEntry mMockBatteryEntry1;
@Mock private BatteryEntry mMockBatteryEntry2;
@Mock private BatteryEntry mMockBatteryEntry3;
@Mock private BatteryEntry mMockBatteryEntry4;
+ @Mock private UsageEvents mUsageEvents1;
@Before
@@ -80,9 +91,11 @@
mFeatureFactory = FakeFeatureFactory.setupForTest();
mPowerUsageFeatureProvider = mFeatureFactory.powerUsageFeatureProvider;
+ DataProcessor.sUsageStatsManager = mUsageStatsManager;
doReturn(mIntent).when(mContext).registerReceiver(any(), any());
doReturn(100).when(mIntent).getIntExtra(eq(BatteryManager.EXTRA_SCALE), anyInt());
doReturn(66).when(mIntent).getIntExtra(eq(BatteryManager.EXTRA_LEVEL), anyInt());
+ doReturn(mUserManager).when(mContext).getSystemService(UserManager.class);
}
@Test
@@ -146,6 +159,86 @@
}
@Test
+ public void getAppUsageEvents_returnExpectedResult() throws RemoteException {
+ UserInfo userInfo = new UserInfo(/*id=*/ 0, "user_0", /*flags=*/ 0);
+ final List<UserInfo> userInfoList = new ArrayList<>();
+ userInfoList.add(userInfo);
+ doReturn(userInfoList).when(mUserManager).getAliveUsers();
+ doReturn(true).when(mUserManager).isUserUnlocked(userInfo.id);
+ doReturn(mUsageEvents1)
+ .when(mUsageStatsManager)
+ .queryEventsForUser(anyLong(), anyLong(), anyInt(), any());
+
+ final Map<Long, UsageEvents> resultMap = DataProcessor.getAppUsageEvents(mContext);
+
+ assertThat(resultMap.size()).isEqualTo(1);
+ assertThat(resultMap.get(Long.valueOf(userInfo.id))).isEqualTo(mUsageEvents1);
+ }
+
+ @Test
+ public void getAppUsageEvents_lockedUser_returnNull() throws RemoteException {
+ UserInfo userInfo = new UserInfo(/*id=*/ 0, "user_0", /*flags=*/ 0);
+ final List<UserInfo> userInfoList = new ArrayList<>();
+ userInfoList.add(userInfo);
+ doReturn(userInfoList).when(mUserManager).getAliveUsers();
+ // Test locked user.
+ doReturn(false).when(mUserManager).isUserUnlocked(userInfo.id);
+
+ final Map<Long, UsageEvents> resultMap = DataProcessor.getAppUsageEvents(mContext);
+
+ assertThat(resultMap).isNull();
+ }
+
+ @Test
+ public void getAppUsageEvents_nullUsageEvents_returnNull() throws RemoteException {
+ UserInfo userInfo = new UserInfo(/*id=*/ 0, "user_0", /*flags=*/ 0);
+ final List<UserInfo> userInfoList = new ArrayList<>();
+ userInfoList.add(userInfo);
+ doReturn(userInfoList).when(mUserManager).getAliveUsers();
+ doReturn(true).when(mUserManager).isUserUnlocked(userInfo.id);
+ doReturn(null)
+ .when(mUsageStatsManager).queryEventsForUser(anyLong(), anyLong(), anyInt(), any());
+
+ final Map<Long, UsageEvents> resultMap = DataProcessor.getAppUsageEvents(mContext);
+
+ assertThat(resultMap).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);
+ Event event4 = getUsageEvent(Event.DEVICE_SHUTDOWN, /*timestamp=*/ 4);
+ Event event5 = getUsageEvent(Event.ACTIVITY_RESUMED, /*timestamp=*/ 5);
+ event5.mPackage = null;
+ List<Event> events1 = new ArrayList<>();
+ events1.add(event1);
+ events1.add(event2);
+ List<Event> events2 = new ArrayList<>();
+ events2.add(event3);
+ events2.add(event4);
+ events2.add(event5);
+ final long userId1 = 101L;
+ final long userId2 = 102L;
+ final long userId3 = 103L;
+ final Map<Long, UsageEvents> appUsageEvents = new HashMap();
+ appUsageEvents.put(userId1, getUsageEvents(events1));
+ appUsageEvents.put(userId2, getUsageEvents(events2));
+ appUsageEvents.put(userId3, getUsageEvents(new ArrayList<>()));
+
+ final List<AppUsageEvent> appUsageEventList =
+ DataProcessor.generateAppUsageEventListFromUsageEvents(mContext, appUsageEvents);
+
+ assertThat(appUsageEventList.size()).isEqualTo(3);
+ assetAppUsageEvent(
+ appUsageEventList.get(0), AppUsageEventType.ACTIVITY_RESUMED, /*timestamp=*/ 2);
+ assetAppUsageEvent(
+ appUsageEventList.get(1), AppUsageEventType.ACTIVITY_STOPPED, /*timestamp=*/ 3);
+ assetAppUsageEvent(
+ appUsageEventList.get(2), AppUsageEventType.DEVICE_SHUTDOWN, /*timestamp=*/ 4);
+ }
+
+ @Test
public void getHistoryMapWithExpectedTimestamps_emptyHistoryMap_returnEmptyMap() {
assertThat(DataProcessor
.getHistoryMapWithExpectedTimestamps(mContext, new HashMap<>()))
@@ -1213,6 +1306,30 @@
return new BatteryHistEntry(values);
}
+ private UsageEvents getUsageEvents(final List<Event> 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 Event getUsageEvent(
+ final int eventType, final long timestamp) {
+ final Event event = new Event();
+ event.mEventType = eventType;
+ event.mPackage = "package";
+ event.mTimeStamp = timestamp;
+ return event;
+ }
+
+ private void assetAppUsageEvent(
+ final AppUsageEvent event, final AppUsageEventType eventType, final long timestamp) {
+ assertThat(event.getType()).isEqualTo(eventType);
+ assertThat(event.getTimestamp()).isEqualTo(timestamp);
+ }
+
private static void verifyExpectedBatteryLevelData(
final BatteryLevelData resultData,
final List<Long> expectedDailyTimestamps,
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtilsTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtilsTest.java
index 0c66267..a04baa0 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtilsTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtilsTest.java
@@ -22,6 +22,7 @@
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
import android.content.ContentResolver;
import android.content.ContentValues;
@@ -34,6 +35,7 @@
import android.os.UserHandle;
import android.os.UserManager;
+import com.android.settings.fuelgauge.batteryusage.db.AppUsageEventEntity;
import com.android.settings.testutils.BatteryTestUtils;
import org.junit.Before;
@@ -94,6 +96,53 @@
}
@Test
+ public void sendAppUsageEventData_returnsExpectedList() {
+ // Configures the testing AppUsageEvent data.
+ final List<AppUsageEvent> appUsageEventList = new ArrayList<>();
+ final AppUsageEvent appUsageEvent1 =
+ AppUsageEvent.newBuilder()
+ .setUid(101L)
+ .setType(AppUsageEventType.ACTIVITY_RESUMED)
+ .build();
+ final AppUsageEvent appUsageEvent2 =
+ AppUsageEvent.newBuilder()
+ .setUid(1001L)
+ .setType(AppUsageEventType.ACTIVITY_STOPPED)
+ .build();
+ final AppUsageEvent appUsageEvent3 =
+ AppUsageEvent.newBuilder()
+ .setType(AppUsageEventType.DEVICE_SHUTDOWN)
+ .build();
+ appUsageEventList.add(appUsageEvent1);
+ appUsageEventList.add(appUsageEvent2);
+ appUsageEventList.add(appUsageEvent3);
+
+ final List<ContentValues> valuesList =
+ DatabaseUtils.sendAppUsageEventData(mContext, appUsageEventList);
+
+ assertThat(valuesList).hasSize(2);
+ assertThat(valuesList.get(0).getAsInteger(AppUsageEventEntity.KEY_APP_USAGE_EVENT_TYPE))
+ .isEqualTo(1);
+ assertThat(valuesList.get(1).getAsInteger(AppUsageEventEntity.KEY_APP_USAGE_EVENT_TYPE))
+ .isEqualTo(2);
+ // Verifies the inserted ContentValues into content provider.
+ final ContentValues[] valuesArray =
+ new ContentValues[] {valuesList.get(0), valuesList.get(1)};
+ verify(mMockContentResolver).bulkInsert(
+ DatabaseUtils.APP_USAGE_EVENT_URI, valuesArray);
+ verify(mMockContentResolver).notifyChange(
+ DatabaseUtils.APP_USAGE_EVENT_URI, /*observer=*/ null);
+ }
+
+ @Test
+ public void sendAppUsageEventData_emptyAppUsageEventList_notSend() {
+ final List<ContentValues> valuesList =
+ DatabaseUtils.sendAppUsageEventData(mContext, new ArrayList<>());
+ assertThat(valuesList).hasSize(0);
+ verifyNoMoreInteractions(mMockContentResolver);
+ }
+
+ @Test
public void sendBatteryEntryData_nullBatteryIntent_returnsNullValue() {
doReturn(null).when(mContext).registerReceiver(any(), any());
assertThat(
@@ -123,8 +172,8 @@
assertThat(valuesList).hasSize(2);
// Verifies the ContentValues content.
- verifyContentValues(0.5, valuesList.get(0));
- verifyContentValues(0.0, valuesList.get(1));
+ verifyBatteryEntryContentValues(0.5, valuesList.get(0));
+ verifyBatteryEntryContentValues(0.0, valuesList.get(1));
// Verifies the inserted ContentValues into content provider.
final ContentValues[] valuesArray =
new ContentValues[] {valuesList.get(0), valuesList.get(1)};
@@ -146,7 +195,7 @@
/*isFullChargeStart=*/ false);
assertThat(valuesList).hasSize(1);
- verifyFakeContentValues(valuesList.get(0));
+ verifyFakeBatteryEntryContentValues(valuesList.get(0));
// Verifies the inserted ContentValues into content provider.
verify(mMockContentResolver).insert(any(), any());
verify(mMockContentResolver).notifyChange(
@@ -165,7 +214,7 @@
/*isFullChargeStart=*/ false);
assertThat(valuesList).hasSize(1);
- verifyFakeContentValues(valuesList.get(0));
+ verifyFakeBatteryEntryContentValues(valuesList.get(0));
// Verifies the inserted ContentValues into content provider.
verify(mMockContentResolver).insert(any(), any());
verify(mMockContentResolver).notifyChange(
@@ -184,7 +233,7 @@
/*isFullChargeStart=*/ false);
assertThat(valuesList).hasSize(1);
- verifyFakeContentValues(valuesList.get(0));
+ verifyFakeBatteryEntryContentValues(valuesList.get(0));
// Verifies the inserted ContentValues into content provider.
verify(mMockContentResolver).insert(any(), any());
verify(mMockContentResolver).notifyChange(
@@ -192,13 +241,49 @@
}
@Test
+ public void getAppUsageStartTimestampOfUser_emptyCursorContent_returnEarliestTimestamp() {
+ final MatrixCursor cursor =
+ new MatrixCursor(new String[] {AppUsageEventEntity.KEY_TIMESTAMP});
+ DatabaseUtils.sFakeAppUsageLatestTimestampSupplier = () -> cursor;
+
+ final long earliestTimestamp = 10001L;
+ assertThat(DatabaseUtils.getAppUsageStartTimestampOfUser(
+ mContext, /*userId=*/ 0, earliestTimestamp)).isEqualTo(earliestTimestamp);
+ }
+
+ @Test
+ public void getAppUsageStartTimestampOfUser_nullCursor_returnEarliestTimestamp() {
+ DatabaseUtils.sFakeAppUsageLatestTimestampSupplier = () -> null;
+ final long earliestTimestamp = 10001L;
+ assertThat(DatabaseUtils.getAppUsageStartTimestampOfUser(
+ mContext, /*userId=*/ 0, earliestTimestamp)).isEqualTo(earliestTimestamp);
+ }
+
+ @Test
+ public void getAppUsageStartTimestampOfUser_returnExpectedResult() {
+ final long returnedTimestamp = 10001L;
+ final MatrixCursor cursor =
+ new MatrixCursor(new String[] {AppUsageEventEntity.KEY_TIMESTAMP});
+ // Adds fake data into the cursor.
+ cursor.addRow(new Object[] {returnedTimestamp});
+ DatabaseUtils.sFakeAppUsageLatestTimestampSupplier = () -> cursor;
+
+ final long earliestTimestamp1 = 1001L;
+ assertThat(DatabaseUtils.getAppUsageStartTimestampOfUser(
+ mContext, /*userId=*/ 0, earliestTimestamp1)).isEqualTo(returnedTimestamp);
+ final long earliestTimestamp2 = 100001L;
+ assertThat(DatabaseUtils.getAppUsageStartTimestampOfUser(
+ mContext, /*userId=*/ 0, earliestTimestamp2)).isEqualTo(earliestTimestamp2);
+ }
+
+ @Test
public void getHistoryMapSinceLastFullCharge_emptyCursorContent_returnEmptyMap() {
final MatrixCursor cursor = new MatrixCursor(
new String[] {
BatteryHistEntry.KEY_UID,
BatteryHistEntry.KEY_USER_ID,
BatteryHistEntry.KEY_TIMESTAMP});
- doReturn(cursor).when(mMockContentResolver).query(any(), any(), any(), any());
+ DatabaseUtils.sFakeBatteryStateSupplier = () -> cursor;
assertThat(DatabaseUtils.getHistoryMapSinceLastFullCharge(
mContext, /*calendar=*/ null)).isEmpty();
@@ -206,7 +291,7 @@
@Test
public void getHistoryMapSinceLastFullCharge_nullCursor_returnEmptyMap() {
- doReturn(null).when(mMockContentResolver).query(any(), any(), any(), any());
+ DatabaseUtils.sFakeBatteryStateSupplier = () -> null;
assertThat(DatabaseUtils.getHistoryMapSinceLastFullCharge(
mContext, /*calendar=*/ null)).isEmpty();
}
@@ -216,7 +301,6 @@
final Long timestamp1 = Long.valueOf(1001L);
final Long timestamp2 = Long.valueOf(1002L);
final MatrixCursor cursor = getMatrixCursor();
- doReturn(cursor).when(mMockContentResolver).query(any(), any(), any(), any());
// Adds fake data into the cursor.
cursor.addRow(new Object[] {
"app name1", timestamp1, 1, ConvertUtils.CONSUMER_TYPE_UID_BATTERY});
@@ -226,6 +310,7 @@
"app name3", timestamp2, 3, ConvertUtils.CONSUMER_TYPE_UID_BATTERY});
cursor.addRow(new Object[] {
"app name4", timestamp2, 4, ConvertUtils.CONSUMER_TYPE_UID_BATTERY});
+ DatabaseUtils.sFakeBatteryStateSupplier = () -> cursor;
final Map<Long, Map<String, BatteryHistEntry>> batteryHistMap =
DatabaseUtils.getHistoryMapSinceLastFullCharge(
@@ -251,9 +336,7 @@
doReturn(mMockContext).when(mContext).createPackageContextAsUser(
"com.fake.package", /*flags=*/ 0, UserHandle.OWNER);
BatteryTestUtils.setWorkProfile(mContext);
- doReturn(getMatrixCursor()).when(mMockContentResolver2)
- .query(any(), any(), any(), any());
- doReturn(null).when(mMockContentResolver).query(any(), any(), any(), any());
+ DatabaseUtils.sFakeBatteryStateSupplier = () -> getMatrixCursor();
final Map<Long, Map<String, BatteryHistEntry>> batteryHistMap =
DatabaseUtils.getHistoryMapSinceLastFullCharge(
@@ -262,7 +345,8 @@
assertThat(batteryHistMap).isEmpty();
}
- private static void verifyContentValues(double consumedPower, ContentValues values) {
+ private static void verifyBatteryEntryContentValues(
+ double consumedPower, ContentValues values) {
final BatteryInformation batteryInformation =
ConvertUtils.getBatteryInformation(
values, BatteryHistEntry.KEY_BATTERY_INFORMATION);
@@ -275,7 +359,7 @@
.isEqualTo(BatteryManager.BATTERY_HEALTH_COLD);
}
- private static void verifyFakeContentValues(ContentValues values) {
+ private static void verifyFakeBatteryEntryContentValues(ContentValues values) {
final BatteryInformation batteryInformation =
ConvertUtils.getBatteryInformation(
values, BatteryHistEntry.KEY_BATTERY_INFORMATION);
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/PeriodicJobReceiverTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/PeriodicJobReceiverTest.java
index 3693209..c9a3e64 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/PeriodicJobReceiverTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/PeriodicJobReceiverTest.java
@@ -62,7 +62,7 @@
// Inserts fake data into database for testing.
final BatteryStateDatabase database = BatteryTestUtils.setUpBatteryStateDatabase(mContext);
- BatteryTestUtils.insertDataToBatteryStateDatabase(
+ BatteryTestUtils.insertDataToBatteryStateTable(
mContext, Clock.systemUTC().millis(), "com.android.systemui");
mDao = database.batteryStateDao();
}
@@ -122,9 +122,9 @@
private void insertExpiredData(int shiftDay) {
final long expiredTimeInMs =
Clock.systemUTC().millis() - Duration.ofDays(shiftDay).toMillis();
- BatteryTestUtils.insertDataToBatteryStateDatabase(
+ BatteryTestUtils.insertDataToBatteryStateTable(
mContext, expiredTimeInMs - 1, "com.android.systemui");
- BatteryTestUtils.insertDataToBatteryStateDatabase(
+ BatteryTestUtils.insertDataToBatteryStateTable(
mContext, expiredTimeInMs, "com.android.systemui");
// Ensures the testing environment is correct.
assertThat(mDao.getAllAfter(0)).hasSize(3);
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/bugreport/BugReportContentProviderTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/bugreport/BugReportContentProviderTest.java
index 87e253f..bbbf45f 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/bugreport/BugReportContentProviderTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/bugreport/BugReportContentProviderTest.java
@@ -53,9 +53,9 @@
mBugReportContentProvider.attachInfo(mContext, /*info=*/ null);
// Inserts fake data into database for testing.
BatteryTestUtils.setUpBatteryStateDatabase(mContext);
- BatteryTestUtils.insertDataToBatteryStateDatabase(
+ BatteryTestUtils.insertDataToBatteryStateTable(
mContext, System.currentTimeMillis(), PACKAGE_NAME1);
- BatteryTestUtils.insertDataToBatteryStateDatabase(
+ BatteryTestUtils.insertDataToBatteryStateTable(
mContext, System.currentTimeMillis(), PACKAGE_NAME2);
}
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/db/AppUsageEventDaoTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/db/AppUsageEventDaoTest.java
new file mode 100644
index 0000000..ade585f
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/db/AppUsageEventDaoTest.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.fuelgauge.batteryusage.db;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+import android.database.Cursor;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.settings.testutils.BatteryTestUtils;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+import java.util.List;
+
+/** Tests for {@link AppUsageEventDao}. */
+@RunWith(RobolectricTestRunner.class)
+public final class AppUsageEventDaoTest {
+ 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 long USER_ID1 = 1;
+ private static final long USER_ID2 = 2;
+ private static final String PACKAGE_NAME1 = "com.android.apps.settings";
+ private static final String PACKAGE_NAME2 = "com.android.apps.calendar";
+ private static final String PACKAGE_NAME3 = "com.android.apps.gmail";
+
+ private Context mContext;
+ private BatteryStateDatabase mDatabase;
+ private AppUsageEventDao mAppUsageEventDao;
+
+ @Before
+ public void setUp() {
+ mContext = ApplicationProvider.getApplicationContext();
+ mDatabase = BatteryTestUtils.setUpBatteryStateDatabase(mContext);
+ mAppUsageEventDao = mDatabase.appUsageEventDao();
+ BatteryTestUtils.insertDataToAppUsageEventTable(
+ mContext, USER_ID1, TIMESTAMP3, PACKAGE_NAME3);
+ BatteryTestUtils.insertDataToAppUsageEventTable(
+ mContext, USER_ID2, TIMESTAMP2, PACKAGE_NAME2);
+ BatteryTestUtils.insertDataToAppUsageEventTable(
+ mContext, USER_ID1, TIMESTAMP1, PACKAGE_NAME1, /*multiple=*/ true);
+ }
+
+ @After
+ public void closeDb() {
+ mDatabase.close();
+ BatteryStateDatabase.setBatteryStateDatabase(/*database=*/ null);
+ }
+
+ @Test
+ public void appUsageEventDao_insertAll() throws Exception {
+ final List<AppUsageEventEntity> entities = mAppUsageEventDao.getAllAfter(TIMESTAMP1);
+ assertThat(entities).hasSize(2);
+ // Verifies the queried battery states.
+ assertAppUsageEvent(entities.get(0), TIMESTAMP3, PACKAGE_NAME3);
+ assertAppUsageEvent(entities.get(1), TIMESTAMP2, PACKAGE_NAME2);
+ }
+
+ @Test
+ public void appUsageEventDao_getLatestTimestampOfUser() throws Exception {
+ final Cursor cursor1 = mAppUsageEventDao.getLatestTimestampOfUser(USER_ID1);
+ assertThat(cursor1.getCount()).isEqualTo(1);
+ cursor1.moveToFirst();
+ assertThat(cursor1.getLong(0)).isEqualTo(TIMESTAMP3);
+
+ final Cursor cursor2 = mAppUsageEventDao.getLatestTimestampOfUser(USER_ID2);
+ assertThat(cursor2.getCount()).isEqualTo(1);
+ cursor2.moveToFirst();
+ assertThat(cursor2.getLong(0)).isEqualTo(TIMESTAMP2);
+
+ final long notExistingUserId = 3;
+ final Cursor cursor3 = mAppUsageEventDao.getLatestTimestampOfUser(notExistingUserId);
+ assertThat(cursor3.getCount()).isEqualTo(1);
+ cursor3.moveToFirst();
+ assertThat(cursor3.getLong(0)).isEqualTo(0);
+ }
+
+ @Test
+ public void appUsageEventDao_clearAllBefore() throws Exception {
+ mAppUsageEventDao.clearAllBefore(TIMESTAMP2);
+
+ final List<AppUsageEventEntity> entities = mAppUsageEventDao.getAllAfter(0);
+ assertThat(entities).hasSize(1);
+ // Verifies the queried battery state.
+ assertAppUsageEvent(entities.get(0), TIMESTAMP3, PACKAGE_NAME3);
+ }
+
+ @Test
+ public void appUsageEventDao_clearAll() throws Exception {
+ assertThat(mAppUsageEventDao.getAllAfter(0)).hasSize(3);
+ mAppUsageEventDao.clearAll();
+ assertThat(mAppUsageEventDao.getAllAfter(0)).isEmpty();
+ }
+
+ @Test
+ public void getInstance_createNewInstance() throws Exception {
+ BatteryStateDatabase.setBatteryStateDatabase(/*database=*/ null);
+ assertThat(BatteryStateDatabase.getInstance(mContext)).isNotNull();
+ }
+
+ private static void assertAppUsageEvent(
+ AppUsageEventEntity entity, long timestamp, String packageName) {
+ assertThat(entity.timestamp).isEqualTo(timestamp);
+ assertThat(entity.packageName).isEqualTo(packageName);
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/db/AppUsageEventEntityTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/db/AppUsageEventEntityTest.java
new file mode 100644
index 0000000..3cbf845
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/db/AppUsageEventEntityTest.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.fuelgauge.batteryusage.db;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+/** Tests for {@link AppUsageEventEntity}. */
+@RunWith(RobolectricTestRunner.class)
+public final class AppUsageEventEntityTest {
+ @Test
+ public void testBuilder_returnsExpectedResult() {
+ final long uid = 101L;
+ final long userId = 1001L;
+ final long timestamp = 10001L;
+ final int appUsageEventType = 1;
+ final String packageName = "com.android.settings1";
+ final int instanceId = 100001;
+ final String taskRootPackageName = "com.android.settings2";
+
+ AppUsageEventEntity entity = AppUsageEventEntity
+ .newBuilder()
+ .setUid(uid)
+ .setUserId(userId)
+ .setTimestamp(timestamp)
+ .setAppUsageEventType(appUsageEventType)
+ .setPackageName(packageName)
+ .setInstanceId(instanceId)
+ .setTaskRootPackageName(taskRootPackageName)
+ .build();
+
+ // Verifies the app relative information.
+ assertThat(entity.uid).isEqualTo(uid);
+ assertThat(entity.userId).isEqualTo(userId);
+ assertThat(entity.timestamp).isEqualTo(timestamp);
+ assertThat(entity.appUsageEventType).isEqualTo(appUsageEventType);
+ assertThat(entity.packageName).isEqualTo(packageName);
+ assertThat(entity.instanceId).isEqualTo(instanceId);
+ assertThat(entity.taskRootPackageName).isEqualTo(taskRootPackageName);
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/db/BatteryStateDaoTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/db/BatteryStateDaoTest.java
index 3b887ad..57cf648 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/db/BatteryStateDaoTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/db/BatteryStateDaoTest.java
@@ -53,9 +53,9 @@
mContext = ApplicationProvider.getApplicationContext();
mDatabase = BatteryTestUtils.setUpBatteryStateDatabase(mContext);
mBatteryStateDao = mDatabase.batteryStateDao();
- BatteryTestUtils.insertDataToBatteryStateDatabase(mContext, TIMESTAMP3, PACKAGE_NAME3);
- BatteryTestUtils.insertDataToBatteryStateDatabase(mContext, TIMESTAMP2, PACKAGE_NAME2);
- BatteryTestUtils.insertDataToBatteryStateDatabase(
+ BatteryTestUtils.insertDataToBatteryStateTable(mContext, TIMESTAMP3, PACKAGE_NAME3);
+ BatteryTestUtils.insertDataToBatteryStateTable(mContext, TIMESTAMP2, PACKAGE_NAME2);
+ BatteryTestUtils.insertDataToBatteryStateTable(
mContext, TIMESTAMP1, PACKAGE_NAME1, /*multiple=*/ true,
/*isFullChargeStart=*/ true);
}
@@ -102,9 +102,9 @@
public void batteryStateDao_getCursorSinceLastFullCharge_noFullChargeData_returnSevenDaysData()
throws Exception {
mBatteryStateDao.clearAll();
- BatteryTestUtils.insertDataToBatteryStateDatabase(mContext, TIMESTAMP3, PACKAGE_NAME3);
- BatteryTestUtils.insertDataToBatteryStateDatabase(mContext, TIMESTAMP2, PACKAGE_NAME2);
- BatteryTestUtils.insertDataToBatteryStateDatabase(mContext, TIMESTAMP1, PACKAGE_NAME1);
+ BatteryTestUtils.insertDataToBatteryStateTable(mContext, TIMESTAMP3, PACKAGE_NAME3);
+ BatteryTestUtils.insertDataToBatteryStateTable(mContext, TIMESTAMP2, PACKAGE_NAME2);
+ BatteryTestUtils.insertDataToBatteryStateTable(mContext, TIMESTAMP1, PACKAGE_NAME1);
final Cursor cursor = mBatteryStateDao.getCursorSinceLastFullCharge(TIMESTAMP2);
assertThat(cursor.getCount()).isEqualTo(2);
assertThat(cursor.getColumnCount()).isEqualTo(CURSOR_COLUMN_SIZE);
diff --git a/tests/robotests/src/com/android/settings/testutils/BatteryTestUtils.java b/tests/robotests/src/com/android/settings/testutils/BatteryTestUtils.java
index f119d6e6..c7680b5 100644
--- a/tests/robotests/src/com/android/settings/testutils/BatteryTestUtils.java
+++ b/tests/robotests/src/com/android/settings/testutils/BatteryTestUtils.java
@@ -26,6 +26,8 @@
import com.android.settings.fuelgauge.batteryusage.BatteryInformation;
import com.android.settings.fuelgauge.batteryusage.ConvertUtils;
import com.android.settings.fuelgauge.batteryusage.DeviceBatteryState;
+import com.android.settings.fuelgauge.batteryusage.db.AppUsageEventDao;
+import com.android.settings.fuelgauge.batteryusage.db.AppUsageEventEntity;
import com.android.settings.fuelgauge.batteryusage.db.BatteryState;
import com.android.settings.fuelgauge.batteryusage.db.BatteryStateDao;
import com.android.settings.fuelgauge.batteryusage.db.BatteryStateDatabase;
@@ -70,21 +72,21 @@
}
/** Inserts a fake data into the database for testing. */
- public static void insertDataToBatteryStateDatabase(
+ public static void insertDataToBatteryStateTable(
Context context, long timestamp, String packageName) {
- insertDataToBatteryStateDatabase(
+ insertDataToBatteryStateTable(
context, timestamp, packageName, /*multiple=*/ false, /*isFullChargeStart=*/ false);
}
/** Inserts a fake data into the database for testing. */
- public static void insertDataToBatteryStateDatabase(
+ public static void insertDataToBatteryStateTable(
Context context, long timestamp, String packageName, boolean isFullChargeStart) {
- insertDataToBatteryStateDatabase(
+ insertDataToBatteryStateTable(
context, timestamp, packageName, /*multiple=*/ false, isFullChargeStart);
}
/** Inserts a fake data into the database for testing. */
- public static void insertDataToBatteryStateDatabase(
+ public static void insertDataToBatteryStateTable(
Context context, long timestamp, String packageName, boolean multiple,
boolean isFullChargeStart) {
DeviceBatteryState deviceBatteryState =
@@ -133,6 +135,34 @@
}
}
+ /** Inserts a fake data into the database for testing. */
+ public static void insertDataToAppUsageEventTable(
+ Context context, long userId, long timestamp, String packageName) {
+ insertDataToAppUsageEventTable(
+ context, userId, timestamp, packageName, /*multiple=*/ false);
+ }
+
+ /** Inserts a fake data into the database for testing. */
+ public static void insertDataToAppUsageEventTable(
+ Context context, long userId, long timestamp, String packageName, boolean multiple) {
+ final AppUsageEventEntity entity =
+ new AppUsageEventEntity(
+ /*uid=*/ 101L,
+ userId,
+ timestamp,
+ /*appUsageEventType=*/ 2,
+ packageName,
+ /*instanceId=*/ 10001,
+ /*taskRootPackageName=*/ "com.android.settings");
+ AppUsageEventDao dao =
+ BatteryStateDatabase.getInstance(context).appUsageEventDao();
+ if (multiple) {
+ dao.insertAll(ImmutableList.of(entity));
+ } else {
+ dao.insert(entity);
+ }
+ }
+
public static Intent getCustomBatteryIntent(int plugged, int level, int scale, int status) {
Intent intent = new Intent();
intent.putExtra(BatteryManager.EXTRA_PLUGGED, plugged);