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