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

@@ -80,6 +80,7 @@ android_library {
"guava", "guava",
"jsr305", "jsr305",
"net-utils-framework-common", "net-utils-framework-common",
"app-usage-event-protos-lite",
"settings-contextual-card-protos-lite", "settings-contextual-card-protos-lite",
"settings-log-bridge-protos-lite", "settings-log-bridge-protos-lite",
"settings-telephony-protos-lite", "settings-telephony-protos-lite",

View File

@@ -141,12 +141,17 @@ public interface PowerUsageFeatureProvider {
Intent getResumeChargeIntent(boolean isDockDefender); 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); 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); 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) { public CharSequence[] getHideApplicationEntries(Context context) {
return new CharSequence[0]; 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.Nullable;
import androidx.annotation.VisibleForTesting; 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.BatteryState;
import com.android.settings.fuelgauge.batteryusage.db.BatteryStateDao; import com.android.settings.fuelgauge.batteryusage.db.BatteryStateDao;
import com.android.settings.fuelgauge.batteryusage.db.BatteryStateDatabase; import com.android.settings.fuelgauge.batteryusage.db.BatteryStateDatabase;
@@ -43,11 +45,11 @@ public class BatteryUsageContentProvider extends ContentProvider {
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
public static final Duration QUERY_DURATION_HOURS = Duration.ofDays(6); public static final Duration QUERY_DURATION_HOURS = Duration.ofDays(6);
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
public static final String QUERY_KEY_TIMESTAMP = "timestamp";
/** Codes */ /** Codes */
private static final int BATTERY_STATE_CODE = 1; 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); private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
static { static {
@@ -55,10 +57,19 @@ public class BatteryUsageContentProvider extends ContentProvider {
DatabaseUtils.AUTHORITY, DatabaseUtils.AUTHORITY,
/*path=*/ DatabaseUtils.BATTERY_STATE_TABLE, /*path=*/ DatabaseUtils.BATTERY_STATE_TABLE,
/*code=*/ BATTERY_STATE_CODE); /*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 Clock mClock;
private BatteryStateDao mBatteryStateDao; private BatteryStateDao mBatteryStateDao;
private AppUsageEventDao mAppUsageEventDao;
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
public void setClock(Clock clock) { public void setClock(Clock clock) {
@@ -73,6 +84,7 @@ public class BatteryUsageContentProvider extends ContentProvider {
} }
mClock = Clock.systemUTC(); mClock = Clock.systemUTC();
mBatteryStateDao = BatteryStateDatabase.getInstance(getContext()).batteryStateDao(); mBatteryStateDao = BatteryStateDatabase.getInstance(getContext()).batteryStateDao();
mAppUsageEventDao = BatteryStateDatabase.getInstance(getContext()).appUsageEventDao();
Log.w(TAG, "create content provider from " + getCallingPackage()); Log.w(TAG, "create content provider from " + getCallingPackage());
return true; return true;
} }
@@ -88,6 +100,8 @@ public class BatteryUsageContentProvider extends ContentProvider {
switch (sUriMatcher.match(uri)) { switch (sUriMatcher.match(uri)) {
case BATTERY_STATE_CODE: case BATTERY_STATE_CODE:
return getBatteryStates(uri); return getBatteryStates(uri);
case APP_USAGE_LATEST_TIMESTAMP_CODE:
return getAppUsageLatestTimestamp(uri);
default: default:
throw new IllegalArgumentException("unknown URI: " + uri); throw new IllegalArgumentException("unknown URI: " + uri);
} }
@@ -111,6 +125,14 @@ public class BatteryUsageContentProvider extends ContentProvider {
Log.e(TAG, "insert() from:" + uri + " error:" + e); Log.e(TAG, "insert() from:" + uri + " error:" + e);
return null; 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: default:
throw new IllegalArgumentException("unknown URI: " + uri); throw new IllegalArgumentException("unknown URI: " + uri);
} }
@@ -145,24 +167,54 @@ public class BatteryUsageContentProvider extends ContentProvider {
Log.e(TAG, "query() from:" + uri + " error:" + e); Log.e(TAG, "query() from:" + uri + " error:" + e);
} }
AsyncTask.execute(() -> BootBroadcastReceiver.invokeJobRecheck(getContext())); 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; 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. // If URI contains query parameter QUERY_KEY_TIMESTAMP, use the value directly.
// Otherwise, load the data for QUERY_DURATION_HOURS by default. // Otherwise, load the data for QUERY_DURATION_HOURS by default.
private long getQueryTimestamp(Uri uri, long defaultTimestamp) { private long getQueryTimestamp(Uri uri, long defaultTimestamp) {
final String firstTimestampString = uri.getQueryParameter(QUERY_KEY_TIMESTAMP); Log.d(TAG, "getQueryTimestamp from uri: " + uri);
if (TextUtils.isEmpty(firstTimestampString)) { return getQueryValueFromUri(uri, DatabaseUtils.QUERY_KEY_TIMESTAMP, defaultTimestamp);
Log.w(TAG, "empty query timestamp"); }
return 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 { try {
return Long.parseLong(firstTimestampString); return Long.parseLong(value);
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
Log.e(TAG, "invalid query timestamp: " + firstTimestampString, e); Log.e(TAG, "invalid query value: " + value, e);
return defaultTimestamp; return defaultValue;
} }
} }
} }

View File

@@ -58,7 +58,7 @@ public final class BatteryUsageDataLoader {
final long elapsedTime = System.currentTimeMillis() - start; final long elapsedTime = System.currentTimeMillis() - start;
Log.d(TAG, String.format("getBatteryUsageStats() in %d/ms", elapsedTime)); Log.d(TAG, String.format("getBatteryUsageStats() in %d/ms", elapsedTime));
// Uploads the BatteryEntry data into SettingsIntelligence. // Uploads the BatteryEntry data into database.
DatabaseUtils.sendBatteryEntryData( DatabaseUtils.sendBatteryEntryData(
context, batteryEntryList, batteryUsageStats, isFullChargeStart); context, batteryEntryList, batteryUsageStats, isFullChargeStart);
DataProcessor.closeBatteryUsageStats(batteryUsageStats); DataProcessor.closeBatteryUsageStats(batteryUsageStats);

View File

@@ -16,8 +16,11 @@
package com.android.settings.fuelgauge.batteryusage; package com.android.settings.fuelgauge.batteryusage;
import android.annotation.IntDef; import android.annotation.IntDef;
import android.annotation.Nullable;
import android.app.usage.UsageEvents.Event;
import android.content.ContentValues; import android.content.ContentValues;
import android.content.Context; import android.content.Context;
import android.content.pm.PackageManager;
import android.database.Cursor; import android.database.Cursor;
import android.os.BatteryUsageStats; import android.os.BatteryUsageStats;
import android.os.Build; import android.os.Build;
@@ -25,10 +28,12 @@ import android.os.LocaleList;
import android.os.UserHandle; import android.os.UserHandle;
import android.text.format.DateFormat; import android.text.format.DateFormat;
import android.util.Base64; import android.util.Base64;
import android.util.Log;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import com.android.settings.fuelgauge.BatteryUtils; import com.android.settings.fuelgauge.BatteryUtils;
import com.android.settings.fuelgauge.batteryusage.db.AppUsageEventEntity;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
@@ -49,7 +54,7 @@ public final class ConvertUtils {
CONSUMER_TYPE_SYSTEM_BATTERY, CONSUMER_TYPE_SYSTEM_BATTERY,
}) })
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
public static @interface ConsumerType { public @interface ConsumerType {
} }
public static final int CONSUMER_TYPE_UNKNOWN = 0; public static final int CONSUMER_TYPE_UNKNOWN = 0;
@@ -60,8 +65,8 @@ public final class ConvertUtils {
private ConvertUtils() { private ConvertUtils() {
} }
/** Converts to content values */ /** Converts {@link BatteryEntry} to content values */
public static ContentValues convertToContentValues( public static ContentValues convertBatteryEntryToContentValues(
final BatteryEntry entry, final BatteryEntry entry,
final BatteryUsageStats batteryUsageStats, final BatteryUsageStats batteryUsageStats,
final int batteryLevel, final int batteryLevel,
@@ -103,6 +108,19 @@ public final class ConvertUtils {
return values; 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. */ /** Gets the encoded string from {@link BatteryInformation} instance. */
public static String convertBatteryInformationToString( public static String convertBatteryInformationToString(
final BatteryInformation batteryInformation) { final BatteryInformation batteryInformation) {
@@ -135,7 +153,7 @@ public final class ConvertUtils {
BatteryEntry entry, BatteryEntry entry,
BatteryUsageStats batteryUsageStats) { BatteryUsageStats batteryUsageStats) {
return new BatteryHistEntry( return new BatteryHistEntry(
convertToContentValues( convertBatteryEntryToContentValues(
entry, entry,
batteryUsageStats, batteryUsageStats,
/*batteryLevel=*/ 0, /*batteryLevel=*/ 0,
@@ -146,6 +164,51 @@ public final class ConvertUtils {
/*isFullChargeStart=*/ false)); /*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. */ /** Converts UTC timestamp to human readable local time string. */
public static String utcToLocalTime(Context context, long timestamp) { public static String utcToLocalTime(Context context, long timestamp) {
final Locale locale = getLocale(context); final Locale locale = getLocale(context);
@@ -185,6 +248,50 @@ public final class ConvertUtils {
: Locale.getDefault(); : 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( private static BatteryInformation constructBatteryInformation(
final BatteryEntry entry, final BatteryEntry entry,
final BatteryUsageStats batteryUsageStats, 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 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.ContentValues;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.content.pm.UserInfo;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.BatteryConsumer; import android.os.BatteryConsumer;
import android.os.BatteryStatsManager; import android.os.BatteryStatsManager;
@@ -30,6 +34,8 @@ import android.os.BatteryUsageStatsQuery;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.os.Process; import android.os.Process;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.UidBatteryConsumer; import android.os.UidBatteryConsumer;
import android.os.UserBatteryConsumer; import android.os.UserBatteryConsumer;
import android.os.UserHandle; import android.os.UserHandle;
@@ -90,6 +96,11 @@ public final class DataProcessor {
@VisibleForTesting @VisibleForTesting
static long sFakeCurrentTimeMillis = 0; 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. */ /** A callback listener when battery usage loading async task is executed. */
public interface UsageMapAsyncResponse { public interface UsageMapAsyncResponse {
/** The callback function when batteryUsageMap is loaded. */ /** The callback function when batteryUsageMap is loaded. */
@@ -183,6 +194,55 @@ public final class DataProcessor {
.getBatteryUsageStats(batteryUsageStatsQuery); .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. * 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}. * Generates the list of {@link BatteryEntry} from the supplied {@link BatteryUsageStats}.
*/ */
@@ -508,13 +621,30 @@ public final class DataProcessor {
asyncResponseDelegate).execute(); 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. * @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: * The returned value should be always a 2d map and composed by only 1 part:
* - [SELECTED_INDEX_ALL][SELECTED_INDEX_ALL] * - [SELECTED_INDEX_ALL][SELECTED_INDEX_ALL]
*/ */
@Nullable
private static Map<Integer, Map<Integer, BatteryDiffData>> getBatteryUsageMapFromStatsService( private static Map<Integer, Map<Integer, BatteryDiffData>> getBatteryUsageMapFromStatsService(
final Context context) { final Context context) {
final Map<Integer, Map<Integer, BatteryDiffData>> resultMap = new HashMap<>(); final Map<Integer, Map<Integer, BatteryDiffData>> resultMap = new HashMap<>();
@@ -1335,6 +1465,7 @@ public final class DataProcessor {
return calendar.getTimeInMillis(); return calendar.getTimeInMillis();
} }
/** Whether the Set contains the target. */
private static boolean contains(String target, Set<CharSequence> packageNames) { private static boolean contains(String target, Set<CharSequence> packageNames) {
if (target != null && packageNames != null) { if (target != null && packageNames != null) {
for (CharSequence packageName : packageNames) { for (CharSequence packageName : packageNames) {

View File

@@ -45,12 +45,11 @@ import java.util.Calendar;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.function.Supplier;
/** A utility class to operate battery usage database. */ /** A utility class to operate battery usage database. */
public final class DatabaseUtils { public final class DatabaseUtils {
private static final String TAG = "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. **/ /** 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_THRESHOLD_MS = Duration.ofMinutes(5).toMillis();
private static final long CLEAR_MEMORY_DELAYED_MS = Duration.ofSeconds(2).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"; public static final String AUTHORITY = "com.android.settings.battery.usage.provider";
/** A table name for battery usage history. */ /** A table name for battery usage history. */
public static final String BATTERY_STATE_TABLE = "BatteryState"; 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. */ /** A class name for battery usage data provider. */
public static final String SETTINGS_PACKAGE_PATH = "com.android.settings"; 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. */ /** A content URI to access battery usage states data. */
public static final Uri BATTERY_CONTENT_URI = public static final Uri BATTERY_CONTENT_URI =
@@ -72,6 +81,19 @@ public final class DatabaseUtils {
.authority(AUTHORITY) .authority(AUTHORITY)
.appendPath(BATTERY_STATE_TABLE) .appendPath(BATTERY_STATE_TABLE)
.build(); .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() { private DatabaseUtils() {
} }
@@ -82,6 +104,27 @@ public final class DatabaseUtils {
return userManager.isManagedProfile() && !userManager.isSystemUser(); 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() */ /** Long: for timestamp and String: for BatteryHistEntry.getKey() */
public static Map<Long, Map<String, BatteryHistEntry>> getHistoryMapSinceLastFullCharge( public static Map<Long, Map<String, BatteryHistEntry>> getHistoryMapSinceLastFullCharge(
Context context, Calendar calendar) { Context context, Calendar calendar) {
@@ -113,10 +156,10 @@ public final class DatabaseUtils {
public static void clearAll(Context context) { public static void clearAll(Context context) {
AsyncTask.execute(() -> { AsyncTask.execute(() -> {
try { try {
BatteryStateDatabase final BatteryStateDatabase database = BatteryStateDatabase
.getInstance(context.getApplicationContext()) .getInstance(context.getApplicationContext());
.batteryStateDao() database.batteryStateDao().clearAll();
.clearAll(); database.appUsageEventDao().clearAll();
} catch (RuntimeException e) { } catch (RuntimeException e) {
Log.e(TAG, "clearAll() failed", e); Log.e(TAG, "clearAll() failed", e);
} }
@@ -127,17 +170,59 @@ public final class DatabaseUtils {
public static void clearExpiredDataIfNeeded(Context context) { public static void clearExpiredDataIfNeeded(Context context) {
AsyncTask.execute(() -> { AsyncTask.execute(() -> {
try { try {
BatteryStateDatabase final BatteryStateDatabase database = BatteryStateDatabase
.getInstance(context.getApplicationContext()) .getInstance(context.getApplicationContext());
.batteryStateDao() final long earliestTimestamp = Clock.systemUTC().millis()
.clearAllBefore(Clock.systemUTC().millis() - Duration.ofDays(DATA_RETENTION_INTERVAL_DAY).toMillis();
- Duration.ofDays(DATA_RETENTION_INTERVAL_DAY).toMillis()); database.batteryStateDao().clearAllBefore(earliestTimestamp);
database.appUsageEventDao().clearAllBefore(earliestTimestamp);
} catch (RuntimeException e) { } catch (RuntimeException e) {
Log.e(TAG, "clearAllBefore() failed", 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( static List<ContentValues> sendBatteryEntryData(
final Context context, final Context context,
final List<BatteryEntry> batteryEntryList, final List<BatteryEntry> batteryEntryList,
@@ -178,7 +263,7 @@ public final class DatabaseUtils {
|| backgroundMs != 0; || backgroundMs != 0;
}) })
.forEach(entry -> valuesList.add( .forEach(entry -> valuesList.add(
ConvertUtils.convertToContentValues( ConvertUtils.convertBatteryEntryToContentValues(
entry, entry,
batteryUsageStats, batteryUsageStats,
batteryLevel, batteryLevel,
@@ -197,15 +282,15 @@ public final class DatabaseUtils {
valuesList.toArray(valuesArray); valuesList.toArray(valuesArray);
try { try {
size = resolver.bulkInsert(BATTERY_CONTENT_URI, valuesArray); 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); + isFullChargeStart);
} catch (Exception e) { } 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 { } else {
// Inserts one fake data into battery provider. // Inserts one fake data into battery provider.
final ContentValues contentValues = final ContentValues contentValues =
ConvertUtils.convertToContentValues( ConvertUtils.convertBatteryEntryToContentValues(
/*entry=*/ null, /*entry=*/ null,
/*batteryUsageStats=*/ null, /*batteryUsageStats=*/ null,
batteryLevel, batteryLevel,
@@ -231,6 +316,30 @@ public final class DatabaseUtils {
return valuesList; 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( private static Map<Long, Map<String, BatteryHistEntry>> loadHistoryMapFromContentProvider(
Context context, Uri batteryStateUri) { Context context, Uri batteryStateUri) {
final boolean isWorkProfileUser = isWorkProfile(context); final boolean isWorkProfileUser = isWorkProfile(context);
@@ -247,7 +356,7 @@ public final class DatabaseUtils {
} }
} }
final Map<Long, Map<String, BatteryHistEntry>> resultMap = new HashMap(); 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)) { context.getContentResolver().query(batteryStateUri, null, null, null)) {
if (cursor == null || cursor.getCount() == 0) { if (cursor == null || cursor.getCount() == 0) {
return resultMap; return resultMap;
@@ -286,17 +395,4 @@ public final class DatabaseUtils {
Log.w(TAG, "invoke clearMemory()"); Log.w(TAG, "invoke clearMemory()");
}, CLEAR_MEMORY_DELAYED_MS); }, 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; return;
} }
BatteryUsageDataLoader.enqueueWork(context, /*isFullChargeStart=*/ false); BatteryUsageDataLoader.enqueueWork(context, /*isFullChargeStart=*/ false);
AppUsageDataLoader.enqueueWork(context);
Log.d(TAG, "refresh periodic job from action=" + action); Log.d(TAG, "refresh periodic job from action=" + action);
PeriodicJobManager.getInstance(context).refreshJob(); PeriodicJobManager.getInstance(context).refreshJob();
DatabaseUtils.clearExpiredDataIfNeeded(context); 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; return this;
} }
/** Sets the consumer type. */ /** Sets the battery information. */
@CanIgnoreReturnValue @CanIgnoreReturnValue
public Builder setBatteryInformation(String batteryInformation) { public Builder setBatteryInformation(String batteryInformation) {
this.mBatteryInformation = batteryInformation; this.mBatteryInformation = batteryInformation;
return this; return this;
} }
/** Sets the consumer type. */ /** Sets the battery information debug string. */
@CanIgnoreReturnValue @CanIgnoreReturnValue
public Builder setBatteryInformationDebug(String batteryInformationDebug) { public Builder setBatteryInformationDebug(String batteryInformationDebug) {
this.mBatteryInformationDebug = batteryInformationDebug; this.mBatteryInformationDebug = batteryInformationDebug;

View File

@@ -25,7 +25,7 @@ import androidx.room.RoomDatabase;
/** A {@link RoomDatabase} for battery usage states history. */ /** A {@link RoomDatabase} for battery usage states history. */
@Database( @Database(
entities = {BatteryState.class}, entities = {BatteryState.class, AppUsageEventEntity.class},
version = 1) version = 1)
public abstract class BatteryStateDatabase extends RoomDatabase { public abstract class BatteryStateDatabase extends RoomDatabase {
private static final String TAG = "BatteryStateDatabase"; private static final String TAG = "BatteryStateDatabase";
@@ -34,13 +34,15 @@ public abstract class BatteryStateDatabase extends RoomDatabase {
/** Provides DAO for battery state table. */ /** Provides DAO for battery state table. */
public abstract BatteryStateDao batteryStateDao(); public abstract BatteryStateDao batteryStateDao();
/** Provides DAO for app usage event table. */
public abstract AppUsageEventDao appUsageEventDao();
/** Gets or creates an instance of {@link RoomDatabase}. */ /** Gets or creates an instance of {@link RoomDatabase}. */
public static BatteryStateDatabase getInstance(Context context) { public static BatteryStateDatabase getInstance(Context context) {
if (sBatteryStateDatabase == null) { if (sBatteryStateDatabase == null) {
sBatteryStateDatabase = sBatteryStateDatabase =
Room.databaseBuilder( 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. // Allows accessing data in the main thread for dumping bugreport.
.allowMainThreadQueries() .allowMainThreadQueries()
.fallbackToDestructiveMigration() .fallbackToDestructiveMigration()

View File

@@ -13,4 +13,12 @@ java_library {
type: "lite", type: "lite",
}, },
srcs: ["fuelgauge_usage_state.proto"], 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;
}

View File

@@ -0,0 +1,102 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.fuelgauge.batteryusage;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.os.UserManager;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@RunWith(RobolectricTestRunner.class)
public final class AppUsageDataLoaderTest {
private Context mContext;
@Mock
private ContentResolver mMockContentResolver;
@Mock
private UserManager mUserManager;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mContext = spy(RuntimeEnvironment.application);
doReturn(mContext).when(mContext).getApplicationContext();
doReturn(mMockContentResolver).when(mContext).getContentResolver();
doReturn(mUserManager).when(mContext).getSystemService(UserManager.class);
doReturn(new Intent()).when(mContext).registerReceiver(any(), any());
}
@Test
public void loadAppUsageData_withData_insertFakeDataIntoProvider() {
final List<AppUsageEvent> AppUsageEventList = new ArrayList<>();
final AppUsageEvent appUsageEvent = AppUsageEvent.newBuilder().setUid(0).build();
AppUsageEventList.add(appUsageEvent);
AppUsageDataLoader.sFakeAppUsageEventsSupplier = () -> new HashMap<>();
AppUsageDataLoader.sFakeUsageEventsListSupplier = () -> AppUsageEventList;
AppUsageDataLoader.loadAppUsageData(mContext);
verify(mMockContentResolver).bulkInsert(any(), any());
verify(mMockContentResolver).notifyChange(any(), any());
}
@Test
public void loadAppUsageData_nullAppUsageEvents_notInsertDataIntoProvider() {
AppUsageDataLoader.sFakeAppUsageEventsSupplier = () -> null;
AppUsageDataLoader.loadAppUsageData(mContext);
verifyNoMoreInteractions(mMockContentResolver);
}
@Test
public void loadAppUsageData_nullUsageEventsList_notInsertDataIntoProvider() {
AppUsageDataLoader.sFakeAppUsageEventsSupplier = () -> new HashMap<>();
AppUsageDataLoader.sFakeUsageEventsListSupplier = () -> null;
AppUsageDataLoader.loadAppUsageData(mContext);
verifyNoMoreInteractions(mMockContentResolver);
}
@Test
public void loadAppUsageData_emptyUsageEventsList_notInsertDataIntoProvider() {
AppUsageDataLoader.sFakeAppUsageEventsSupplier = () -> new HashMap<>();
AppUsageDataLoader.sFakeUsageEventsListSupplier = () -> new ArrayList<>();
AppUsageDataLoader.loadAppUsageData(mContext);
verifyNoMoreInteractions(mMockContentResolver);
}
}

View File

@@ -68,7 +68,7 @@ public final class BatteryHistEntryTest {
when(mMockBatteryEntry.getConsumerType()) when(mMockBatteryEntry.getConsumerType())
.thenReturn(ConvertUtils.CONSUMER_TYPE_SYSTEM_BATTERY); .thenReturn(ConvertUtils.CONSUMER_TYPE_SYSTEM_BATTERY);
final ContentValues values = final ContentValues values =
ConvertUtils.convertToContentValues( ConvertUtils.convertBatteryEntryToContentValues(
mMockBatteryEntry, mMockBatteryEntry,
mBatteryUsageStats, mBatteryUsageStats,
/*batteryLevel=*/ 12, /*batteryLevel=*/ 12,

View File

@@ -30,6 +30,7 @@ import android.net.Uri;
import androidx.test.core.app.ApplicationProvider; import androidx.test.core.app.ApplicationProvider;
import com.android.settings.fuelgauge.batteryusage.db.AppUsageEventEntity;
import com.android.settings.fuelgauge.batteryusage.db.BatteryState; import com.android.settings.fuelgauge.batteryusage.db.BatteryState;
import com.android.settings.fuelgauge.batteryusage.db.BatteryStateDatabase; import com.android.settings.fuelgauge.batteryusage.db.BatteryStateDatabase;
import com.android.settings.testutils.BatteryTestUtils; import com.android.settings.testutils.BatteryTestUtils;
@@ -52,6 +53,8 @@ public final class BatteryUsageContentProviderTest {
private static final String PACKAGE_NAME1 = "com.android.settings1"; private static final String PACKAGE_NAME1 = "com.android.settings1";
private static final String PACKAGE_NAME2 = "com.android.settings2"; private static final String PACKAGE_NAME2 = "com.android.settings2";
private static final String PACKAGE_NAME3 = "com.android.settings3"; private static final String PACKAGE_NAME3 = "com.android.settings3";
private static final long USER_ID1 = 1;
private static final long USER_ID2 = 2;
private Context mContext; private Context mContext;
private BatteryUsageContentProvider mProvider; private BatteryUsageContentProvider mProvider;
@@ -177,6 +180,37 @@ public final class BatteryUsageContentProviderTest {
BootBroadcastReceiver.ACTION_PERIODIC_JOB_RECHECK); BootBroadcastReceiver.ACTION_PERIODIC_JOB_RECHECK);
} }
@Test
public void query_appUsageTimestamp_returnsExpectedResult() throws Exception {
mProvider.onCreate();
final long timestamp1 = System.currentTimeMillis();
final long timestamp2 = timestamp1 + 2;
final long timestamp3 = timestamp1 + 4;
// Inserts some valid testing data.
BatteryTestUtils.insertDataToAppUsageEventTable(
mContext, USER_ID1, timestamp1, PACKAGE_NAME1);
BatteryTestUtils.insertDataToAppUsageEventTable(
mContext, USER_ID2, timestamp2, PACKAGE_NAME2);
BatteryTestUtils.insertDataToAppUsageEventTable(
mContext, USER_ID1, timestamp3, PACKAGE_NAME3);
final Cursor cursor1 = getCursorOfLatestTimestamp(USER_ID1);
assertThat(cursor1.getCount()).isEqualTo(1);
cursor1.moveToFirst();
assertThat(cursor1.getLong(0)).isEqualTo(timestamp3);
final Cursor cursor2 = getCursorOfLatestTimestamp(USER_ID2);
assertThat(cursor2.getCount()).isEqualTo(1);
cursor2.moveToFirst();
assertThat(cursor2.getLong(0)).isEqualTo(timestamp2);
final long notExistingUserId = 3;
final Cursor cursor3 = getCursorOfLatestTimestamp(notExistingUserId);
assertThat(cursor3.getCount()).isEqualTo(1);
cursor3.moveToFirst();
assertThat(cursor3.getLong(0)).isEqualTo(0);
}
@Test @Test
public void insert_batteryState_returnsExpectedResult() { public void insert_batteryState_returnsExpectedResult() {
mProvider.onCreate(); mProvider.onCreate();
@@ -266,6 +300,34 @@ public final class BatteryUsageContentProviderTest {
assertThat(states.get(0).batteryInformation).isEqualTo(expectedBatteryInformationString); assertThat(states.get(0).batteryInformation).isEqualTo(expectedBatteryInformationString);
} }
@Test
public void insert_appUsageEvent_returnsExpectedResult() {
mProvider.onCreate();
ContentValues values = new ContentValues();
values.put(AppUsageEventEntity.KEY_UID, 101L);
values.put(AppUsageEventEntity.KEY_USER_ID, 1001L);
values.put(AppUsageEventEntity.KEY_TIMESTAMP, 10001L);
values.put(AppUsageEventEntity.KEY_APP_USAGE_EVENT_TYPE, 1);
values.put(AppUsageEventEntity.KEY_PACKAGE_NAME, "com.android.settings1");
values.put(AppUsageEventEntity.KEY_INSTANCE_ID, 100001L);
values.put(AppUsageEventEntity.KEY_TASK_ROOT_PACKAGE_NAME, "com.android.settings2");
final Uri uri = mProvider.insert(DatabaseUtils.APP_USAGE_EVENT_URI, values);
assertThat(uri).isEqualTo(DatabaseUtils.APP_USAGE_EVENT_URI);
// Verifies the AppUsageEventEntity content.
final List<AppUsageEventEntity> entities =
BatteryStateDatabase.getInstance(mContext).appUsageEventDao().getAllAfter(0);
assertThat(entities).hasSize(1);
assertThat(entities.get(0).uid).isEqualTo(101L);
assertThat(entities.get(0).userId).isEqualTo(1001L);
assertThat(entities.get(0).timestamp).isEqualTo(10001L);
assertThat(entities.get(0).appUsageEventType).isEqualTo(1);
assertThat(entities.get(0).packageName).isEqualTo("com.android.settings1");
assertThat(entities.get(0).instanceId).isEqualTo(100001L);
assertThat(entities.get(0).taskRootPackageName).isEqualTo("com.android.settings2");
}
@Test @Test
public void delete_throwsUnsupportedOperationException() { public void delete_throwsUnsupportedOperationException() {
assertThrows( assertThrows(
@@ -293,12 +355,12 @@ public final class BatteryUsageContentProviderTest {
mProvider.setClock(fakeClock); mProvider.setClock(fakeClock);
final long currentTimestamp = currentTime.toMillis(); final long currentTimestamp = currentTime.toMillis();
// Inserts some valid testing data. // Inserts some valid testing data.
BatteryTestUtils.insertDataToBatteryStateDatabase( BatteryTestUtils.insertDataToBatteryStateTable(
mContext, currentTimestamp - 2, PACKAGE_NAME1, mContext, currentTimestamp - 2, PACKAGE_NAME1,
/*isFullChargeStart=*/ true); /*isFullChargeStart=*/ true);
BatteryTestUtils.insertDataToBatteryStateDatabase( BatteryTestUtils.insertDataToBatteryStateTable(
mContext, currentTimestamp - 1, PACKAGE_NAME2); mContext, currentTimestamp - 1, PACKAGE_NAME2);
BatteryTestUtils.insertDataToBatteryStateDatabase( BatteryTestUtils.insertDataToBatteryStateTable(
mContext, currentTimestamp, PACKAGE_NAME3); mContext, currentTimestamp, PACKAGE_NAME3);
final Uri batteryStateQueryContentUri = final Uri batteryStateQueryContentUri =
@@ -307,7 +369,7 @@ public final class BatteryUsageContentProviderTest {
.authority(DatabaseUtils.AUTHORITY) .authority(DatabaseUtils.AUTHORITY)
.appendPath(DatabaseUtils.BATTERY_STATE_TABLE) .appendPath(DatabaseUtils.BATTERY_STATE_TABLE)
.appendQueryParameter( .appendQueryParameter(
BatteryUsageContentProvider.QUERY_KEY_TIMESTAMP, queryTimestamp) DatabaseUtils.QUERY_KEY_TIMESTAMP, queryTimestamp)
.build(); .build();
final Cursor cursor = final Cursor cursor =
@@ -320,4 +382,22 @@ public final class BatteryUsageContentProviderTest {
return cursor; return cursor;
} }
private Cursor getCursorOfLatestTimestamp(final long userId) {
final Uri appUsageLatestTimestampQueryContentUri =
new Uri.Builder()
.scheme(ContentResolver.SCHEME_CONTENT)
.authority(DatabaseUtils.AUTHORITY)
.appendPath(DatabaseUtils.APP_USAGE_LATEST_TIMESTAMP_PATH)
.appendQueryParameter(
DatabaseUtils.QUERY_KEY_USERID, Long.toString(userId))
.build();
return mProvider.query(
appUsageLatestTimestampQueryContentUri,
/*strings=*/ null,
/*s=*/ null,
/*strings1=*/ null,
/*s1=*/ null);
}
} }

View File

@@ -62,7 +62,7 @@ public final class BootBroadcastReceiverTest {
// Inserts fake data into database for testing. // Inserts fake data into database for testing.
final BatteryStateDatabase database = BatteryTestUtils.setUpBatteryStateDatabase(mContext); final BatteryStateDatabase database = BatteryTestUtils.setUpBatteryStateDatabase(mContext);
BatteryTestUtils.insertDataToBatteryStateDatabase( BatteryTestUtils.insertDataToBatteryStateTable(
mContext, Clock.systemUTC().millis(), "com.android.systemui"); mContext, Clock.systemUTC().millis(), "com.android.systemui");
mDao = database.batteryStateDao(); mDao = database.batteryStateDao();
} }
@@ -170,9 +170,9 @@ public final class BootBroadcastReceiverTest {
private void insertExpiredData(int shiftDay) { private void insertExpiredData(int shiftDay) {
final long expiredTimeInMs = final long expiredTimeInMs =
Clock.systemUTC().millis() - Duration.ofDays(shiftDay).toMillis(); Clock.systemUTC().millis() - Duration.ofDays(shiftDay).toMillis();
BatteryTestUtils.insertDataToBatteryStateDatabase( BatteryTestUtils.insertDataToBatteryStateTable(
mContext, expiredTimeInMs - 1, "com.android.systemui"); mContext, expiredTimeInMs - 1, "com.android.systemui");
BatteryTestUtils.insertDataToBatteryStateDatabase( BatteryTestUtils.insertDataToBatteryStateTable(
mContext, expiredTimeInMs, "com.android.systemui"); mContext, expiredTimeInMs, "com.android.systemui");
// Ensures the testing environment is correct. // Ensures the testing environment is correct.
assertThat(mDao.getAllAfter(0)).hasSize(3); assertThat(mDao.getAllAfter(0)).hasSize(3);

View File

@@ -17,16 +17,23 @@ package com.android.settings.fuelgauge.batteryusage;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.spy; import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import android.app.usage.UsageEvents;
import android.app.usage.UsageEvents.Event;
import android.content.ContentValues; import android.content.ContentValues;
import android.content.Context; import android.content.Context;
import android.content.pm.PackageManager;
import android.os.BatteryManager; import android.os.BatteryManager;
import android.os.BatteryUsageStats; import android.os.BatteryUsageStats;
import android.os.LocaleList; import android.os.LocaleList;
import android.os.UserHandle; import android.os.UserHandle;
import com.android.settings.fuelgauge.batteryusage.db.AppUsageEventEntity;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
@@ -43,6 +50,8 @@ public final class ConvertUtilsTest {
private Context mContext; private Context mContext;
@Mock
private PackageManager mMockPackageManager;
@Mock @Mock
private BatteryUsageStats mBatteryUsageStats; private BatteryUsageStats mBatteryUsageStats;
@Mock @Mock
@@ -52,10 +61,11 @@ public final class ConvertUtilsTest {
public void setUp() { public void setUp() {
MockitoAnnotations.initMocks(this); MockitoAnnotations.initMocks(this);
mContext = spy(RuntimeEnvironment.application); mContext = spy(RuntimeEnvironment.application);
when(mContext.getPackageManager()).thenReturn(mMockPackageManager);
} }
@Test @Test
public void convertToContentValues_returnsExpectedContentValues() { public void convertBatteryEntryToContentValues_returnsExpectedContentValues() {
final int expectedType = 3; final int expectedType = 3;
when(mMockBatteryEntry.getUid()).thenReturn(1001); when(mMockBatteryEntry.getUid()).thenReturn(1001);
when(mMockBatteryEntry.getLabel()).thenReturn("Settings"); when(mMockBatteryEntry.getLabel()).thenReturn("Settings");
@@ -76,7 +86,7 @@ public final class ConvertUtilsTest {
.thenReturn(ConvertUtils.CONSUMER_TYPE_SYSTEM_BATTERY); .thenReturn(ConvertUtils.CONSUMER_TYPE_SYSTEM_BATTERY);
final ContentValues values = final ContentValues values =
ConvertUtils.convertToContentValues( ConvertUtils.convertBatteryEntryToContentValues(
mMockBatteryEntry, mMockBatteryEntry,
mBatteryUsageStats, mBatteryUsageStats,
/*batteryLevel=*/ 12, /*batteryLevel=*/ 12,
@@ -121,9 +131,9 @@ public final class ConvertUtilsTest {
} }
@Test @Test
public void convertToContentValues_nullBatteryEntry_returnsExpectedContentValues() { public void convertBatteryEntryToContentValues_nullBatteryEntry_returnsExpectedContentValues() {
final ContentValues values = final ContentValues values =
ConvertUtils.convertToContentValues( ConvertUtils.convertBatteryEntryToContentValues(
/*entry=*/ null, /*entry=*/ null,
/*batteryUsageStats=*/ null, /*batteryUsageStats=*/ null,
/*batteryLevel=*/ 12, /*batteryLevel=*/ 12,
@@ -151,6 +161,31 @@ public final class ConvertUtilsTest {
.isEqualTo(ConvertUtils.FAKE_PACKAGE_NAME); .isEqualTo(ConvertUtils.FAKE_PACKAGE_NAME);
} }
@Test
public void convertAppUsageEventToContentValues_returnsExpectedContentValues() {
final AppUsageEvent appUsageEvent =
AppUsageEvent.newBuilder()
.setUid(101L)
.setUserId(1001L)
.setTimestamp(10001L)
.setType(AppUsageEventType.ACTIVITY_RESUMED)
.setPackageName("com.android.settings1")
.setInstanceId(100001)
.setTaskRootPackageName("com.android.settings2")
.build();
final ContentValues values =
ConvertUtils.convertAppUsageEventToContentValues(appUsageEvent);
assertThat(values.getAsLong(AppUsageEventEntity.KEY_UID)).isEqualTo(101L);
assertThat(values.getAsLong(AppUsageEventEntity.KEY_USER_ID)).isEqualTo(1001L);
assertThat(values.getAsLong(AppUsageEventEntity.KEY_TIMESTAMP)).isEqualTo(10001L);
assertThat(values.getAsInteger(AppUsageEventEntity.KEY_APP_USAGE_EVENT_TYPE)).isEqualTo(1);
assertThat(values.getAsString(AppUsageEventEntity.KEY_PACKAGE_NAME))
.isEqualTo("com.android.settings1");
assertThat(values.getAsInteger(AppUsageEventEntity.KEY_INSTANCE_ID)).isEqualTo(100001);
assertThat(values.getAsString(AppUsageEventEntity.KEY_TASK_ROOT_PACKAGE_NAME))
.isEqualTo("com.android.settings2");
}
@Test @Test
public void convertToBatteryHistEntry_returnsExpectedResult() { public void convertToBatteryHistEntry_returnsExpectedResult() {
final int expectedType = 3; final int expectedType = 3;
@@ -229,6 +264,77 @@ public final class ConvertUtilsTest {
.isEqualTo(ConvertUtils.FAKE_PACKAGE_NAME); .isEqualTo(ConvertUtils.FAKE_PACKAGE_NAME);
} }
@Test
public void convertToAppUsageEvent_returnsExpectedResult()
throws PackageManager.NameNotFoundException {
final Event event = new Event();
event.mEventType = UsageEvents.Event.ACTIVITY_RESUMED;
event.mPackage = "com.android.settings1";
event.mTimeStamp = 101L;
event.mInstanceId = 100001;
event.mTaskRootPackage = "com.android.settings2";
when(mMockPackageManager.getPackageUidAsUser(any(), anyInt())).thenReturn(1001);
final long userId = 2;
final AppUsageEvent appUsageEvent = ConvertUtils.convertToAppUsageEvent(
mContext, event, userId);
assertThat(appUsageEvent.getTimestamp()).isEqualTo(101L);
assertThat(appUsageEvent.getType()).isEqualTo(AppUsageEventType.ACTIVITY_RESUMED);
assertThat(appUsageEvent.getPackageName()).isEqualTo("com.android.settings1");
assertThat(appUsageEvent.getInstanceId()).isEqualTo(100001);
assertThat(appUsageEvent.getTaskRootPackageName()).isEqualTo("com.android.settings2");
assertThat(appUsageEvent.getUid()).isEqualTo(1001L);
assertThat(appUsageEvent.getUserId()).isEqualTo(userId);
}
@Test
public void convertToAppUsageEvent_emptyInstanceIdAndRootName_returnsExpectedResult()
throws PackageManager.NameNotFoundException {
final Event event = new Event();
event.mEventType = UsageEvents.Event.DEVICE_SHUTDOWN;
event.mPackage = "com.android.settings1";
event.mTimeStamp = 101L;
when(mMockPackageManager.getPackageUidAsUser(any(), anyInt())).thenReturn(1001);
final long userId = 1;
final AppUsageEvent appUsageEvent = ConvertUtils.convertToAppUsageEvent(
mContext, event, userId);
assertThat(appUsageEvent.getTimestamp()).isEqualTo(101L);
assertThat(appUsageEvent.getType()).isEqualTo(AppUsageEventType.DEVICE_SHUTDOWN);
assertThat(appUsageEvent.getPackageName()).isEqualTo("com.android.settings1");
assertThat(appUsageEvent.getInstanceId()).isEqualTo(0);
assertThat(appUsageEvent.getTaskRootPackageName()).isEqualTo("");
assertThat(appUsageEvent.getUid()).isEqualTo(1001L);
assertThat(appUsageEvent.getUserId()).isEqualTo(userId);
}
@Test
public void convertToAppUsageEvent_emptyPackageName_returnsNull() {
final Event event = new Event();
event.mPackage = null;
final AppUsageEvent appUsageEvent = ConvertUtils.convertToAppUsageEvent(
mContext, event, /*userId=*/ 0);
assertThat(appUsageEvent).isNull();
}
@Test
public void convertToAppUsageEvent_failToGetUid_returnsNull()
throws PackageManager.NameNotFoundException {
final Event event = new Event();
event.mEventType = UsageEvents.Event.DEVICE_SHUTDOWN;
event.mPackage = "com.android.settings1";
when(mMockPackageManager.getPackageUidAsUser(any(), anyInt()))
.thenThrow(new PackageManager.NameNotFoundException());
final long userId = 1;
final AppUsageEvent appUsageEvent = ConvertUtils.convertToAppUsageEvent(
mContext, event, userId);
assertThat(appUsageEvent).isNull();
}
@Test @Test
public void getLocale_nullContext_returnDefaultLocale() { public void getLocale_nullContext_returnDefaultLocale() {
assertThat(ConvertUtils.getLocale(/*context=*/ null)) assertThat(ConvertUtils.getLocale(/*context=*/ null))

View File

@@ -18,6 +18,7 @@ package com.android.settings.fuelgauge.batteryusage;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.any; import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyInt; import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doReturn;
@@ -25,12 +26,19 @@ import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.spy; import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import android.app.usage.IUsageStatsManager;
import android.app.usage.UsageEvents;
import android.app.usage.UsageEvents.Event;
import android.content.ContentValues; import android.content.ContentValues;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.pm.UserInfo;
import android.os.BatteryConsumer; import android.os.BatteryConsumer;
import android.os.BatteryManager; import android.os.BatteryManager;
import android.os.BatteryUsageStats; import android.os.BatteryUsageStats;
import android.os.Parcel;
import android.os.RemoteException;
import android.os.UserManager;
import android.text.format.DateUtils; import android.text.format.DateUtils;
import com.android.settings.fuelgauge.BatteryUtils; import com.android.settings.fuelgauge.BatteryUtils;
@@ -65,10 +73,13 @@ public class DataProcessorTest {
@Mock private Intent mIntent; @Mock private Intent mIntent;
@Mock private BatteryUsageStats mBatteryUsageStats; @Mock private BatteryUsageStats mBatteryUsageStats;
@Mock private UserManager mUserManager;
@Mock private IUsageStatsManager mUsageStatsManager;
@Mock private BatteryEntry mMockBatteryEntry1; @Mock private BatteryEntry mMockBatteryEntry1;
@Mock private BatteryEntry mMockBatteryEntry2; @Mock private BatteryEntry mMockBatteryEntry2;
@Mock private BatteryEntry mMockBatteryEntry3; @Mock private BatteryEntry mMockBatteryEntry3;
@Mock private BatteryEntry mMockBatteryEntry4; @Mock private BatteryEntry mMockBatteryEntry4;
@Mock private UsageEvents mUsageEvents1;
@Before @Before
@@ -80,9 +91,11 @@ public class DataProcessorTest {
mFeatureFactory = FakeFeatureFactory.setupForTest(); mFeatureFactory = FakeFeatureFactory.setupForTest();
mPowerUsageFeatureProvider = mFeatureFactory.powerUsageFeatureProvider; mPowerUsageFeatureProvider = mFeatureFactory.powerUsageFeatureProvider;
DataProcessor.sUsageStatsManager = mUsageStatsManager;
doReturn(mIntent).when(mContext).registerReceiver(any(), any()); doReturn(mIntent).when(mContext).registerReceiver(any(), any());
doReturn(100).when(mIntent).getIntExtra(eq(BatteryManager.EXTRA_SCALE), anyInt()); doReturn(100).when(mIntent).getIntExtra(eq(BatteryManager.EXTRA_SCALE), anyInt());
doReturn(66).when(mIntent).getIntExtra(eq(BatteryManager.EXTRA_LEVEL), anyInt()); doReturn(66).when(mIntent).getIntExtra(eq(BatteryManager.EXTRA_LEVEL), anyInt());
doReturn(mUserManager).when(mContext).getSystemService(UserManager.class);
} }
@Test @Test
@@ -145,6 +158,86 @@ public class DataProcessorTest {
expectedHourlyLevels); expectedHourlyLevels);
} }
@Test
public void getAppUsageEvents_returnExpectedResult() throws RemoteException {
UserInfo userInfo = new UserInfo(/*id=*/ 0, "user_0", /*flags=*/ 0);
final List<UserInfo> userInfoList = new ArrayList<>();
userInfoList.add(userInfo);
doReturn(userInfoList).when(mUserManager).getAliveUsers();
doReturn(true).when(mUserManager).isUserUnlocked(userInfo.id);
doReturn(mUsageEvents1)
.when(mUsageStatsManager)
.queryEventsForUser(anyLong(), anyLong(), anyInt(), any());
final Map<Long, UsageEvents> resultMap = DataProcessor.getAppUsageEvents(mContext);
assertThat(resultMap.size()).isEqualTo(1);
assertThat(resultMap.get(Long.valueOf(userInfo.id))).isEqualTo(mUsageEvents1);
}
@Test
public void getAppUsageEvents_lockedUser_returnNull() throws RemoteException {
UserInfo userInfo = new UserInfo(/*id=*/ 0, "user_0", /*flags=*/ 0);
final List<UserInfo> userInfoList = new ArrayList<>();
userInfoList.add(userInfo);
doReturn(userInfoList).when(mUserManager).getAliveUsers();
// Test locked user.
doReturn(false).when(mUserManager).isUserUnlocked(userInfo.id);
final Map<Long, UsageEvents> resultMap = DataProcessor.getAppUsageEvents(mContext);
assertThat(resultMap).isNull();
}
@Test
public void getAppUsageEvents_nullUsageEvents_returnNull() throws RemoteException {
UserInfo userInfo = new UserInfo(/*id=*/ 0, "user_0", /*flags=*/ 0);
final List<UserInfo> userInfoList = new ArrayList<>();
userInfoList.add(userInfo);
doReturn(userInfoList).when(mUserManager).getAliveUsers();
doReturn(true).when(mUserManager).isUserUnlocked(userInfo.id);
doReturn(null)
.when(mUsageStatsManager).queryEventsForUser(anyLong(), anyLong(), anyInt(), any());
final Map<Long, UsageEvents> resultMap = DataProcessor.getAppUsageEvents(mContext);
assertThat(resultMap).isNull();
}
@Test public void generateAppUsageEventListFromUsageEvents_returnExpectedResult() {
Event event1 = getUsageEvent(Event.NOTIFICATION_INTERRUPTION, /*timestamp=*/ 1);
Event event2 = getUsageEvent(Event.ACTIVITY_RESUMED, /*timestamp=*/ 2);
Event event3 = getUsageEvent(Event.ACTIVITY_STOPPED, /*timestamp=*/ 3);
Event event4 = getUsageEvent(Event.DEVICE_SHUTDOWN, /*timestamp=*/ 4);
Event event5 = getUsageEvent(Event.ACTIVITY_RESUMED, /*timestamp=*/ 5);
event5.mPackage = null;
List<Event> events1 = new ArrayList<>();
events1.add(event1);
events1.add(event2);
List<Event> events2 = new ArrayList<>();
events2.add(event3);
events2.add(event4);
events2.add(event5);
final long userId1 = 101L;
final long userId2 = 102L;
final long userId3 = 103L;
final Map<Long, UsageEvents> appUsageEvents = new HashMap();
appUsageEvents.put(userId1, getUsageEvents(events1));
appUsageEvents.put(userId2, getUsageEvents(events2));
appUsageEvents.put(userId3, getUsageEvents(new ArrayList<>()));
final List<AppUsageEvent> appUsageEventList =
DataProcessor.generateAppUsageEventListFromUsageEvents(mContext, appUsageEvents);
assertThat(appUsageEventList.size()).isEqualTo(3);
assetAppUsageEvent(
appUsageEventList.get(0), AppUsageEventType.ACTIVITY_RESUMED, /*timestamp=*/ 2);
assetAppUsageEvent(
appUsageEventList.get(1), AppUsageEventType.ACTIVITY_STOPPED, /*timestamp=*/ 3);
assetAppUsageEvent(
appUsageEventList.get(2), AppUsageEventType.DEVICE_SHUTDOWN, /*timestamp=*/ 4);
}
@Test @Test
public void getHistoryMapWithExpectedTimestamps_emptyHistoryMap_returnEmptyMap() { public void getHistoryMapWithExpectedTimestamps_emptyHistoryMap_returnEmptyMap() {
assertThat(DataProcessor assertThat(DataProcessor
@@ -1213,6 +1306,30 @@ public class DataProcessorTest {
return new BatteryHistEntry(values); return new BatteryHistEntry(values);
} }
private UsageEvents getUsageEvents(final List<Event> events) {
UsageEvents usageEvents = new UsageEvents(events, new String[] {"package"});
Parcel parcel = Parcel.obtain();
parcel.setDataPosition(0);
usageEvents.writeToParcel(parcel, 0);
parcel.setDataPosition(0);
return UsageEvents.CREATOR.createFromParcel(parcel);
}
private Event getUsageEvent(
final int eventType, final long timestamp) {
final Event event = new Event();
event.mEventType = eventType;
event.mPackage = "package";
event.mTimeStamp = timestamp;
return event;
}
private void assetAppUsageEvent(
final AppUsageEvent event, final AppUsageEventType eventType, final long timestamp) {
assertThat(event.getType()).isEqualTo(eventType);
assertThat(event.getTimestamp()).isEqualTo(timestamp);
}
private static void verifyExpectedBatteryLevelData( private static void verifyExpectedBatteryLevelData(
final BatteryLevelData resultData, final BatteryLevelData resultData,
final List<Long> expectedDailyTimestamps, final List<Long> expectedDailyTimestamps,

View File

@@ -22,6 +22,7 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.spy; import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import android.content.ContentResolver; import android.content.ContentResolver;
import android.content.ContentValues; import android.content.ContentValues;
@@ -34,6 +35,7 @@ import android.os.BatteryUsageStats;
import android.os.UserHandle; import android.os.UserHandle;
import android.os.UserManager; import android.os.UserManager;
import com.android.settings.fuelgauge.batteryusage.db.AppUsageEventEntity;
import com.android.settings.testutils.BatteryTestUtils; import com.android.settings.testutils.BatteryTestUtils;
import org.junit.Before; import org.junit.Before;
@@ -93,6 +95,53 @@ public final class DatabaseUtilsTest {
assertThat(DatabaseUtils.isWorkProfile(mContext)).isFalse(); assertThat(DatabaseUtils.isWorkProfile(mContext)).isFalse();
} }
@Test
public void sendAppUsageEventData_returnsExpectedList() {
// Configures the testing AppUsageEvent data.
final List<AppUsageEvent> appUsageEventList = new ArrayList<>();
final AppUsageEvent appUsageEvent1 =
AppUsageEvent.newBuilder()
.setUid(101L)
.setType(AppUsageEventType.ACTIVITY_RESUMED)
.build();
final AppUsageEvent appUsageEvent2 =
AppUsageEvent.newBuilder()
.setUid(1001L)
.setType(AppUsageEventType.ACTIVITY_STOPPED)
.build();
final AppUsageEvent appUsageEvent3 =
AppUsageEvent.newBuilder()
.setType(AppUsageEventType.DEVICE_SHUTDOWN)
.build();
appUsageEventList.add(appUsageEvent1);
appUsageEventList.add(appUsageEvent2);
appUsageEventList.add(appUsageEvent3);
final List<ContentValues> valuesList =
DatabaseUtils.sendAppUsageEventData(mContext, appUsageEventList);
assertThat(valuesList).hasSize(2);
assertThat(valuesList.get(0).getAsInteger(AppUsageEventEntity.KEY_APP_USAGE_EVENT_TYPE))
.isEqualTo(1);
assertThat(valuesList.get(1).getAsInteger(AppUsageEventEntity.KEY_APP_USAGE_EVENT_TYPE))
.isEqualTo(2);
// Verifies the inserted ContentValues into content provider.
final ContentValues[] valuesArray =
new ContentValues[] {valuesList.get(0), valuesList.get(1)};
verify(mMockContentResolver).bulkInsert(
DatabaseUtils.APP_USAGE_EVENT_URI, valuesArray);
verify(mMockContentResolver).notifyChange(
DatabaseUtils.APP_USAGE_EVENT_URI, /*observer=*/ null);
}
@Test
public void sendAppUsageEventData_emptyAppUsageEventList_notSend() {
final List<ContentValues> valuesList =
DatabaseUtils.sendAppUsageEventData(mContext, new ArrayList<>());
assertThat(valuesList).hasSize(0);
verifyNoMoreInteractions(mMockContentResolver);
}
@Test @Test
public void sendBatteryEntryData_nullBatteryIntent_returnsNullValue() { public void sendBatteryEntryData_nullBatteryIntent_returnsNullValue() {
doReturn(null).when(mContext).registerReceiver(any(), any()); doReturn(null).when(mContext).registerReceiver(any(), any());
@@ -123,8 +172,8 @@ public final class DatabaseUtilsTest {
assertThat(valuesList).hasSize(2); assertThat(valuesList).hasSize(2);
// Verifies the ContentValues content. // Verifies the ContentValues content.
verifyContentValues(0.5, valuesList.get(0)); verifyBatteryEntryContentValues(0.5, valuesList.get(0));
verifyContentValues(0.0, valuesList.get(1)); verifyBatteryEntryContentValues(0.0, valuesList.get(1));
// Verifies the inserted ContentValues into content provider. // Verifies the inserted ContentValues into content provider.
final ContentValues[] valuesArray = final ContentValues[] valuesArray =
new ContentValues[] {valuesList.get(0), valuesList.get(1)}; new ContentValues[] {valuesList.get(0), valuesList.get(1)};
@@ -146,7 +195,7 @@ public final class DatabaseUtilsTest {
/*isFullChargeStart=*/ false); /*isFullChargeStart=*/ false);
assertThat(valuesList).hasSize(1); assertThat(valuesList).hasSize(1);
verifyFakeContentValues(valuesList.get(0)); verifyFakeBatteryEntryContentValues(valuesList.get(0));
// Verifies the inserted ContentValues into content provider. // Verifies the inserted ContentValues into content provider.
verify(mMockContentResolver).insert(any(), any()); verify(mMockContentResolver).insert(any(), any());
verify(mMockContentResolver).notifyChange( verify(mMockContentResolver).notifyChange(
@@ -165,7 +214,7 @@ public final class DatabaseUtilsTest {
/*isFullChargeStart=*/ false); /*isFullChargeStart=*/ false);
assertThat(valuesList).hasSize(1); assertThat(valuesList).hasSize(1);
verifyFakeContentValues(valuesList.get(0)); verifyFakeBatteryEntryContentValues(valuesList.get(0));
// Verifies the inserted ContentValues into content provider. // Verifies the inserted ContentValues into content provider.
verify(mMockContentResolver).insert(any(), any()); verify(mMockContentResolver).insert(any(), any());
verify(mMockContentResolver).notifyChange( verify(mMockContentResolver).notifyChange(
@@ -184,13 +233,49 @@ public final class DatabaseUtilsTest {
/*isFullChargeStart=*/ false); /*isFullChargeStart=*/ false);
assertThat(valuesList).hasSize(1); assertThat(valuesList).hasSize(1);
verifyFakeContentValues(valuesList.get(0)); verifyFakeBatteryEntryContentValues(valuesList.get(0));
// Verifies the inserted ContentValues into content provider. // Verifies the inserted ContentValues into content provider.
verify(mMockContentResolver).insert(any(), any()); verify(mMockContentResolver).insert(any(), any());
verify(mMockContentResolver).notifyChange( verify(mMockContentResolver).notifyChange(
DatabaseUtils.BATTERY_CONTENT_URI, /*observer=*/ null); DatabaseUtils.BATTERY_CONTENT_URI, /*observer=*/ null);
} }
@Test
public void getAppUsageStartTimestampOfUser_emptyCursorContent_returnEarliestTimestamp() {
final MatrixCursor cursor =
new MatrixCursor(new String[] {AppUsageEventEntity.KEY_TIMESTAMP});
DatabaseUtils.sFakeAppUsageLatestTimestampSupplier = () -> cursor;
final long earliestTimestamp = 10001L;
assertThat(DatabaseUtils.getAppUsageStartTimestampOfUser(
mContext, /*userId=*/ 0, earliestTimestamp)).isEqualTo(earliestTimestamp);
}
@Test
public void getAppUsageStartTimestampOfUser_nullCursor_returnEarliestTimestamp() {
DatabaseUtils.sFakeAppUsageLatestTimestampSupplier = () -> null;
final long earliestTimestamp = 10001L;
assertThat(DatabaseUtils.getAppUsageStartTimestampOfUser(
mContext, /*userId=*/ 0, earliestTimestamp)).isEqualTo(earliestTimestamp);
}
@Test
public void getAppUsageStartTimestampOfUser_returnExpectedResult() {
final long returnedTimestamp = 10001L;
final MatrixCursor cursor =
new MatrixCursor(new String[] {AppUsageEventEntity.KEY_TIMESTAMP});
// Adds fake data into the cursor.
cursor.addRow(new Object[] {returnedTimestamp});
DatabaseUtils.sFakeAppUsageLatestTimestampSupplier = () -> cursor;
final long earliestTimestamp1 = 1001L;
assertThat(DatabaseUtils.getAppUsageStartTimestampOfUser(
mContext, /*userId=*/ 0, earliestTimestamp1)).isEqualTo(returnedTimestamp);
final long earliestTimestamp2 = 100001L;
assertThat(DatabaseUtils.getAppUsageStartTimestampOfUser(
mContext, /*userId=*/ 0, earliestTimestamp2)).isEqualTo(earliestTimestamp2);
}
@Test @Test
public void getHistoryMapSinceLastFullCharge_emptyCursorContent_returnEmptyMap() { public void getHistoryMapSinceLastFullCharge_emptyCursorContent_returnEmptyMap() {
final MatrixCursor cursor = new MatrixCursor( final MatrixCursor cursor = new MatrixCursor(
@@ -198,7 +283,7 @@ public final class DatabaseUtilsTest {
BatteryHistEntry.KEY_UID, BatteryHistEntry.KEY_UID,
BatteryHistEntry.KEY_USER_ID, BatteryHistEntry.KEY_USER_ID,
BatteryHistEntry.KEY_TIMESTAMP}); BatteryHistEntry.KEY_TIMESTAMP});
doReturn(cursor).when(mMockContentResolver).query(any(), any(), any(), any()); DatabaseUtils.sFakeBatteryStateSupplier = () -> cursor;
assertThat(DatabaseUtils.getHistoryMapSinceLastFullCharge( assertThat(DatabaseUtils.getHistoryMapSinceLastFullCharge(
mContext, /*calendar=*/ null)).isEmpty(); mContext, /*calendar=*/ null)).isEmpty();
@@ -206,7 +291,7 @@ public final class DatabaseUtilsTest {
@Test @Test
public void getHistoryMapSinceLastFullCharge_nullCursor_returnEmptyMap() { public void getHistoryMapSinceLastFullCharge_nullCursor_returnEmptyMap() {
doReturn(null).when(mMockContentResolver).query(any(), any(), any(), any()); DatabaseUtils.sFakeBatteryStateSupplier = () -> null;
assertThat(DatabaseUtils.getHistoryMapSinceLastFullCharge( assertThat(DatabaseUtils.getHistoryMapSinceLastFullCharge(
mContext, /*calendar=*/ null)).isEmpty(); mContext, /*calendar=*/ null)).isEmpty();
} }
@@ -216,7 +301,6 @@ public final class DatabaseUtilsTest {
final Long timestamp1 = Long.valueOf(1001L); final Long timestamp1 = Long.valueOf(1001L);
final Long timestamp2 = Long.valueOf(1002L); final Long timestamp2 = Long.valueOf(1002L);
final MatrixCursor cursor = getMatrixCursor(); final MatrixCursor cursor = getMatrixCursor();
doReturn(cursor).when(mMockContentResolver).query(any(), any(), any(), any());
// Adds fake data into the cursor. // Adds fake data into the cursor.
cursor.addRow(new Object[] { cursor.addRow(new Object[] {
"app name1", timestamp1, 1, ConvertUtils.CONSUMER_TYPE_UID_BATTERY}); "app name1", timestamp1, 1, ConvertUtils.CONSUMER_TYPE_UID_BATTERY});
@@ -226,6 +310,7 @@ public final class DatabaseUtilsTest {
"app name3", timestamp2, 3, ConvertUtils.CONSUMER_TYPE_UID_BATTERY}); "app name3", timestamp2, 3, ConvertUtils.CONSUMER_TYPE_UID_BATTERY});
cursor.addRow(new Object[] { cursor.addRow(new Object[] {
"app name4", timestamp2, 4, ConvertUtils.CONSUMER_TYPE_UID_BATTERY}); "app name4", timestamp2, 4, ConvertUtils.CONSUMER_TYPE_UID_BATTERY});
DatabaseUtils.sFakeBatteryStateSupplier = () -> cursor;
final Map<Long, Map<String, BatteryHistEntry>> batteryHistMap = final Map<Long, Map<String, BatteryHistEntry>> batteryHistMap =
DatabaseUtils.getHistoryMapSinceLastFullCharge( DatabaseUtils.getHistoryMapSinceLastFullCharge(
@@ -251,9 +336,7 @@ public final class DatabaseUtilsTest {
doReturn(mMockContext).when(mContext).createPackageContextAsUser( doReturn(mMockContext).when(mContext).createPackageContextAsUser(
"com.fake.package", /*flags=*/ 0, UserHandle.OWNER); "com.fake.package", /*flags=*/ 0, UserHandle.OWNER);
BatteryTestUtils.setWorkProfile(mContext); BatteryTestUtils.setWorkProfile(mContext);
doReturn(getMatrixCursor()).when(mMockContentResolver2) DatabaseUtils.sFakeBatteryStateSupplier = () -> getMatrixCursor();
.query(any(), any(), any(), any());
doReturn(null).when(mMockContentResolver).query(any(), any(), any(), any());
final Map<Long, Map<String, BatteryHistEntry>> batteryHistMap = final Map<Long, Map<String, BatteryHistEntry>> batteryHistMap =
DatabaseUtils.getHistoryMapSinceLastFullCharge( DatabaseUtils.getHistoryMapSinceLastFullCharge(
@@ -262,7 +345,8 @@ public final class DatabaseUtilsTest {
assertThat(batteryHistMap).isEmpty(); assertThat(batteryHistMap).isEmpty();
} }
private static void verifyContentValues(double consumedPower, ContentValues values) { private static void verifyBatteryEntryContentValues(
double consumedPower, ContentValues values) {
final BatteryInformation batteryInformation = final BatteryInformation batteryInformation =
ConvertUtils.getBatteryInformation( ConvertUtils.getBatteryInformation(
values, BatteryHistEntry.KEY_BATTERY_INFORMATION); values, BatteryHistEntry.KEY_BATTERY_INFORMATION);
@@ -275,7 +359,7 @@ public final class DatabaseUtilsTest {
.isEqualTo(BatteryManager.BATTERY_HEALTH_COLD); .isEqualTo(BatteryManager.BATTERY_HEALTH_COLD);
} }
private static void verifyFakeContentValues(ContentValues values) { private static void verifyFakeBatteryEntryContentValues(ContentValues values) {
final BatteryInformation batteryInformation = final BatteryInformation batteryInformation =
ConvertUtils.getBatteryInformation( ConvertUtils.getBatteryInformation(
values, BatteryHistEntry.KEY_BATTERY_INFORMATION); values, BatteryHistEntry.KEY_BATTERY_INFORMATION);

View File

@@ -62,7 +62,7 @@ public final class PeriodicJobReceiverTest {
// Inserts fake data into database for testing. // Inserts fake data into database for testing.
final BatteryStateDatabase database = BatteryTestUtils.setUpBatteryStateDatabase(mContext); final BatteryStateDatabase database = BatteryTestUtils.setUpBatteryStateDatabase(mContext);
BatteryTestUtils.insertDataToBatteryStateDatabase( BatteryTestUtils.insertDataToBatteryStateTable(
mContext, Clock.systemUTC().millis(), "com.android.systemui"); mContext, Clock.systemUTC().millis(), "com.android.systemui");
mDao = database.batteryStateDao(); mDao = database.batteryStateDao();
} }
@@ -122,9 +122,9 @@ public final class PeriodicJobReceiverTest {
private void insertExpiredData(int shiftDay) { private void insertExpiredData(int shiftDay) {
final long expiredTimeInMs = final long expiredTimeInMs =
Clock.systemUTC().millis() - Duration.ofDays(shiftDay).toMillis(); Clock.systemUTC().millis() - Duration.ofDays(shiftDay).toMillis();
BatteryTestUtils.insertDataToBatteryStateDatabase( BatteryTestUtils.insertDataToBatteryStateTable(
mContext, expiredTimeInMs - 1, "com.android.systemui"); mContext, expiredTimeInMs - 1, "com.android.systemui");
BatteryTestUtils.insertDataToBatteryStateDatabase( BatteryTestUtils.insertDataToBatteryStateTable(
mContext, expiredTimeInMs, "com.android.systemui"); mContext, expiredTimeInMs, "com.android.systemui");
// Ensures the testing environment is correct. // Ensures the testing environment is correct.
assertThat(mDao.getAllAfter(0)).hasSize(3); assertThat(mDao.getAllAfter(0)).hasSize(3);

View File

@@ -53,9 +53,9 @@ public final class BugReportContentProviderTest {
mBugReportContentProvider.attachInfo(mContext, /*info=*/ null); mBugReportContentProvider.attachInfo(mContext, /*info=*/ null);
// Inserts fake data into database for testing. // Inserts fake data into database for testing.
BatteryTestUtils.setUpBatteryStateDatabase(mContext); BatteryTestUtils.setUpBatteryStateDatabase(mContext);
BatteryTestUtils.insertDataToBatteryStateDatabase( BatteryTestUtils.insertDataToBatteryStateTable(
mContext, System.currentTimeMillis(), PACKAGE_NAME1); mContext, System.currentTimeMillis(), PACKAGE_NAME1);
BatteryTestUtils.insertDataToBatteryStateDatabase( BatteryTestUtils.insertDataToBatteryStateTable(
mContext, System.currentTimeMillis(), PACKAGE_NAME2); mContext, System.currentTimeMillis(), PACKAGE_NAME2);
} }

View File

@@ -0,0 +1,127 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.fuelgauge.batteryusage.db;
import static com.google.common.truth.Truth.assertThat;
import android.content.Context;
import android.database.Cursor;
import androidx.test.core.app.ApplicationProvider;
import com.android.settings.testutils.BatteryTestUtils;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import java.util.List;
/** Tests for {@link AppUsageEventDao}. */
@RunWith(RobolectricTestRunner.class)
public final class AppUsageEventDaoTest {
private static final long TIMESTAMP1 = System.currentTimeMillis();
private static final long TIMESTAMP2 = System.currentTimeMillis() + 2;
private static final long TIMESTAMP3 = System.currentTimeMillis() + 4;
private static final long USER_ID1 = 1;
private static final long USER_ID2 = 2;
private static final String PACKAGE_NAME1 = "com.android.apps.settings";
private static final String PACKAGE_NAME2 = "com.android.apps.calendar";
private static final String PACKAGE_NAME3 = "com.android.apps.gmail";
private Context mContext;
private BatteryStateDatabase mDatabase;
private AppUsageEventDao mAppUsageEventDao;
@Before
public void setUp() {
mContext = ApplicationProvider.getApplicationContext();
mDatabase = BatteryTestUtils.setUpBatteryStateDatabase(mContext);
mAppUsageEventDao = mDatabase.appUsageEventDao();
BatteryTestUtils.insertDataToAppUsageEventTable(
mContext, USER_ID1, TIMESTAMP3, PACKAGE_NAME3);
BatteryTestUtils.insertDataToAppUsageEventTable(
mContext, USER_ID2, TIMESTAMP2, PACKAGE_NAME2);
BatteryTestUtils.insertDataToAppUsageEventTable(
mContext, USER_ID1, TIMESTAMP1, PACKAGE_NAME1, /*multiple=*/ true);
}
@After
public void closeDb() {
mDatabase.close();
BatteryStateDatabase.setBatteryStateDatabase(/*database=*/ null);
}
@Test
public void appUsageEventDao_insertAll() throws Exception {
final List<AppUsageEventEntity> entities = mAppUsageEventDao.getAllAfter(TIMESTAMP1);
assertThat(entities).hasSize(2);
// Verifies the queried battery states.
assertAppUsageEvent(entities.get(0), TIMESTAMP3, PACKAGE_NAME3);
assertAppUsageEvent(entities.get(1), TIMESTAMP2, PACKAGE_NAME2);
}
@Test
public void appUsageEventDao_getLatestTimestampOfUser() throws Exception {
final Cursor cursor1 = mAppUsageEventDao.getLatestTimestampOfUser(USER_ID1);
assertThat(cursor1.getCount()).isEqualTo(1);
cursor1.moveToFirst();
assertThat(cursor1.getLong(0)).isEqualTo(TIMESTAMP3);
final Cursor cursor2 = mAppUsageEventDao.getLatestTimestampOfUser(USER_ID2);
assertThat(cursor2.getCount()).isEqualTo(1);
cursor2.moveToFirst();
assertThat(cursor2.getLong(0)).isEqualTo(TIMESTAMP2);
final long notExistingUserId = 3;
final Cursor cursor3 = mAppUsageEventDao.getLatestTimestampOfUser(notExistingUserId);
assertThat(cursor3.getCount()).isEqualTo(1);
cursor3.moveToFirst();
assertThat(cursor3.getLong(0)).isEqualTo(0);
}
@Test
public void appUsageEventDao_clearAllBefore() throws Exception {
mAppUsageEventDao.clearAllBefore(TIMESTAMP2);
final List<AppUsageEventEntity> entities = mAppUsageEventDao.getAllAfter(0);
assertThat(entities).hasSize(1);
// Verifies the queried battery state.
assertAppUsageEvent(entities.get(0), TIMESTAMP3, PACKAGE_NAME3);
}
@Test
public void appUsageEventDao_clearAll() throws Exception {
assertThat(mAppUsageEventDao.getAllAfter(0)).hasSize(3);
mAppUsageEventDao.clearAll();
assertThat(mAppUsageEventDao.getAllAfter(0)).isEmpty();
}
@Test
public void getInstance_createNewInstance() throws Exception {
BatteryStateDatabase.setBatteryStateDatabase(/*database=*/ null);
assertThat(BatteryStateDatabase.getInstance(mContext)).isNotNull();
}
private static void assertAppUsageEvent(
AppUsageEventEntity entity, long timestamp, String packageName) {
assertThat(entity.timestamp).isEqualTo(timestamp);
assertThat(entity.packageName).isEqualTo(packageName);
}
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.fuelgauge.batteryusage.db;
import static com.google.common.truth.Truth.assertThat;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
/** Tests for {@link AppUsageEventEntity}. */
@RunWith(RobolectricTestRunner.class)
public final class AppUsageEventEntityTest {
@Test
public void testBuilder_returnsExpectedResult() {
final long uid = 101L;
final long userId = 1001L;
final long timestamp = 10001L;
final int appUsageEventType = 1;
final String packageName = "com.android.settings1";
final int instanceId = 100001;
final String taskRootPackageName = "com.android.settings2";
AppUsageEventEntity entity = AppUsageEventEntity
.newBuilder()
.setUid(uid)
.setUserId(userId)
.setTimestamp(timestamp)
.setAppUsageEventType(appUsageEventType)
.setPackageName(packageName)
.setInstanceId(instanceId)
.setTaskRootPackageName(taskRootPackageName)
.build();
// Verifies the app relative information.
assertThat(entity.uid).isEqualTo(uid);
assertThat(entity.userId).isEqualTo(userId);
assertThat(entity.timestamp).isEqualTo(timestamp);
assertThat(entity.appUsageEventType).isEqualTo(appUsageEventType);
assertThat(entity.packageName).isEqualTo(packageName);
assertThat(entity.instanceId).isEqualTo(instanceId);
assertThat(entity.taskRootPackageName).isEqualTo(taskRootPackageName);
}
}

View File

@@ -53,9 +53,9 @@ public final class BatteryStateDaoTest {
mContext = ApplicationProvider.getApplicationContext(); mContext = ApplicationProvider.getApplicationContext();
mDatabase = BatteryTestUtils.setUpBatteryStateDatabase(mContext); mDatabase = BatteryTestUtils.setUpBatteryStateDatabase(mContext);
mBatteryStateDao = mDatabase.batteryStateDao(); mBatteryStateDao = mDatabase.batteryStateDao();
BatteryTestUtils.insertDataToBatteryStateDatabase(mContext, TIMESTAMP3, PACKAGE_NAME3); BatteryTestUtils.insertDataToBatteryStateTable(mContext, TIMESTAMP3, PACKAGE_NAME3);
BatteryTestUtils.insertDataToBatteryStateDatabase(mContext, TIMESTAMP2, PACKAGE_NAME2); BatteryTestUtils.insertDataToBatteryStateTable(mContext, TIMESTAMP2, PACKAGE_NAME2);
BatteryTestUtils.insertDataToBatteryStateDatabase( BatteryTestUtils.insertDataToBatteryStateTable(
mContext, TIMESTAMP1, PACKAGE_NAME1, /*multiple=*/ true, mContext, TIMESTAMP1, PACKAGE_NAME1, /*multiple=*/ true,
/*isFullChargeStart=*/ true); /*isFullChargeStart=*/ true);
} }
@@ -102,9 +102,9 @@ public final class BatteryStateDaoTest {
public void batteryStateDao_getCursorSinceLastFullCharge_noFullChargeData_returnSevenDaysData() public void batteryStateDao_getCursorSinceLastFullCharge_noFullChargeData_returnSevenDaysData()
throws Exception { throws Exception {
mBatteryStateDao.clearAll(); mBatteryStateDao.clearAll();
BatteryTestUtils.insertDataToBatteryStateDatabase(mContext, TIMESTAMP3, PACKAGE_NAME3); BatteryTestUtils.insertDataToBatteryStateTable(mContext, TIMESTAMP3, PACKAGE_NAME3);
BatteryTestUtils.insertDataToBatteryStateDatabase(mContext, TIMESTAMP2, PACKAGE_NAME2); BatteryTestUtils.insertDataToBatteryStateTable(mContext, TIMESTAMP2, PACKAGE_NAME2);
BatteryTestUtils.insertDataToBatteryStateDatabase(mContext, TIMESTAMP1, PACKAGE_NAME1); BatteryTestUtils.insertDataToBatteryStateTable(mContext, TIMESTAMP1, PACKAGE_NAME1);
final Cursor cursor = mBatteryStateDao.getCursorSinceLastFullCharge(TIMESTAMP2); final Cursor cursor = mBatteryStateDao.getCursorSinceLastFullCharge(TIMESTAMP2);
assertThat(cursor.getCount()).isEqualTo(2); assertThat(cursor.getCount()).isEqualTo(2);
assertThat(cursor.getColumnCount()).isEqualTo(CURSOR_COLUMN_SIZE); assertThat(cursor.getColumnCount()).isEqualTo(CURSOR_COLUMN_SIZE);

View File

@@ -26,6 +26,8 @@ import androidx.room.Room;
import com.android.settings.fuelgauge.batteryusage.BatteryInformation; import com.android.settings.fuelgauge.batteryusage.BatteryInformation;
import com.android.settings.fuelgauge.batteryusage.ConvertUtils; import com.android.settings.fuelgauge.batteryusage.ConvertUtils;
import com.android.settings.fuelgauge.batteryusage.DeviceBatteryState; import com.android.settings.fuelgauge.batteryusage.DeviceBatteryState;
import com.android.settings.fuelgauge.batteryusage.db.AppUsageEventDao;
import com.android.settings.fuelgauge.batteryusage.db.AppUsageEventEntity;
import com.android.settings.fuelgauge.batteryusage.db.BatteryState; import com.android.settings.fuelgauge.batteryusage.db.BatteryState;
import com.android.settings.fuelgauge.batteryusage.db.BatteryStateDao; import com.android.settings.fuelgauge.batteryusage.db.BatteryStateDao;
import com.android.settings.fuelgauge.batteryusage.db.BatteryStateDatabase; import com.android.settings.fuelgauge.batteryusage.db.BatteryStateDatabase;
@@ -70,21 +72,21 @@ public class BatteryTestUtils {
} }
/** Inserts a fake data into the database for testing. */ /** Inserts a fake data into the database for testing. */
public static void insertDataToBatteryStateDatabase( public static void insertDataToBatteryStateTable(
Context context, long timestamp, String packageName) { Context context, long timestamp, String packageName) {
insertDataToBatteryStateDatabase( insertDataToBatteryStateTable(
context, timestamp, packageName, /*multiple=*/ false, /*isFullChargeStart=*/ false); context, timestamp, packageName, /*multiple=*/ false, /*isFullChargeStart=*/ false);
} }
/** Inserts a fake data into the database for testing. */ /** Inserts a fake data into the database for testing. */
public static void insertDataToBatteryStateDatabase( public static void insertDataToBatteryStateTable(
Context context, long timestamp, String packageName, boolean isFullChargeStart) { Context context, long timestamp, String packageName, boolean isFullChargeStart) {
insertDataToBatteryStateDatabase( insertDataToBatteryStateTable(
context, timestamp, packageName, /*multiple=*/ false, isFullChargeStart); context, timestamp, packageName, /*multiple=*/ false, isFullChargeStart);
} }
/** Inserts a fake data into the database for testing. */ /** Inserts a fake data into the database for testing. */
public static void insertDataToBatteryStateDatabase( public static void insertDataToBatteryStateTable(
Context context, long timestamp, String packageName, boolean multiple, Context context, long timestamp, String packageName, boolean multiple,
boolean isFullChargeStart) { boolean isFullChargeStart) {
DeviceBatteryState deviceBatteryState = DeviceBatteryState deviceBatteryState =
@@ -133,6 +135,34 @@ public class BatteryTestUtils {
} }
} }
/** Inserts a fake data into the database for testing. */
public static void insertDataToAppUsageEventTable(
Context context, long userId, long timestamp, String packageName) {
insertDataToAppUsageEventTable(
context, userId, timestamp, packageName, /*multiple=*/ false);
}
/** Inserts a fake data into the database for testing. */
public static void insertDataToAppUsageEventTable(
Context context, long userId, long timestamp, String packageName, boolean multiple) {
final AppUsageEventEntity entity =
new AppUsageEventEntity(
/*uid=*/ 101L,
userId,
timestamp,
/*appUsageEventType=*/ 2,
packageName,
/*instanceId=*/ 10001,
/*taskRootPackageName=*/ "com.android.settings");
AppUsageEventDao dao =
BatteryStateDatabase.getInstance(context).appUsageEventDao();
if (multiple) {
dao.insertAll(ImmutableList.of(entity));
} else {
dao.insert(entity);
}
}
public static Intent getCustomBatteryIntent(int plugged, int level, int scale, int status) { public static Intent getCustomBatteryIntent(int plugged, int level, int scale, int status) {
Intent intent = new Intent(); Intent intent = new Intent();
intent.putExtra(BatteryManager.EXTRA_PLUGGED, plugged); intent.putExtra(BatteryManager.EXTRA_PLUGGED, plugged);