diff --git a/api/system-current.txt b/api/system-current.txt index e99ac06c258ea..43bd8dd5eb81a 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -927,6 +927,9 @@ package android.app.usage { method public java.util.Map getAppStandbyBuckets(); method public void registerAppUsageObserver(int, java.lang.String[], long, java.util.concurrent.TimeUnit, android.app.PendingIntent); method public void registerUsageSessionObserver(int, java.lang.String[], long, java.util.concurrent.TimeUnit, long, java.util.concurrent.TimeUnit, android.app.PendingIntent, android.app.PendingIntent); + method public void reportUsageStart(android.app.Activity, java.lang.String); + method public void reportUsageStart(android.app.Activity, java.lang.String, long); + method public void reportUsageStop(android.app.Activity, java.lang.String); method public void setAppStandbyBucket(java.lang.String, int); method public void setAppStandbyBuckets(java.util.Map); method public void unregisterAppUsageObserver(int); diff --git a/core/java/android/app/usage/IUsageStatsManager.aidl b/core/java/android/app/usage/IUsageStatsManager.aidl index 4d52263c1d78d..bbae7d3463ae1 100644 --- a/core/java/android/app/usage/IUsageStatsManager.aidl +++ b/core/java/android/app/usage/IUsageStatsManager.aidl @@ -55,4 +55,8 @@ interface IUsageStatsManager { long sessionThresholdTimeMs, in PendingIntent limitReachedCallbackIntent, in PendingIntent sessionEndCallbackIntent, String callingPackage); void unregisterUsageSessionObserver(int sessionObserverId, String callingPackage); + void reportUsageStart(in IBinder activity, String token, String callingPackage); + void reportPastUsageStart(in IBinder activity, String token, long timeAgoMs, + String callingPackage); + void reportUsageStop(in IBinder activity, String token, String callingPackage); } diff --git a/core/java/android/app/usage/UsageStatsManager.java b/core/java/android/app/usage/UsageStatsManager.java index 26beb45be13f2..605deac809947 100644 --- a/core/java/android/app/usage/UsageStatsManager.java +++ b/core/java/android/app/usage/UsageStatsManager.java @@ -23,6 +23,7 @@ import android.annotation.RequiresPermission; import android.annotation.SystemApi; import android.annotation.SystemService; import android.annotation.UnsupportedAppUsage; +import android.app.Activity; import android.app.PendingIntent; import android.content.Context; import android.content.pm.ParceledListSlice; @@ -579,15 +580,18 @@ public final class UsageStatsManager { /** * @hide * Register an app usage limit observer that receives a callback on the provided intent when - * the sum of usages of apps in the packages array exceeds the {@code timeLimit} specified. The - * observer will automatically be unregistered when the time limit is reached and the intent - * is delivered. Registering an {@code observerId} that was already registered will override - * the previous one. No more than 1000 unique {@code observerId} may be registered by a single - * uid at any one time. + * the sum of usages of apps and tokens in the {@code observed} array exceeds the + * {@code timeLimit} specified. The structure of a token is a String with the reporting + * package's name and a token the reporting app will use, separated by the forward slash + * character. Example: com.reporting.package/5OM3*0P4QU3-7OK3N + * The observer will automatically be unregistered when the time limit is reached and the + * intent is delivered. Registering an {@code observerId} that was already registered will + * override the previous one. No more than 1000 unique {@code observerId} may be registered by + * a single uid at any one time. * @param observerId A unique id associated with the group of apps to be monitored. There can * be multiple groups with common packages and different time limits. - * @param packages The list of packages to observe for foreground activity time. Cannot be null - * and must include at least one package. + * @param observedEntities The list of packages and token to observe for usage time. Cannot be + * null and must include at least one package or token. * @param timeLimit The total time the set of apps can be in the foreground before the * callbackIntent is delivered. Must be at least one minute. * @param timeUnit The unit for time specified in {@code timeLimit}. Cannot be null. @@ -600,11 +604,11 @@ public final class UsageStatsManager { */ @SystemApi @RequiresPermission(android.Manifest.permission.OBSERVE_APP_USAGE) - public void registerAppUsageObserver(int observerId, @NonNull String[] packages, long timeLimit, - @NonNull TimeUnit timeUnit, @NonNull PendingIntent callbackIntent) { + public void registerAppUsageObserver(int observerId, @NonNull String[] observedEntities, + long timeLimit, @NonNull TimeUnit timeUnit, @NonNull PendingIntent callbackIntent) { try { - mService.registerAppUsageObserver(observerId, packages, timeUnit.toMillis(timeLimit), - callbackIntent, mContext.getOpPackageName()); + mService.registerAppUsageObserver(observerId, observedEntities, + timeUnit.toMillis(timeLimit), callbackIntent, mContext.getOpPackageName()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -631,18 +635,21 @@ public final class UsageStatsManager { /** * Register a usage session observer that receives a callback on the provided {@code - * limitReachedCallbackIntent} when the sum of usages of apps in the packages array exceeds - * the {@code timeLimit} specified within a usage session. After the {@code timeLimit} has - * been reached, the usage session observer will receive a callback on the provided {@code - * sessionEndCallbackIntent} when the usage session ends. Registering another session - * observer against a {@code sessionObserverId} that has already been registered will - * override the previous session observer. + * limitReachedCallbackIntent} when the sum of usages of apps and tokens in the {@code + * observed} array exceeds the {@code timeLimit} specified within a usage session. The + * structure of a token is a String with the reporting packages' name and a token the + * reporting app will use, separated by the forward slash character. + * Example: com.reporting.package/5OM3*0P4QU3-7OK3N + * After the {@code timeLimit} has been reached, the usage session observer will receive a + * callback on the provided {@code sessionEndCallbackIntent} when the usage session ends. + * Registering another session observer against a {@code sessionObserverId} that has already + * been registered will override the previous session observer. * * @param sessionObserverId A unique id associated with the group of apps to be * monitored. There can be multiple groups with common * packages and different time limits. - * @param packages The list of packages to observe for foreground activity time. Cannot be null - * and must include at least one package. + * @param observedEntities The list of packages and token to observe for usage time. Cannot be + * null and must include at least one package or token. * @param timeLimit The total time the set of apps can be used continuously before the {@code * limitReachedCallbackIntent} is delivered. Must be at least one minute. * @param timeUnit The unit for time specified in {@code timeLimit}. Cannot be null. @@ -668,13 +675,13 @@ public final class UsageStatsManager { */ @SystemApi @RequiresPermission(android.Manifest.permission.OBSERVE_APP_USAGE) - public void registerUsageSessionObserver(int sessionObserverId, @NonNull String[] packages, - long timeLimit, @NonNull TimeUnit timeUnit, long sessionThresholdTime, - @NonNull TimeUnit sessionThresholdTimeUnit, + public void registerUsageSessionObserver(int sessionObserverId, + @NonNull String[] observedEntities, long timeLimit, @NonNull TimeUnit timeUnit, + long sessionThresholdTime, @NonNull TimeUnit sessionThresholdTimeUnit, @NonNull PendingIntent limitReachedCallbackIntent, @Nullable PendingIntent sessionEndCallbackIntent) { try { - mService.registerUsageSessionObserver(sessionObserverId, packages, + mService.registerUsageSessionObserver(sessionObserverId, observedEntities, timeUnit.toMillis(timeLimit), sessionThresholdTimeUnit.toMillis(sessionThresholdTime), limitReachedCallbackIntent, sessionEndCallbackIntent, @@ -704,6 +711,71 @@ public final class UsageStatsManager { } } + /** + * Report usage associated with a particular {@code token} has started. Tokens are app defined + * strings used to represent usage of in-app features. Apps with the {@link + * android.Manifest.permission#OBSERVE_APP_USAGE} permission can register time limit observers + * to monitor the usage of a token. In app usage can only associated with an {@code activity} + * and usage will be considered stopped if the activity stops or crashes. + * @see #registerAppUsageObserver + * @see #registerUsageSessionObserver + * + * @param activity The activity {@code token} is associated with. + * @param token The token to report usage against. + * @hide + */ + @SystemApi + public void reportUsageStart(@NonNull Activity activity, @NonNull String token) { + try { + mService.reportUsageStart(activity.getActivityToken(), token, + mContext.getOpPackageName()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Report usage associated with a particular {@code token} had started some amount of time in + * the past. Tokens are app defined strings used to represent usage of in-app features. Apps + * with the {@link android.Manifest.permission#OBSERVE_APP_USAGE} permission can register time + * limit observers to monitor the usage of a token. In app usage can only associated with an + * {@code activity} and usage will be considered stopped if the activity stops or crashes. + * @see #registerAppUsageObserver + * @see #registerUsageSessionObserver + * + * @param activity The activity {@code token} is associated with. + * @param token The token to report usage against. + * @param timeAgoMs How long ago the start of usage took place + * @hide + */ + @SystemApi + public void reportUsageStart(@NonNull Activity activity, @NonNull String token, + long timeAgoMs) { + try { + mService.reportPastUsageStart(activity.getActivityToken(), token, timeAgoMs, + mContext.getOpPackageName()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Report the usage associated with a particular {@code token} has stopped. + * + * @param activity The activity {@code token} is associated with. + * @param token The token to report usage against. + * @hide + */ + @SystemApi + public void reportUsageStop(@NonNull Activity activity, @NonNull String token) { + try { + mService.reportUsageStop(activity.getActivityToken(), token, + mContext.getOpPackageName()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + /** @hide */ public static String reasonToString(int standbyReason) { StringBuilder sb = new StringBuilder(); diff --git a/services/tests/servicestests/src/com/android/server/usage/AppTimeLimitControllerTests.java b/services/tests/servicestests/src/com/android/server/usage/AppTimeLimitControllerTests.java index 8496a961959df..b348aeef802e5 100644 --- a/services/tests/servicestests/src/com/android/server/usage/AppTimeLimitControllerTests.java +++ b/services/tests/servicestests/src/com/android/server/usage/AppTimeLimitControllerTests.java @@ -699,10 +699,52 @@ public class AppTimeLimitControllerTests { assertTrue(hasUsageSessionObserver(UID, OBS_ID1)); } + /** Verify the timeout message is delivered at the right time after past usage was reported */ + @Test + public void testAppUsageObserver_PastUsage() throws Exception { + setTime(10_000L); + addAppUsageObserver(OBS_ID1, GROUP1, 6_000L); + setTime(20_000L); + startPastUsage(PKG_SOC1, 5_000); + setTime(21_000L); + assertTrue(mLimitReachedLatch.await(2_000L, TimeUnit.MILLISECONDS)); + stopUsage(PKG_SOC1); + // Verify that the observer was removed + assertFalse(hasAppUsageObserver(UID, OBS_ID1)); + } + + /** + * Verify the timeout message is delivered at the right time after past usage was reported + * that overlaps with already known usage + */ + @Test + public void testAppUsageObserver_PastUsageOverlap() throws Exception { + setTime(0L); + addAppUsageObserver(OBS_ID1, GROUP1, 20_000L); + setTime(10_000L); + startUsage(PKG_SOC1); + setTime(20_000L); + stopUsage(PKG_SOC1); + setTime(25_000L); + startPastUsage(PKG_SOC1, 9_000); + setTime(26_000L); + // the 4 seconds of overlapped usage should not be counted + assertFalse(mLimitReachedLatch.await(2_000L, TimeUnit.MILLISECONDS)); + setTime(30_000L); + assertTrue(mLimitReachedLatch.await(4_000L, TimeUnit.MILLISECONDS)); + stopUsage(PKG_SOC1); + // Verify that the observer was removed + assertFalse(hasAppUsageObserver(UID, OBS_ID1)); + } + private void startUsage(String packageName) { mController.noteUsageStart(packageName, USER_ID); } + private void startPastUsage(String packageName, int timeAgo) { + mController.noteUsageStart(packageName, USER_ID, timeAgo); + } + private void stopUsage(String packageName) { mController.noteUsageStop(packageName, USER_ID); } diff --git a/services/usage/java/com/android/server/usage/AppTimeLimitController.java b/services/usage/java/com/android/server/usage/AppTimeLimitController.java index 8e1ede116abf8..2ed11fe92e153 100644 --- a/services/usage/java/com/android/server/usage/AppTimeLimitController.java +++ b/services/usage/java/com/android/server/usage/AppTimeLimitController.java @@ -23,7 +23,6 @@ import android.os.Looper; import android.os.Message; import android.os.SystemClock; import android.util.ArrayMap; -import android.util.ArraySet; import android.util.Slog; import android.util.SparseArray; @@ -62,6 +61,8 @@ public class AppTimeLimitController { private static final long ONE_MINUTE = 60_000L; + private static final Integer ONE = new Integer(1); + /** Collection of data for each user that has reported usage */ @GuardedBy("mLock") private final SparseArray mUsers = new SparseArray<>(); @@ -79,11 +80,11 @@ public class AppTimeLimitController { private @UserIdInt int userId; - /** Set of the currently active entities */ - private final ArraySet currentlyActive = new ArraySet<>(); + /** Count of the currently active entities */ + public final ArrayMap currentlyActive = new ArrayMap<>(); /** Map from entity name for quick lookup */ - private final ArrayMap> observedMap = new ArrayMap<>(); + public final ArrayMap> observedMap = new ArrayMap<>(); private UserData(@UserIdInt int userId) { this.userId = userId; @@ -94,7 +95,7 @@ public class AppTimeLimitController { // TODO: Consider using a bloom filter here if number of actives becomes large final int size = entities.length; for (int i = 0; i < size; i++) { - if (currentlyActive.contains(entities[i])) { + if (currentlyActive.containsKey(entities[i])) { return true; } } @@ -137,7 +138,7 @@ public class AppTimeLimitController { pw.print(" Currently Active:"); final int nActive = currentlyActive.size(); for (int i = 0; i < nActive; i++) { - pw.print(currentlyActive.valueAt(i)); + pw.print(currentlyActive.keyAt(i)); pw.print(", "); } pw.println(); @@ -233,6 +234,7 @@ public class AppTimeLimitController { protected long mUsageTimeMs; protected int mActives; protected long mLastKnownUsageTimeMs; + protected long mLastUsageEndTimeMs; protected WeakReference mUserRef; protected WeakReference mObserverAppRef; protected PendingIntent mLimitReachedCallback; @@ -271,9 +273,15 @@ public class AppTimeLimitController { @GuardedBy("mLock") void noteUsageStart(long startTimeMs, long currentTimeMs) { if (mActives++ == 0) { + // If last known usage ended after the start of this usage, there is overlap + // between the last usage session and this one. Avoid double counting by only + // counting from the end of the last session. This has a rare side effect that some + // usage will not be accounted for if the previous session started and stopped + // within this current usage. + startTimeMs = mLastUsageEndTimeMs > startTimeMs ? mLastUsageEndTimeMs : startTimeMs; mLastKnownUsageTimeMs = startTimeMs; final long timeRemaining = - mTimeLimitMs - mUsageTimeMs + currentTimeMs - startTimeMs; + mTimeLimitMs - mUsageTimeMs - currentTimeMs + startTimeMs; if (timeRemaining > 0) { if (DEBUG) { Slog.d(TAG, "Posting timeout for " + mObserverId + " for " @@ -287,7 +295,7 @@ public class AppTimeLimitController { mActives = mObserved.length; final UserData user = mUserRef.get(); if (user == null) return; - final Object[] array = user.currentlyActive.toArray(); + final Object[] array = user.currentlyActive.keySet().toArray(); Slog.e(TAG, "Too many noted usage starts! Observed entities: " + Arrays.toString( mObserved) + " Active Entities: " + Arrays.toString(array)); @@ -300,6 +308,8 @@ public class AppTimeLimitController { if (--mActives == 0) { final boolean limitNotCrossed = mUsageTimeMs < mTimeLimitMs; mUsageTimeMs += stopTimeMs - mLastKnownUsageTimeMs; + + mLastUsageEndTimeMs = stopTimeMs; if (limitNotCrossed && mUsageTimeMs >= mTimeLimitMs) { // Crossed the limit if (DEBUG) Slog.d(TAG, "MTB informing group obs=" + mObserverId); @@ -312,7 +322,7 @@ public class AppTimeLimitController { mActives = 0; final UserData user = mUserRef.get(); if (user == null) return; - final Object[] array = user.currentlyActive.toArray(); + final Object[] array = user.currentlyActive.keySet().toArray(); Slog.e(TAG, "Too many noted usage stops! Observed entities: " + Arrays.toString( mObserved) + " Active Entities: " + Arrays.toString(array)); @@ -409,7 +419,6 @@ public class AppTimeLimitController { } class SessionUsageGroup extends UsageGroup { - private long mLastUsageEndTimeMs; private long mNewSessionThresholdMs; private PendingIntent mSessionEndCallback; @@ -451,7 +460,6 @@ public class AppTimeLimitController { public void noteUsageStop(long stopTimeMs) { super.noteUsageStop(stopTimeMs); if (mActives == 0) { - mLastUsageEndTimeMs = stopTimeMs; if (mUsageTimeMs >= mTimeLimitMs) { // Usage has ended. Schedule the session end callback to be triggered once // the new session threshold has been reached @@ -467,7 +475,10 @@ public class AppTimeLimitController { UserData user = mUserRef.get(); if (user == null) return; if (mListener != null) { - mListener.onSessionEnd(mObserverId, user.userId, mUsageTimeMs, mSessionEndCallback); + mListener.onSessionEnd(mObserverId, + user.userId, + mUsageTimeMs, + mSessionEndCallback); } } @@ -599,7 +610,7 @@ public class AppTimeLimitController { // TODO: Consider using a bloom filter here if number of actives becomes large final int size = group.mObserved.length; for (int i = 0; i < size; i++) { - if (user.currentlyActive.contains(group.mObserved[i])) { + if (user.currentlyActive.containsKey(group.mObserved[i])) { // Entity is currently active. Start group's usage. group.noteUsageStart(currentTimeMs); } @@ -717,21 +728,28 @@ public class AppTimeLimitController { /** * Called when an entity becomes active. * - * @param name The entity that became active - * @param userId The user + * @param name The entity that became active + * @param userId The user + * @param timeAgoMs Time since usage was started */ - public void noteUsageStart(String name, int userId) throws IllegalArgumentException { + public void noteUsageStart(String name, int userId, long timeAgoMs) + throws IllegalArgumentException { synchronized (mLock) { UserData user = getOrCreateUserDataLocked(userId); if (DEBUG) Slog.d(TAG, "Usage entity " + name + " became active"); - if (user.currentlyActive.contains(name)) { - throw new IllegalArgumentException( - "Unable to start usage for " + name + ", already in use"); + + final int index = user.currentlyActive.indexOfKey(name); + if (index >= 0) { + final Integer count = user.currentlyActive.valueAt(index); + if (count != null) { + // There are multiple instances of this entity. Just increment the count. + user.currentlyActive.setValueAt(index, count + 1); + return; + } } final long currentTime = getUptimeMillis(); - // Add to the list of active entities - user.currentlyActive.add(name); + user.currentlyActive.put(name, ONE); ArrayList groups = user.observedMap.get(name); if (groups == null) return; @@ -739,11 +757,21 @@ public class AppTimeLimitController { final int size = groups.size(); for (int i = 0; i < size; i++) { UsageGroup group = groups.get(i); - group.noteUsageStart(currentTime); + group.noteUsageStart(currentTime - timeAgoMs, currentTime); } } } + /** + * Called when an entity becomes active. + * + * @param name The entity that became active + * @param userId The user + */ + public void noteUsageStart(String name, int userId) throws IllegalArgumentException { + noteUsageStart(name, userId, 0); + } + /** * Called when an entity becomes inactive. * @@ -754,10 +782,21 @@ public class AppTimeLimitController { synchronized (mLock) { UserData user = getOrCreateUserDataLocked(userId); if (DEBUG) Slog.d(TAG, "Usage entity " + name + " became inactive"); - if (!user.currentlyActive.remove(name)) { + + final int index = user.currentlyActive.indexOfKey(name); + if (index < 0) { throw new IllegalArgumentException( "Unable to stop usage for " + name + ", not in use"); } + + final Integer count = user.currentlyActive.valueAt(index); + if (!count.equals(ONE)) { + // There are multiple instances of this entity. Just decrement the count. + user.currentlyActive.setValueAt(index, count - 1); + return; + } + + user.currentlyActive.removeAt(index); final long currentTime = getUptimeMillis(); // Check if any of the groups need to watch for this entity @@ -769,6 +808,7 @@ public class AppTimeLimitController { UsageGroup group = groups.get(i); group.noteUsageStop(currentTime); } + } } @@ -780,7 +820,8 @@ public class AppTimeLimitController { @GuardedBy("mLock") private void postInformSessionEndListenerLocked(SessionUsageGroup group, long timeout) { - mHandler.sendMessageDelayed(mHandler.obtainMessage(MyHandler.MSG_INFORM_SESSION_END, group), + mHandler.sendMessageDelayed( + mHandler.obtainMessage(MyHandler.MSG_INFORM_SESSION_END, group), timeout); } @@ -800,7 +841,27 @@ public class AppTimeLimitController { mHandler.removeMessages(MyHandler.MSG_CHECK_TIMEOUT, group); } - void dump(PrintWriter pw) { + void dump(String[] args, PrintWriter pw) { + if (args != null) { + for (int i = 0; i < args.length; i++) { + String arg = args[i]; + if ("actives".equals(arg)) { + synchronized (mLock) { + final int nUsers = mUsers.size(); + for (int user = 0; user < nUsers; user++) { + final ArrayMap actives = + mUsers.valueAt(user).currentlyActive; + final int nActive = actives.size(); + for (int active = 0; active < nActive; active++) { + pw.println(actives.keyAt(active)); + } + } + } + return; + } + } + } + synchronized (mLock) { pw.println("\n App Time Limits"); final int nUsers = mUsers.size(); diff --git a/services/usage/java/com/android/server/usage/UsageStatsService.java b/services/usage/java/com/android/server/usage/UsageStatsService.java index 57dc08fcd2535..f146370b01d7d 100644 --- a/services/usage/java/com/android/server/usage/UsageStatsService.java +++ b/services/usage/java/com/android/server/usage/UsageStatsService.java @@ -53,6 +53,7 @@ import android.os.Binder; import android.os.Environment; import android.os.FileUtils; import android.os.Handler; +import android.os.IBinder; import android.os.IDeviceIdleController; import android.os.Looper; import android.os.Message; @@ -105,6 +106,8 @@ public class UsageStatsService extends SystemService implements private static final boolean ENABLE_KERNEL_UPDATES = true; private static final File KERNEL_COUNTER_FILE = new File("/proc/uid_procstat/set"); + private static final char TOKEN_DELIMITER = '/'; + // Handler message types. static final int MSG_REPORT_EVENT = 0; static final int MSG_FLUSH_TO_DISK = 1; @@ -135,6 +138,10 @@ public class UsageStatsService extends SystemService implements /** Manages app time limit observers */ AppTimeLimitController mAppTimeLimit; + final SparseArray> mUsageReporters = new SparseArray(); + final SparseArray mVisibleActivities = new SparseArray(); + + private UsageStatsManagerInternal.AppIdleStateChangeListener mStandbyChangeListener = new UsageStatsManagerInternal.AppIdleStateChangeListener() { @Override @@ -270,7 +277,7 @@ public class UsageStatsService extends SystemService implements mHandler.obtainMessage(MSG_REMOVE_USER, userId, 0).sendToTarget(); } } else if (Intent.ACTION_USER_STARTED.equals(action)) { - if (userId >=0) { + if (userId >= 0) { mAppStandby.postCheckIdleStates(userId); } } @@ -434,17 +441,46 @@ public class UsageStatsService extends SystemService implements mAppStandby.reportEvent(event, elapsedRealtime, userId); switch (event.mEventType) { case Event.ACTIVITY_RESUMED: - try { - mAppTimeLimit.noteUsageStart(event.getPackageName(), userId); - } catch (IllegalArgumentException iae) { - Slog.e(TAG, "Failed to note usage start", iae); + synchronized (mVisibleActivities) { + mVisibleActivities.put(event.mInstanceId, event.getClassName()); + try { + mAppTimeLimit.noteUsageStart(event.getPackageName(), userId); + } catch (IllegalArgumentException iae) { + Slog.e(TAG, "Failed to note usage start", iae); + } } break; - case Event.ACTIVITY_PAUSED: - try { - mAppTimeLimit.noteUsageStop(event.getPackageName(), userId); - } catch (IllegalArgumentException iae) { - Slog.e(TAG, "Failed to note usage stop", iae); + case Event.ACTIVITY_STOPPED: + case Event.ACTIVITY_DESTROYED: + ArraySet tokens; + synchronized (mUsageReporters) { + tokens = mUsageReporters.removeReturnOld(event.mInstanceId); + } + if (tokens != null) { + synchronized (tokens) { + final int size = tokens.size(); + // Stop usage on behalf of a UsageReporter that stopped + for (int i = 0; i < size; i++) { + final String token = tokens.valueAt(i); + try { + mAppTimeLimit.noteUsageStop( + buildFullToken(event.getPackageName(), token), userId); + } catch (IllegalArgumentException iae) { + Slog.w(TAG, "Failed to stop usage for during reporter death: " + + iae); + } + } + } + } + + synchronized (mVisibleActivities) { + if (mVisibleActivities.removeReturnOld(event.mInstanceId) != null) { + try { + mAppTimeLimit.noteUsageStop(event.getPackageName(), userId); + } catch (IllegalArgumentException iae) { + Slog.w(TAG, "Failed to note usage stop", iae); + } + } } break; } @@ -599,6 +635,14 @@ public class UsageStatsService extends SystemService implements return beginTime <= currentTime && beginTime < endTime; } + private String buildFullToken(String packageName, String token) { + final StringBuilder sb = new StringBuilder(packageName.length() + token.length() + 1); + sb.append(packageName); + sb.append(TOKEN_DELIMITER); + sb.append(token); + return sb.toString(); + } + private void flushToDiskLocked() { final int userCount = mUserState.size(); for (int i = 0; i < userCount; i++) { @@ -627,8 +671,7 @@ public class UsageStatsService extends SystemService implements String arg = args[i]; if ("--checkin".equals(arg)) { checkin = true; - } else - if ("-c".equals(arg)) { + } else if ("-c".equals(arg)) { compact = true; } else if ("flush".equals(arg)) { flushToDiskLocked(); @@ -637,6 +680,15 @@ public class UsageStatsService extends SystemService implements } else if ("is-app-standby-enabled".equals(arg)) { pw.println(mAppStandby.mAppIdleEnabled); return; + } else if ("apptimelimit".equals(arg)) { + if (i + 1 >= args.length) { + mAppTimeLimit.dump(null, pw); + } else { + final String[] remainingArgs = + Arrays.copyOfRange(args, i + 1, args.length); + mAppTimeLimit.dump(remainingArgs, pw); + } + return; } else if (arg != null && !arg.startsWith("-")) { // Anything else that doesn't start with '-' is a pkg to filter pkg = arg; @@ -666,7 +718,7 @@ public class UsageStatsService extends SystemService implements mAppStandby.dumpState(args, pw); } - mAppTimeLimit.dump(pw); + mAppTimeLimit.dump(null, pw); } } @@ -1231,16 +1283,82 @@ public class UsageStatsService extends SystemService implements final int userId = UserHandle.getUserId(callingUid); final long token = Binder.clearCallingIdentity(); try { - UsageStatsService.this.unregisterUsageSessionObserver(callingUid, sessionObserverId, userId); + UsageStatsService.this.unregisterUsageSessionObserver(callingUid, sessionObserverId, + userId); } finally { Binder.restoreCallingIdentity(token); } } + + @Override + public void reportUsageStart(IBinder activity, String token, String callingPackage) { + reportPastUsageStart(activity, token, 0, callingPackage); + } + + @Override + public void reportPastUsageStart(IBinder activity, String token, long timeAgoMs, + String callingPackage) { + + final int callingUid = Binder.getCallingUid(); + final int userId = UserHandle.getUserId(callingUid); + final long binderToken = Binder.clearCallingIdentity(); + try { + ArraySet tokens; + synchronized (mUsageReporters) { + tokens = mUsageReporters.get(activity.hashCode()); + if (tokens == null) { + tokens = new ArraySet(); + mUsageReporters.put(activity.hashCode(), tokens); + } + } + + synchronized (tokens) { + if (!tokens.add(token)) { + throw new IllegalArgumentException(token + " for " + callingPackage + + " is already reported as started for this activity"); + } + } + + mAppTimeLimit.noteUsageStart(buildFullToken(callingPackage, token), + userId, timeAgoMs); + } finally { + Binder.restoreCallingIdentity(binderToken); + } + } + + @Override + public void reportUsageStop(IBinder activity, String token, String callingPackage) { + final int callingUid = Binder.getCallingUid(); + final int userId = UserHandle.getUserId(callingUid); + final long binderToken = Binder.clearCallingIdentity(); + try { + ArraySet tokens; + synchronized (mUsageReporters) { + tokens = mUsageReporters.get(activity.hashCode()); + if (tokens == null) { + throw new IllegalArgumentException( + "Unknown reporter trying to stop token " + token + " for " + + callingPackage); + } + } + + synchronized (tokens) { + if (!tokens.remove(token)) { + throw new IllegalArgumentException(token + " for " + callingPackage + + " is already reported as stopped for this activity"); + } + } + mAppTimeLimit.noteUsageStop(buildFullToken(callingPackage, token), userId); + } finally { + Binder.restoreCallingIdentity(binderToken); + } + } } void registerAppUsageObserver(int callingUid, int observerId, String[] packages, long timeLimitMs, PendingIntent callbackIntent, int userId) { - mAppTimeLimit.addAppUsageObserver(callingUid, observerId, packages, timeLimitMs, callbackIntent, + mAppTimeLimit.addAppUsageObserver(callingUid, observerId, packages, timeLimitMs, + callbackIntent, userId); } diff --git a/tests/UsageReportingTest/Android.mk b/tests/UsageReportingTest/Android.mk new file mode 100644 index 0000000000000..afb6e16b1fdf9 --- /dev/null +++ b/tests/UsageReportingTest/Android.mk @@ -0,0 +1,17 @@ +LOCAL_PATH:= $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_MODULE_TAGS := tests + +# Only compile source java files in this apk. +LOCAL_SRC_FILES := $(call all-java-files-under, src) + +LOCAL_USE_AAPT2 := true +LOCAL_STATIC_ANDROID_LIBRARIES := androidx.legacy_legacy-support-v4 + +LOCAL_CERTIFICATE := platform + +LOCAL_PACKAGE_NAME := UsageReportingTest +LOCAL_PRIVATE_PLATFORM_APIS := true + +include $(BUILD_PACKAGE) diff --git a/tests/UsageReportingTest/AndroidManifest.xml b/tests/UsageReportingTest/AndroidManifest.xml new file mode 100644 index 0000000000000..be0b09e972a58 --- /dev/null +++ b/tests/UsageReportingTest/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + diff --git a/tests/UsageReportingTest/res/layout/row_item.xml b/tests/UsageReportingTest/res/layout/row_item.xml new file mode 100644 index 0000000000000..1eb2dab291240 --- /dev/null +++ b/tests/UsageReportingTest/res/layout/row_item.xml @@ -0,0 +1,36 @@ + + + + + + + +