diff options
15 files changed, 884 insertions, 64 deletions
diff --git a/api/system-current.txt b/api/system-current.txt index 09dca1e1b298..ecff03994a92 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -917,6 +917,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); diff --git a/core/java/android/app/usage/IUsageStatsManager.aidl b/core/java/android/app/usage/IUsageStatsManager.aidl index 4d52263c1d78..bbae7d3463ae 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 3f348033a8aa..724a71c202f3 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 8496a961959d..b348aeef802e 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 8e1ede116abf..2ed11fe92e15 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<UserData> mUsers = new SparseArray<>(); @@ -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; @@ -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<UserData> mUserRef; protected WeakReference<ObserverAppData> 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<UsageGroup> groups = user.observedMap.get(name); if (groups == null) return; @@ -739,12 +757,22 @@ 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. * * @param name The entity that became 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<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(); diff --git a/services/usage/java/com/android/server/usage/UsageStatsService.java b/services/usage/java/com/android/server/usage/UsageStatsService.java index 57dc08fcd253..f146370b01d7 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<ArraySet<String>> mUsageReporters = new SparseArray(); + final SparseArray<String> 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<String> 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<String> 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<String> 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 000000000000..afb6e16b1fdf --- /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 000000000000..be0b09e972a5 --- /dev/null +++ b/tests/UsageReportingTest/AndroidManifest.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> + +<!-- + Note: Add android:sharedUserId="android.uid.system" to the root element to simulate the system UID + caller case. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.tests.usagereporter" + > + + <application android:label="@string/reporter_app"> + <activity android:name="UsageReporterActivity" + android:label="UsageReporter"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + </activity> + + </application> +</manifest> diff --git a/tests/UsageReportingTest/res/layout/row_item.xml b/tests/UsageReportingTest/res/layout/row_item.xml new file mode 100644 index 000000000000..1eb2dab29124 --- /dev/null +++ b/tests/UsageReportingTest/res/layout/row_item.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2018 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. +--> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="horizontal" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:paddingLeft="16dp" + android:paddingRight="16dp" + android:background="@color/inactive_color"> + + <TextView android:id="@+id/token" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_weight="1" + android:textStyle="bold"/> + + <Button android:id="@+id/start" style="@style/ActionButton" + android:text="@string/start" /> + + <Button android:id="@+id/stop" style="@style/ActionButton" + android:text="@string/stop" /> +</LinearLayout> diff --git a/tests/UsageReportingTest/res/menu/main.xml b/tests/UsageReportingTest/res/menu/main.xml new file mode 100644 index 000000000000..9847c2dce8f2 --- /dev/null +++ b/tests/UsageReportingTest/res/menu/main.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8" ?> +<!-- Copyright (C) 2018 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. +--> + +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:id="@+id/add_token" + android:title="@string/add_token"/> + <item android:id="@+id/add_many_tokens" + android:title="@string/add_many_tokens"/> + <item android:id="@+id/stop_all" + android:title="@string/stop_all_tokens"/> + <group android:checkableBehavior="all"> + <item android:id="@+id/restore_on_start" + android:title="@string/restore_tokens_on_start"/> + </group> +</menu>
\ No newline at end of file diff --git a/tests/UsageReportingTest/res/values/colors.xml b/tests/UsageReportingTest/res/values/colors.xml new file mode 100644 index 000000000000..03bcf8a60182 --- /dev/null +++ b/tests/UsageReportingTest/res/values/colors.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2018 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. +--> +<resources> + <color name="active_color">#FFF</color> + <color name="inactive_color">#AAA</color> +</resources>
\ No newline at end of file diff --git a/tests/UsageReportingTest/res/values/strings.xml b/tests/UsageReportingTest/res/values/strings.xml new file mode 100644 index 000000000000..015290e732a0 --- /dev/null +++ b/tests/UsageReportingTest/res/values/strings.xml @@ -0,0 +1,47 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2018 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. +--> + +<resources> + <!-- Do not translate --> + <string name="reporter_app">Usage Reporter App</string> + <!-- Do not translate --> + <string name="start">Start</string> + <!-- Do not translate --> + <string name="stop">Stop</string> + <!-- Do not translate --> + <string name="default_token">SuperSecretToken</string> + + <!-- Do not translate --> + <string name="add_token">Add Token</string> + <!-- Do not translate --> + <string name="add_many_tokens">Add Many Tokens</string> + <!-- Do not translate --> + <string name="stop_all_tokens">Stop All</string> + <!-- Do not translate --> + <string name="restore_tokens_on_start">Readd Tokens on Start</string> + + + <!-- Do not translate --> + <string name="token_query">Enter token(s) (delimit tokens with commas)</string> + <!-- Do not translate --> + <string name="many_tokens_query">Generate how many tokens?</string> + <!-- Do not translate --> + <string name="stop_all_tokens_query">Stop all tokens?</string> + <!-- Do not translate --> + <string name="ok">OK</string> + <!-- Do not translate --> + <string name="cancel">Cancel</string> +</resources>
\ No newline at end of file diff --git a/tests/UsageReportingTest/res/values/styles.xml b/tests/UsageReportingTest/res/values/styles.xml new file mode 100644 index 000000000000..e5b86c5e836b --- /dev/null +++ b/tests/UsageReportingTest/res/values/styles.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2018 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. +--> + +<resources> + <style name="ActionButton"> + <item name="android:layout_width">wrap_content</item> + <item name="android:layout_height">wrap_content</item> + <item name="android:textAppearance">@style/TextAppearance.ActionButton</item> + </style> + + <style name="TextAppearance" parent="android:TextAppearance"> + </style> + + <style name="TextAppearance.ActionButton"> + <item name="android:textStyle">italic</item> + </style> + +</resources> diff --git a/tests/UsageReportingTest/src/com/android/tests/usagereporter/UsageReporterActivity.java b/tests/UsageReportingTest/src/com/android/tests/usagereporter/UsageReporterActivity.java new file mode 100644 index 000000000000..946be8fe93d3 --- /dev/null +++ b/tests/UsageReportingTest/src/com/android/tests/usagereporter/UsageReporterActivity.java @@ -0,0 +1,320 @@ +/* + * Copyright (C) 2018 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.tests.usagereporter; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.ListActivity; +import android.app.usage.UsageStatsManager; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Bundle; +import android.text.InputType; +import android.text.TextUtils; +import android.util.ArraySet; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.Button; +import android.widget.EditText; +import android.widget.TextView; +import android.widget.Toast; + +import java.util.ArrayList; + +public class UsageReporterActivity extends ListActivity { + + private Activity mActivity; + private final ArrayList<String> mTokens = new ArrayList(); + private final ArraySet<String> mActives = new ArraySet(); + private UsageStatsManager mUsageStatsManager; + private Adapter mAdapter; + private boolean mRestoreOnStart = false; + private static Context sContext; + + /** Called with the activity is first created. */ + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + sContext = getApplicationContext(); + + mUsageStatsManager = (UsageStatsManager) getSystemService(Context.USAGE_STATS_SERVICE); + + mAdapter = new Adapter(); + setListAdapter(mAdapter); + } + + @Override + protected void onStart() { + super.onStart(); + mActivity = this; + + + if (mRestoreOnStart) { + ArrayList<String> removed = null; + for (String token : mActives) { + try { + mUsageStatsManager.reportUsageStart(mActivity, token); + } catch (Exception e) { + // Somthing went wrong, recover and move on + if (removed == null) { + removed = new ArrayList(); + } + removed.add(token); + } + } + if (removed != null) { + for (String token : removed) { + mActives.remove(token); + } + } + } else { + mActives.clear(); + } + } + + /** + * Called when the activity is about to start interacting with the user. + */ + @Override + protected void onResume() { + super.onResume(); + mAdapter.notifyDataSetChanged(); + } + + + /** + * Called when your activity's options menu needs to be created. + */ + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.main, menu); + return super.onCreateOptionsMenu(menu); + } + + + /** + * Called when a menu item is selected. + */ + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.add_token: + callAddToken(); + return true; + case R.id.add_many_tokens: + callAddManyTokens(); + return true; + case R.id.stop_all: + callStopAll(); + return true; + case R.id.restore_on_start: + mRestoreOnStart = !mRestoreOnStart; + item.setChecked(mRestoreOnStart); + return true; + } + + return super.onOptionsItemSelected(item); + } + + private void callAddToken() { + final AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(getString(R.string.token_query)); + final EditText input = new EditText(this); + input.setInputType(InputType.TYPE_CLASS_TEXT); + input.setHint(getString(R.string.default_token)); + builder.setView(input); + + builder.setPositiveButton(getString(R.string.ok), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + String tokenNames = input.getText().toString().trim(); + if (TextUtils.isEmpty(tokenNames)) { + tokenNames = getString(R.string.default_token); + } + String[] tokens = tokenNames.split(","); + for (String token : tokens) { + if (mTokens.contains(token)) continue; + mTokens.add(token); + } + mAdapter.notifyDataSetChanged(); + + } + }); + builder.setNegativeButton(getString(R.string.cancel), + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.cancel(); + } + }); + + builder.show(); + } + + private void callAddManyTokens() { + final AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(getString(R.string.many_tokens_query)); + final EditText input = new EditText(this); + input.setInputType(InputType.TYPE_CLASS_NUMBER); + builder.setView(input); + + builder.setPositiveButton(getString(R.string.ok), + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + String val = input.getText().toString().trim(); + if (TextUtils.isEmpty(val)) return; + int n = Integer.parseInt(val); + for (int i = 0; i < n; i++) { + final String token = getString(R.string.default_token) + i; + if (mTokens.contains(token)) continue; + mTokens.add(token); + } + mAdapter.notifyDataSetChanged(); + + } + }); + builder.setNegativeButton(getString(R.string.cancel), + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.cancel(); + } + }); + + builder.show(); + } + + private void callStopAll() { + final AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(getString(R.string.stop_all_tokens_query)); + + builder.setPositiveButton(getString(R.string.ok), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + for (String token : mActives) { + mUsageStatsManager.reportUsageStop(mActivity, token); + } + mActives.clear(); + mAdapter.notifyDataSetChanged(); + } + }); + builder.setNegativeButton(getString(R.string.cancel), + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.cancel(); + } + }); + builder.show(); + } + + /** + * A call-back for when the user presses the back button. + */ + OnClickListener mStartListener = new OnClickListener() { + public void onClick(View v) { + final View parent = (View) v.getParent(); + final String token = ((TextView) parent.findViewById(R.id.token)).getText().toString(); + try { + mUsageStatsManager.reportUsageStart(mActivity, token); + } catch (Exception e) { + Toast.makeText(sContext, e.toString(), Toast.LENGTH_LONG).show(); + } + parent.setBackgroundColor(getColor(R.color.active_color)); + mActives.add(token); + } + }; + + /** + * A call-back for when the user presses the clear button. + */ + OnClickListener mStopListener = new OnClickListener() { + public void onClick(View v) { + final View parent = (View) v.getParent(); + + final String token = ((TextView) parent.findViewById(R.id.token)).getText().toString(); + try { + mUsageStatsManager.reportUsageStop(mActivity, token); + } catch (Exception e) { + Toast.makeText(sContext, e.toString(), Toast.LENGTH_LONG).show(); + } + parent.setBackgroundColor(getColor(R.color.inactive_color)); + mActives.remove(token); + } + }; + + + private class Adapter extends BaseAdapter { + @Override + public int getCount() { + return mTokens.size(); + } + + @Override + public Object getItem(int position) { + return mTokens.get(position); + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + final ViewHolder holder; + if (convertView == null) { + convertView = LayoutInflater.from(UsageReporterActivity.this) + .inflate(R.layout.row_item, parent, false); + holder = new ViewHolder(); + holder.tokenName = (TextView) convertView.findViewById(R.id.token); + + holder.startButton = ((Button) convertView.findViewById(R.id.start)); + holder.startButton.setOnClickListener(mStartListener); + holder.stopButton = ((Button) convertView.findViewById(R.id.stop)); + holder.stopButton.setOnClickListener(mStopListener); + convertView.setTag(holder); + } else { + holder = (ViewHolder) convertView.getTag(); + } + + final String token = mTokens.get(position); + holder.tokenName.setText(mTokens.get(position)); + if (mActives.contains(token)) { + convertView.setBackgroundColor(getColor(R.color.active_color)); + } else { + convertView.setBackgroundColor(getColor(R.color.inactive_color)); + } + return convertView; + } + } + + private static class ViewHolder { + public TextView tokenName; + public Button startButton; + public Button stopButton; + } +} diff --git a/tests/UsageStatsTest/src/com/android/tests/usagestats/UsageStatsActivity.java b/tests/UsageStatsTest/src/com/android/tests/usagestats/UsageStatsActivity.java index 3d8ce21a2c00..3c628f6e0013 100644 --- a/tests/UsageStatsTest/src/com/android/tests/usagestats/UsageStatsActivity.java +++ b/tests/UsageStatsTest/src/com/android/tests/usagestats/UsageStatsActivity.java @@ -155,7 +155,7 @@ public class UsageStatsActivity extends ListActivity { intent.setPackage(getPackageName()); intent.putExtra(EXTRA_KEY_TIMEOUT, true); mUsageStatsManager.registerAppUsageObserver(1, packages, - 30, TimeUnit.SECONDS, PendingIntent.getActivity(UsageStatsActivity.this, + 60, TimeUnit.SECONDS, PendingIntent.getActivity(UsageStatsActivity.this, 1, intent, 0)); } } |