Load app usage events data in the hourly job.

Test: make RunSettingsRoboTests + manual
Bug: 260964679
Change-Id: Iaccaa77bd52fb7356cdcb786c64523f21040b128
This commit is contained in:
Kuan Wang
2022-12-08 10:21:43 +08:00
parent 52ad3ba925
commit 6c4f83f33d
29 changed files with 1590 additions and 93 deletions

View File

@@ -141,12 +141,17 @@ public interface PowerUsageFeatureProvider {
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);
}

View File

@@ -165,4 +165,9 @@ public class PowerUsageFeatureProviderImpl implements PowerUsageFeatureProvider
public CharSequence[] getHideApplicationEntries(Context context) {
return new CharSequence[0];
}
@Override
public Set<CharSequence> getIgnoreScreenOnTimeTaskRootSet(Context context) {
return new ArraySet<>();
}
}

View File

@@ -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);
}
}
}

View File

@@ -29,6 +29,8 @@ import androidx.annotation.NonNull;
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 @@ public class BatteryUsageContentProvider extends ContentProvider {
@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 @@ public class BatteryUsageContentProvider extends ContentProvider {
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 @@ public class BatteryUsageContentProvider extends ContentProvider {
}
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 @@ public class BatteryUsageContentProvider extends ContentProvider {
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 @@ public class BatteryUsageContentProvider extends ContentProvider {
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 @@ public class BatteryUsageContentProvider extends ContentProvider {
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;
}
}
}

View File

@@ -58,7 +58,7 @@ public final class BatteryUsageDataLoader {
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);

View File

@@ -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.LocaleList;
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 @@ public final class ConvertUtils {
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 @@ public final class ConvertUtils {
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 @@ public final class ConvertUtils {
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 @@ public final class ConvertUtils {
BatteryEntry entry,
BatteryUsageStats batteryUsageStats) {
return new BatteryHistEntry(
convertToContentValues(
convertBatteryEntryToContentValues(
entry,
batteryUsageStats,
/*batteryLevel=*/ 0,
@@ -146,6 +164,51 @@ public final class ConvertUtils {
/*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 @@ public final class ConvertUtils {
: 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,

View File

@@ -18,10 +18,14 @@ package com.android.settings.fuelgauge.batteryusage;
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.BatteryUsageStatsQuery;
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 @@ public final class DataProcessor {
@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. */
@@ -183,6 +194,55 @@ public final class DataProcessor {
.getBatteryUsageStats(batteryUsageStatsQuery);
}
/**
* 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.
*/
@@ -196,6 +256,59 @@ public final class DataProcessor {
}
}
/**
* 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}.
*/
@@ -508,13 +621,30 @@ public final class DataProcessor {
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 @@ public final class DataProcessor {
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) {

View File

@@ -45,12 +45,11 @@ import java.util.Calendar;
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 final class DatabaseUtils {
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 @@ public final class DatabaseUtils {
.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 @@ public final class DatabaseUtils {
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 final class DatabaseUtils {
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 final class DatabaseUtils {
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 @@ public final class DatabaseUtils {
|| backgroundMs != 0;
})
.forEach(entry -> valuesList.add(
ConvertUtils.convertToContentValues(
ConvertUtils.convertBatteryEntryToContentValues(
entry,
batteryUsageStats,
batteryLevel,
@@ -197,15 +282,15 @@ public final class DatabaseUtils {
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 @@ public final class DatabaseUtils {
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 @@ public final class DatabaseUtils {
}
}
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 @@ public final class DatabaseUtils {
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();
}
}

View File

@@ -40,6 +40,7 @@ public final class PeriodicJobReceiver extends BroadcastReceiver {
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);

View File

@@ -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();
}

View File

@@ -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() {}
}
}

View File

@@ -204,14 +204,14 @@ public class BatteryState {
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;

View File

@@ -25,7 +25,7 @@ import androidx.room.RoomDatabase;
/** 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 @@ public abstract class BatteryStateDatabase extends RoomDatabase {
/** 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()

View File

@@ -13,4 +13,12 @@ java_library {
type: "lite",
},
srcs: ["fuelgauge_usage_state.proto"],
}
java_library {
name: "app-usage-event-protos-lite",
proto: {
type: "lite",
},
srcs: ["app_usage_event.proto"],
}

View File

@@ -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;
}