Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 36778525 authored by Michael Wachenschwanz's avatar Michael Wachenschwanz
Browse files

Add Usage Reporting Api to UsageStatsManager

The Usage Reporting Api allows apps to report usage within the app to
platform. Apps with the the OBSERVE_APP_USAGE permission may register
observers that use the reported in-app usage.

Test: manual (using the included Usage Reporter App)
Test: atest CtsUsageStatsTestCases:UsageReportingTest
Test: atest FrameworksServicesTests:AppTimeLimitControllerTests
Bug: 112486938

Change-Id: Iddd6f0993bbbf68a2032b34d473ef8d67da7747a
parent 183bdcf1
Loading
Loading
Loading
Loading
+3 −0
Original line number Original line Diff line number Diff line
@@ -917,6 +917,9 @@ package android.app.usage {
    method public java.util.Map<java.lang.String, java.lang.Integer> getAppStandbyBuckets();
    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 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 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 setAppStandbyBucket(java.lang.String, int);
    method public void setAppStandbyBuckets(java.util.Map<java.lang.String, java.lang.Integer>);
    method public void setAppStandbyBuckets(java.util.Map<java.lang.String, java.lang.Integer>);
    method public void unregisterAppUsageObserver(int);
    method public void unregisterAppUsageObserver(int);
+4 −0
Original line number Original line Diff line number Diff line
@@ -55,4 +55,8 @@ interface IUsageStatsManager {
            long sessionThresholdTimeMs, in PendingIntent limitReachedCallbackIntent,
            long sessionThresholdTimeMs, in PendingIntent limitReachedCallbackIntent,
            in PendingIntent sessionEndCallbackIntent, String callingPackage);
            in PendingIntent sessionEndCallbackIntent, String callingPackage);
    void unregisterUsageSessionObserver(int sessionObserverId, 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);
}
}
+95 −23
Original line number Original line Diff line number Diff line
@@ -23,6 +23,7 @@ import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.annotation.SystemApi;
import android.annotation.SystemService;
import android.annotation.SystemService;
import android.annotation.UnsupportedAppUsage;
import android.annotation.UnsupportedAppUsage;
import android.app.Activity;
import android.app.PendingIntent;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Context;
import android.content.pm.ParceledListSlice;
import android.content.pm.ParceledListSlice;
@@ -579,15 +580,18 @@ public final class UsageStatsManager {
    /**
    /**
     * @hide
     * @hide
     * Register an app usage limit observer that receives a callback on the provided intent when
     * 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
     * the sum of usages of apps and tokens in the {@code observed} array exceeds the
     * observer will automatically be unregistered when the time limit is reached and the intent
     * {@code timeLimit} specified. The structure of a token is a String with the reporting
     * is delivered. Registering an {@code observerId} that was already registered will override
     * package's name and a token the reporting app will use, separated by the forward slash
     * the previous one. No more than 1000 unique {@code observerId} may be registered by a single
     * character. Example: com.reporting.package/5OM3*0P4QU3-7OK3N
     * uid at any one time.
     * 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
     * @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.
     *                  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
     * @param observedEntities The list of packages and token to observe for usage time. Cannot be
     *                 and must include at least one package.
     *                         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
     * @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.
     *                  callbackIntent is delivered. Must be at least one minute.
     * @param timeUnit The unit for time specified in {@code timeLimit}. Cannot be null.
     * @param timeUnit The unit for time specified in {@code timeLimit}. Cannot be null.
@@ -600,11 +604,11 @@ public final class UsageStatsManager {
     */
     */
    @SystemApi
    @SystemApi
    @RequiresPermission(android.Manifest.permission.OBSERVE_APP_USAGE)
    @RequiresPermission(android.Manifest.permission.OBSERVE_APP_USAGE)
    public void registerAppUsageObserver(int observerId, @NonNull String[] packages, long timeLimit,
    public void registerAppUsageObserver(int observerId, @NonNull String[] observedEntities,
            @NonNull TimeUnit timeUnit, @NonNull PendingIntent callbackIntent) {
            long timeLimit, @NonNull TimeUnit timeUnit, @NonNull PendingIntent callbackIntent) {
        try {
        try {
            mService.registerAppUsageObserver(observerId, packages, timeUnit.toMillis(timeLimit),
            mService.registerAppUsageObserver(observerId, observedEntities,
                    callbackIntent, mContext.getOpPackageName());
                    timeUnit.toMillis(timeLimit), callbackIntent, mContext.getOpPackageName());
        } catch (RemoteException e) {
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
            throw e.rethrowFromSystemServer();
        }
        }
@@ -631,18 +635,21 @@ public final class UsageStatsManager {


    /**
    /**
     * Register a usage session observer that receives a callback on the provided {@code
     * 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
     * limitReachedCallbackIntent} when the sum of usages of apps and tokens in the {@code
     * the {@code timeLimit} specified within a usage session. After the {@code timeLimit} has
     * observed} array exceeds the {@code timeLimit} specified within a usage session. The
     * been reached, the usage session observer will receive a callback on the provided {@code
     * structure of a token is a String with the reporting packages' name and a token the
     * sessionEndCallbackIntent} when the usage session ends. Registering another session
     * reporting app will use, separated by the forward slash character.
     * observer against a {@code sessionObserverId} that has already been registered will
     * Example: com.reporting.package/5OM3*0P4QU3-7OK3N
     * override the previous session observer.
     * 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
     * @param sessionObserverId A unique id associated with the group of apps to be
     *                          monitored. There can be multiple groups with common
     *                          monitored. There can be multiple groups with common
     *                          packages and different time limits.
     *                          packages and different time limits.
     * @param packages The list of packages to observe for foreground activity time. Cannot be null
     * @param observedEntities The list of packages and token to observe for usage time. Cannot be
     *                 and must include at least one package.
     *                         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
     * @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.
     *                  limitReachedCallbackIntent} is delivered. Must be at least one minute.
     * @param timeUnit The unit for time specified in {@code timeLimit}. Cannot be null.
     * @param timeUnit The unit for time specified in {@code timeLimit}. Cannot be null.
@@ -668,13 +675,13 @@ public final class UsageStatsManager {
     */
     */
    @SystemApi
    @SystemApi
    @RequiresPermission(android.Manifest.permission.OBSERVE_APP_USAGE)
    @RequiresPermission(android.Manifest.permission.OBSERVE_APP_USAGE)
    public void registerUsageSessionObserver(int sessionObserverId, @NonNull String[] packages,
    public void registerUsageSessionObserver(int sessionObserverId,
            long timeLimit, @NonNull TimeUnit timeUnit, long sessionThresholdTime,
            @NonNull String[] observedEntities, long timeLimit, @NonNull TimeUnit timeUnit,
            @NonNull TimeUnit sessionThresholdTimeUnit,
            long sessionThresholdTime,  @NonNull TimeUnit sessionThresholdTimeUnit,
            @NonNull PendingIntent limitReachedCallbackIntent,
            @NonNull PendingIntent limitReachedCallbackIntent,
            @Nullable PendingIntent sessionEndCallbackIntent) {
            @Nullable PendingIntent sessionEndCallbackIntent) {
        try {
        try {
            mService.registerUsageSessionObserver(sessionObserverId, packages,
            mService.registerUsageSessionObserver(sessionObserverId, observedEntities,
                    timeUnit.toMillis(timeLimit),
                    timeUnit.toMillis(timeLimit),
                    sessionThresholdTimeUnit.toMillis(sessionThresholdTime),
                    sessionThresholdTimeUnit.toMillis(sessionThresholdTime),
                    limitReachedCallbackIntent, sessionEndCallbackIntent,
                    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 */
    /** @hide */
    public static String reasonToString(int standbyReason) {
    public static String reasonToString(int standbyReason) {
        StringBuilder sb = new StringBuilder();
        StringBuilder sb = new StringBuilder();
+42 −0
Original line number Original line Diff line number Diff line
@@ -699,10 +699,52 @@ public class AppTimeLimitControllerTests {
        assertTrue(hasUsageSessionObserver(UID, OBS_ID1));
        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) {
    private void startUsage(String packageName) {
        mController.noteUsageStart(packageName, USER_ID);
        mController.noteUsageStart(packageName, USER_ID);
    }
    }


    private void startPastUsage(String packageName, int timeAgo) {
        mController.noteUsageStart(packageName, USER_ID, timeAgo);
    }

    private void stopUsage(String packageName) {
    private void stopUsage(String packageName) {
        mController.noteUsageStop(packageName, USER_ID);
        mController.noteUsageStop(packageName, USER_ID);
    }
    }
+86 −25
Original line number Original line Diff line number Diff line
@@ -23,7 +23,6 @@ import android.os.Looper;
import android.os.Message;
import android.os.Message;
import android.os.SystemClock;
import android.os.SystemClock;
import android.util.ArrayMap;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Slog;
import android.util.Slog;
import android.util.SparseArray;
import android.util.SparseArray;


@@ -62,6 +61,8 @@ public class AppTimeLimitController {


    private static final long ONE_MINUTE = 60_000L;
    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 */
    /** Collection of data for each user that has reported usage */
    @GuardedBy("mLock")
    @GuardedBy("mLock")
    private final SparseArray<UserData> mUsers = new SparseArray<>();
    private final SparseArray<UserData> mUsers = new SparseArray<>();
@@ -79,11 +80,11 @@ public class AppTimeLimitController {
        private @UserIdInt
        private @UserIdInt
        int userId;
        int userId;


        /** Set of the currently active entities */
        /** Count of the currently active entities */
        private final ArraySet<String> currentlyActive = new ArraySet<>();
        public final ArrayMap<String, Integer> currentlyActive = new ArrayMap<>();


        /** Map from entity name for quick lookup */
        /** 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) {
        private UserData(@UserIdInt int userId) {
            this.userId = userId;
            this.userId = userId;
@@ -94,7 +95,7 @@ public class AppTimeLimitController {
            // TODO: Consider using a bloom filter here if number of actives becomes large
            // TODO: Consider using a bloom filter here if number of actives becomes large
            final int size = entities.length;
            final int size = entities.length;
            for (int i = 0; i < size; i++) {
            for (int i = 0; i < size; i++) {
                if (currentlyActive.contains(entities[i])) {
                if (currentlyActive.containsKey(entities[i])) {
                    return true;
                    return true;
                }
                }
            }
            }
@@ -137,7 +138,7 @@ public class AppTimeLimitController {
            pw.print(" Currently Active:");
            pw.print(" Currently Active:");
            final int nActive = currentlyActive.size();
            final int nActive = currentlyActive.size();
            for (int i = 0; i < nActive; i++) {
            for (int i = 0; i < nActive; i++) {
                pw.print(currentlyActive.valueAt(i));
                pw.print(currentlyActive.keyAt(i));
                pw.print(", ");
                pw.print(", ");
            }
            }
            pw.println();
            pw.println();
@@ -233,6 +234,7 @@ public class AppTimeLimitController {
        protected long mUsageTimeMs;
        protected long mUsageTimeMs;
        protected int mActives;
        protected int mActives;
        protected long mLastKnownUsageTimeMs;
        protected long mLastKnownUsageTimeMs;
        protected long mLastUsageEndTimeMs;
        protected WeakReference<UserData> mUserRef;
        protected WeakReference<UserData> mUserRef;
        protected WeakReference<ObserverAppData> mObserverAppRef;
        protected WeakReference<ObserverAppData> mObserverAppRef;
        protected PendingIntent mLimitReachedCallback;
        protected PendingIntent mLimitReachedCallback;
@@ -271,9 +273,15 @@ public class AppTimeLimitController {
        @GuardedBy("mLock")
        @GuardedBy("mLock")
        void noteUsageStart(long startTimeMs, long currentTimeMs) {
        void noteUsageStart(long startTimeMs, long currentTimeMs) {
            if (mActives++ == 0) {
            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;
                mLastKnownUsageTimeMs = startTimeMs;
                final long timeRemaining =
                final long timeRemaining =
                        mTimeLimitMs - mUsageTimeMs + currentTimeMs - startTimeMs;
                        mTimeLimitMs - mUsageTimeMs - currentTimeMs + startTimeMs;
                if (timeRemaining > 0) {
                if (timeRemaining > 0) {
                    if (DEBUG) {
                    if (DEBUG) {
                        Slog.d(TAG, "Posting timeout for " + mObserverId + " for "
                        Slog.d(TAG, "Posting timeout for " + mObserverId + " for "
@@ -287,7 +295,7 @@ public class AppTimeLimitController {
                    mActives = mObserved.length;
                    mActives = mObserved.length;
                    final UserData user = mUserRef.get();
                    final UserData user = mUserRef.get();
                    if (user == null) return;
                    if (user == null) return;
                    final Object[] array = user.currentlyActive.toArray();
                    final Object[] array = user.currentlyActive.keySet().toArray();
                    Slog.e(TAG,
                    Slog.e(TAG,
                            "Too many noted usage starts! Observed entities: " + Arrays.toString(
                            "Too many noted usage starts! Observed entities: " + Arrays.toString(
                                    mObserved) + "   Active Entities: " + Arrays.toString(array));
                                    mObserved) + "   Active Entities: " + Arrays.toString(array));
@@ -300,6 +308,8 @@ public class AppTimeLimitController {
            if (--mActives == 0) {
            if (--mActives == 0) {
                final boolean limitNotCrossed = mUsageTimeMs < mTimeLimitMs;
                final boolean limitNotCrossed = mUsageTimeMs < mTimeLimitMs;
                mUsageTimeMs += stopTimeMs - mLastKnownUsageTimeMs;
                mUsageTimeMs += stopTimeMs - mLastKnownUsageTimeMs;

                mLastUsageEndTimeMs = stopTimeMs;
                if (limitNotCrossed && mUsageTimeMs >= mTimeLimitMs) {
                if (limitNotCrossed && mUsageTimeMs >= mTimeLimitMs) {
                    // Crossed the limit
                    // Crossed the limit
                    if (DEBUG) Slog.d(TAG, "MTB informing group obs=" + mObserverId);
                    if (DEBUG) Slog.d(TAG, "MTB informing group obs=" + mObserverId);
@@ -312,7 +322,7 @@ public class AppTimeLimitController {
                    mActives = 0;
                    mActives = 0;
                    final UserData user = mUserRef.get();
                    final UserData user = mUserRef.get();
                    if (user == null) return;
                    if (user == null) return;
                    final Object[] array = user.currentlyActive.toArray();
                    final Object[] array = user.currentlyActive.keySet().toArray();
                    Slog.e(TAG,
                    Slog.e(TAG,
                            "Too many noted usage stops! Observed entities: " + Arrays.toString(
                            "Too many noted usage stops! Observed entities: " + Arrays.toString(
                                    mObserved) + "   Active Entities: " + Arrays.toString(array));
                                    mObserved) + "   Active Entities: " + Arrays.toString(array));
@@ -409,7 +419,6 @@ public class AppTimeLimitController {
    }
    }


    class SessionUsageGroup extends UsageGroup {
    class SessionUsageGroup extends UsageGroup {
        private long mLastUsageEndTimeMs;
        private long mNewSessionThresholdMs;
        private long mNewSessionThresholdMs;
        private PendingIntent mSessionEndCallback;
        private PendingIntent mSessionEndCallback;


@@ -451,7 +460,6 @@ public class AppTimeLimitController {
        public void noteUsageStop(long stopTimeMs) {
        public void noteUsageStop(long stopTimeMs) {
            super.noteUsageStop(stopTimeMs);
            super.noteUsageStop(stopTimeMs);
            if (mActives == 0) {
            if (mActives == 0) {
                mLastUsageEndTimeMs = stopTimeMs;
                if (mUsageTimeMs >= mTimeLimitMs) {
                if (mUsageTimeMs >= mTimeLimitMs) {
                    // Usage has ended. Schedule the session end callback to be triggered once
                    // Usage has ended. Schedule the session end callback to be triggered once
                    // the new session threshold has been reached
                    // the new session threshold has been reached
@@ -467,7 +475,10 @@ public class AppTimeLimitController {
            UserData user = mUserRef.get();
            UserData user = mUserRef.get();
            if (user == null) return;
            if (user == null) return;
            if (mListener != null) {
            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
        // TODO: Consider using a bloom filter here if number of actives becomes large
        final int size = group.mObserved.length;
        final int size = group.mObserved.length;
        for (int i = 0; i < size; i++) {
        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.
                // Entity is currently active. Start group's usage.
                group.noteUsageStart(currentTimeMs);
                group.noteUsageStart(currentTimeMs);
            }
            }
@@ -719,19 +730,26 @@ public class AppTimeLimitController {
     *
     *
     * @param name      The entity that became active
     * @param name      The entity that became active
     * @param userId    The user
     * @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) {
        synchronized (mLock) {
            UserData user = getOrCreateUserDataLocked(userId);
            UserData user = getOrCreateUserDataLocked(userId);
            if (DEBUG) Slog.d(TAG, "Usage entity " + name + " became active");
            if (DEBUG) Slog.d(TAG, "Usage entity " + name + " became active");
            if (user.currentlyActive.contains(name)) {

                throw new IllegalArgumentException(
            final int index = user.currentlyActive.indexOfKey(name);
                        "Unable to start usage for " + name + ", already in use");
            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();
            final long currentTime = getUptimeMillis();


            // Add to the list of active entities
            user.currentlyActive.put(name, ONE);
            user.currentlyActive.add(name);


            ArrayList<UsageGroup> groups = user.observedMap.get(name);
            ArrayList<UsageGroup> groups = user.observedMap.get(name);
            if (groups == null) return;
            if (groups == null) return;
@@ -739,11 +757,21 @@ public class AppTimeLimitController {
            final int size = groups.size();
            final int size = groups.size();
            for (int i = 0; i < size; i++) {
            for (int i = 0; i < size; i++) {
                UsageGroup group = groups.get(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.
     * Called when an entity becomes inactive.
     *
     *
@@ -754,10 +782,21 @@ public class AppTimeLimitController {
        synchronized (mLock) {
        synchronized (mLock) {
            UserData user = getOrCreateUserDataLocked(userId);
            UserData user = getOrCreateUserDataLocked(userId);
            if (DEBUG) Slog.d(TAG, "Usage entity " + name + " became inactive");
            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(
                throw new IllegalArgumentException(
                        "Unable to stop usage for " + name + ", not in use");
                        "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();
            final long currentTime = getUptimeMillis();


            // Check if any of the groups need to watch for this entity
            // Check if any of the groups need to watch for this entity
@@ -769,6 +808,7 @@ public class AppTimeLimitController {
                UsageGroup group = groups.get(i);
                UsageGroup group = groups.get(i);
                group.noteUsageStop(currentTime);
                group.noteUsageStop(currentTime);
            }
            }

        }
        }
    }
    }


@@ -780,7 +820,8 @@ public class AppTimeLimitController {


    @GuardedBy("mLock")
    @GuardedBy("mLock")
    private void postInformSessionEndListenerLocked(SessionUsageGroup group, long timeout) {
    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);
                timeout);
    }
    }


@@ -800,7 +841,27 @@ public class AppTimeLimitController {
        mHandler.removeMessages(MyHandler.MSG_CHECK_TIMEOUT, group);
        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) {
        synchronized (mLock) {
            pw.println("\n  App Time Limits");
            pw.println("\n  App Time Limits");
            final int nUsers = mUsers.size();
            final int nUsers = mUsers.size();
Loading