Load app usage events data in the hourly job.
Test: make RunSettingsRoboTests + manual Bug: 260964679 Change-Id: Iaccaa77bd52fb7356cdcb786c64523f21040b128
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
@@ -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<>();
|
||||
}
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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);
|
||||
|
@@ -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,
|
||||
|
@@ -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) {
|
||||
|
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -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);
|
||||
|
@@ -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();
|
||||
}
|
@@ -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() {}
|
||||
}
|
||||
|
||||
|
||||
}
|
@@ -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;
|
||||
|
@@ -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()
|
||||
|
@@ -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"],
|
||||
}
|
@@ -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;
|
||||
}
|
Reference in New Issue
Block a user