Loading api/system-current.txt +3 −0 Original line number Diff line number Diff line Loading @@ -927,6 +927,9 @@ package android.app.usage { method public java.util.Map<java.lang.String, java.lang.Integer> 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<java.lang.String, java.lang.Integer>); method public void unregisterAppUsageObserver(int); Loading core/java/android/app/usage/IUsageStatsManager.aidl +4 −0 Original line number Diff line number Diff line Loading @@ -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); } core/java/android/app/usage/UsageStatsManager.java +95 −23 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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. Loading @@ -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(); } Loading @@ -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. Loading @@ -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, Loading Loading @@ -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(); Loading services/tests/servicestests/src/com/android/server/usage/AppTimeLimitControllerTests.java +42 −0 Original line number Diff line number Diff line Loading @@ -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); } Loading services/usage/java/com/android/server/usage/AppTimeLimitController.java +86 −25 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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<UserData> mUsers = new SparseArray<>(); Loading @@ -79,11 +80,11 @@ public class AppTimeLimitController { private @UserIdInt int userId; /** Set of the currently active entities */ private final ArraySet<String> currentlyActive = new ArraySet<>(); /** Count of the currently active entities */ public final ArrayMap<String, Integer> currentlyActive = new ArrayMap<>(); /** Map from entity name for quick lookup */ private final ArrayMap<String, ArrayList<UsageGroup>> observedMap = new ArrayMap<>(); public final ArrayMap<String, ArrayList<UsageGroup>> observedMap = new ArrayMap<>(); private UserData(@UserIdInt int userId) { this.userId = userId; Loading @@ -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; } } Loading Loading @@ -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(); Loading Loading @@ -233,6 +234,7 @@ public class AppTimeLimitController { protected long mUsageTimeMs; protected int mActives; protected long mLastKnownUsageTimeMs; protected long mLastUsageEndTimeMs; protected WeakReference<UserData> mUserRef; protected WeakReference<ObserverAppData> mObserverAppRef; protected PendingIntent mLimitReachedCallback; Loading Loading @@ -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 " Loading @@ -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)); Loading @@ -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); Loading @@ -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)); Loading Loading @@ -409,7 +419,6 @@ public class AppTimeLimitController { } class SessionUsageGroup extends UsageGroup { private long mLastUsageEndTimeMs; private long mNewSessionThresholdMs; private PendingIntent mSessionEndCallback; Loading Loading @@ -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 Loading @@ -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); } } Loading Loading @@ -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); } Loading Loading @@ -719,19 +730,26 @@ public class AppTimeLimitController { * * @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<UsageGroup> groups = user.observedMap.get(name); if (groups == null) return; Loading @@ -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. * Loading @@ -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 Loading @@ -769,6 +808,7 @@ public class AppTimeLimitController { UsageGroup group = groups.get(i); group.noteUsageStop(currentTime); } } } Loading @@ -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); } Loading @@ -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<String, Integer> 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(); Loading Loading
api/system-current.txt +3 −0 Original line number Diff line number Diff line Loading @@ -927,6 +927,9 @@ package android.app.usage { method public java.util.Map<java.lang.String, java.lang.Integer> 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<java.lang.String, java.lang.Integer>); method public void unregisterAppUsageObserver(int); Loading
core/java/android/app/usage/IUsageStatsManager.aidl +4 −0 Original line number Diff line number Diff line Loading @@ -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); }
core/java/android/app/usage/UsageStatsManager.java +95 −23 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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. Loading @@ -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(); } Loading @@ -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. Loading @@ -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, Loading Loading @@ -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(); Loading
services/tests/servicestests/src/com/android/server/usage/AppTimeLimitControllerTests.java +42 −0 Original line number Diff line number Diff line Loading @@ -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); } Loading
services/usage/java/com/android/server/usage/AppTimeLimitController.java +86 −25 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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<UserData> mUsers = new SparseArray<>(); Loading @@ -79,11 +80,11 @@ public class AppTimeLimitController { private @UserIdInt int userId; /** Set of the currently active entities */ private final ArraySet<String> currentlyActive = new ArraySet<>(); /** Count of the currently active entities */ public final ArrayMap<String, Integer> currentlyActive = new ArrayMap<>(); /** Map from entity name for quick lookup */ private final ArrayMap<String, ArrayList<UsageGroup>> observedMap = new ArrayMap<>(); public final ArrayMap<String, ArrayList<UsageGroup>> observedMap = new ArrayMap<>(); private UserData(@UserIdInt int userId) { this.userId = userId; Loading @@ -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; } } Loading Loading @@ -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(); Loading Loading @@ -233,6 +234,7 @@ public class AppTimeLimitController { protected long mUsageTimeMs; protected int mActives; protected long mLastKnownUsageTimeMs; protected long mLastUsageEndTimeMs; protected WeakReference<UserData> mUserRef; protected WeakReference<ObserverAppData> mObserverAppRef; protected PendingIntent mLimitReachedCallback; Loading Loading @@ -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 " Loading @@ -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)); Loading @@ -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); Loading @@ -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)); Loading Loading @@ -409,7 +419,6 @@ public class AppTimeLimitController { } class SessionUsageGroup extends UsageGroup { private long mLastUsageEndTimeMs; private long mNewSessionThresholdMs; private PendingIntent mSessionEndCallback; Loading Loading @@ -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 Loading @@ -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); } } Loading Loading @@ -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); } Loading Loading @@ -719,19 +730,26 @@ public class AppTimeLimitController { * * @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<UsageGroup> groups = user.observedMap.get(name); if (groups == null) return; Loading @@ -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. * Loading @@ -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 Loading @@ -769,6 +808,7 @@ public class AppTimeLimitController { UsageGroup group = groups.get(i); group.noteUsageStop(currentTime); } } } Loading @@ -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); } Loading @@ -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<String, Integer> 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(); Loading