/** * 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.server.usage; import android.annotation.UserIdInt; import android.app.PendingIntent; import android.app.usage.UsageStatsManagerInternal; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.SystemClock; import android.os.UserHandle; import android.util.ArrayMap; import android.util.ArraySet; import android.util.Slog; import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import java.io.PrintWriter; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Arrays; /** * Monitors and informs of any app time limits exceeded. It must be informed when an app * enters the foreground and exits. Used by UsageStatsService. Manages multiple users. * * Test: atest FrameworksServicesTests:AppTimeLimitControllerTests * Test: manual: frameworks/base/tests/UsageStatsTest */ public class AppTimeLimitController { private static final String TAG = "AppTimeLimitController"; private static final boolean DEBUG = false; /** Lock class for this object */ private static class Lock {} /** Lock object for the data in this class. */ private final Lock mLock = new Lock(); private final MyHandler mHandler; private TimeLimitCallbackListener mListener; private static final long MAX_OBSERVER_PER_UID = 1000; private static final long ONE_MINUTE = 60_000L; private static final Integer ONE = new Integer(1); /** Collection of data for each user that has reported usage */ @GuardedBy("mLock") private final SparseArray mUsers = new SparseArray<>(); /** * Collection of data for each app that is registering observers * WARNING: Entries are currently not removed, based on the assumption there are a small * fixed number of apps on device that can register observers. */ @GuardedBy("mLock") private final SparseArray mObserverApps = new SparseArray<>(); private class UserData { /** userId of the user */ private @UserIdInt int userId; /** Count of the currently active entities */ public final ArrayMap currentlyActive = new ArrayMap<>(); /** Map from entity name for quick lookup */ public final ArrayMap> observedMap = new ArrayMap<>(); private UserData(@UserIdInt int userId) { this.userId = userId; } @GuardedBy("mLock") boolean isActive(String[] entities) { // 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.containsKey(entities[i])) { return true; } } return false; } @GuardedBy("mLock") void addUsageGroup(UsageGroup group) { final int size = group.mObserved.length; for (int i = 0; i < size; i++) { ArrayList list = observedMap.get(group.mObserved[i]); if (list == null) { list = new ArrayList<>(); observedMap.put(group.mObserved[i], list); } list.add(group); } } @GuardedBy("mLock") void removeUsageGroup(UsageGroup group) { final int size = group.mObserved.length; for (int i = 0; i < size; i++) { final String observed = group.mObserved[i]; final ArrayList list = observedMap.get(observed); if (list != null) { list.remove(group); if (list.isEmpty()) { // No more observers for this observed entity, remove from map observedMap.remove(observed); } } } } @GuardedBy("mLock") void dump(PrintWriter pw) { pw.print(" userId="); pw.println(userId); pw.print(" Currently Active:"); final int nActive = currentlyActive.size(); for (int i = 0; i < nActive; i++) { pw.print(currentlyActive.keyAt(i)); pw.print(", "); } pw.println(); pw.print(" Observed Entities:"); final int nEntities = observedMap.size(); for (int i = 0; i < nEntities; i++) { pw.print(observedMap.keyAt(i)); pw.print(", "); } pw.println(); } } private class ObserverAppData { /** uid of the observing app */ private int uid; /** Map of observerId to details of the time limit group */ SparseArray appUsageGroups = new SparseArray<>(); /** Map of observerId to details of the time limit group */ SparseArray sessionUsageGroups = new SparseArray<>(); /** Map of observerId to details of the app usage limit group */ SparseArray appUsageLimitGroups = new SparseArray<>(); private ObserverAppData(int uid) { this.uid = uid; } @GuardedBy("mLock") void removeAppUsageGroup(int observerId) { appUsageGroups.remove(observerId); } @GuardedBy("mLock") void removeSessionUsageGroup(int observerId) { sessionUsageGroups.remove(observerId); } @GuardedBy("mLock") void removeAppUsageLimitGroup(int observerId) { appUsageLimitGroups.remove(observerId); } @GuardedBy("mLock") void dump(PrintWriter pw) { pw.print(" uid="); pw.println(uid); pw.println(" App Usage Groups:"); final int nAppUsageGroups = appUsageGroups.size(); for (int i = 0; i < nAppUsageGroups; i++) { appUsageGroups.valueAt(i).dump(pw); pw.println(); } pw.println(" Session Usage Groups:"); final int nSessionUsageGroups = sessionUsageGroups.size(); for (int i = 0; i < nSessionUsageGroups; i++) { sessionUsageGroups.valueAt(i).dump(pw); pw.println(); } pw.println(" App Usage Limit Groups:"); final int nAppUsageLimitGroups = appUsageLimitGroups.size(); for (int i = 0; i < nAppUsageLimitGroups; i++) { appUsageLimitGroups.valueAt(i).dump(pw); pw.println(); } } } /** * Listener interface for being informed when an app group's time limit is reached. */ public interface TimeLimitCallbackListener { /** * Time limit for a group, keyed by the observerId, has been reached. * * @param observerId The observerId of the group whose limit was reached * @param userId The userId * @param timeLimit The original time limit in milliseconds * @param timeElapsed How much time was actually spent on apps in the group, in * milliseconds * @param callbackIntent The PendingIntent to send when the limit is reached */ public void onLimitReached(int observerId, @UserIdInt int userId, long timeLimit, long timeElapsed, PendingIntent callbackIntent); /** * Session ended for a group, keyed by the observerId, after limit was reached. * * @param observerId The observerId of the group whose limit was reached * @param userId The userId * @param timeElapsed How much time was actually spent on apps in the group, in * milliseconds * @param callbackIntent The PendingIntent to send when the limit is reached */ public void onSessionEnd(int observerId, @UserIdInt int userId, long timeElapsed, PendingIntent callbackIntent); } abstract class UsageGroup { protected int mObserverId; protected String[] mObserved; protected long mTimeLimitMs; protected long mUsageTimeMs; protected int mActives; protected long mLastKnownUsageTimeMs; protected long mLastUsageEndTimeMs; protected WeakReference mUserRef; protected WeakReference mObserverAppRef; protected PendingIntent mLimitReachedCallback; UsageGroup(UserData user, ObserverAppData observerApp, int observerId, String[] observed, long timeLimitMs, PendingIntent limitReachedCallback) { mUserRef = new WeakReference<>(user); mObserverAppRef = new WeakReference<>(observerApp); mObserverId = observerId; mObserved = observed; mTimeLimitMs = timeLimitMs; mLimitReachedCallback = limitReachedCallback; } @GuardedBy("mLock") public long getTimeLimitMs() { return mTimeLimitMs; } @GuardedBy("mLock") public long getUsageTimeMs() { return mUsageTimeMs; } @GuardedBy("mLock") public void remove() { UserData user = mUserRef.get(); if (user != null) { user.removeUsageGroup(this); } // Clear the callback, so any racy inflight message will do nothing mLimitReachedCallback = null; } @GuardedBy("mLock") void noteUsageStart(long startTimeMs) { noteUsageStart(startTimeMs, startTimeMs); } @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; if (timeRemaining > 0) { if (DEBUG) { Slog.d(TAG, "Posting timeout for " + mObserverId + " for " + timeRemaining + "ms"); } postCheckTimeoutLocked(this, timeRemaining); } } else { if (mActives > mObserved.length) { // Try to get to a sane state and log the issue mActives = mObserved.length; final UserData user = mUserRef.get(); if (user == null) return; 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)); } } } @GuardedBy("mLock") void noteUsageStop(long stopTimeMs) { 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); postInformLimitReachedListenerLocked(this); } cancelCheckTimeoutLocked(this); } else { if (mActives < 0) { // Try to get to a sane state and log the issue mActives = 0; final UserData user = mUserRef.get(); if (user == null) return; 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)); } } } @GuardedBy("mLock") void checkTimeout(long currentTimeMs) { final UserData user = mUserRef.get(); if (user == null) return; long timeRemainingMs = mTimeLimitMs - mUsageTimeMs; if (DEBUG) Slog.d(TAG, "checkTimeout timeRemaining=" + timeRemainingMs); // Already reached the limit, no need to report again if (timeRemainingMs <= 0) return; if (DEBUG) { Slog.d(TAG, "checkTimeout"); } // Double check that at least one entity in this group is currently active if (user.isActive(mObserved)) { if (DEBUG) { Slog.d(TAG, "checkTimeout group is active"); } final long timeUsedMs = currentTimeMs - mLastKnownUsageTimeMs; if (timeRemainingMs <= timeUsedMs) { if (DEBUG) Slog.d(TAG, "checkTimeout : Time limit reached"); // Hit the limit, set timeRemaining to zero to avoid checking again mUsageTimeMs += timeUsedMs; mLastKnownUsageTimeMs = currentTimeMs; AppTimeLimitController.this.postInformLimitReachedListenerLocked(this); } else { if (DEBUG) Slog.d(TAG, "checkTimeout : Some more time remaining"); AppTimeLimitController.this.postCheckTimeoutLocked(this, timeRemainingMs - timeUsedMs); } } } @GuardedBy("mLock") public void onLimitReached() { UserData user = mUserRef.get(); if (user == null) return; if (mListener != null) { mListener.onLimitReached(mObserverId, user.userId, mTimeLimitMs, mUsageTimeMs, mLimitReachedCallback); } } @GuardedBy("mLock") void dump(PrintWriter pw) { pw.print(" Group id="); pw.print(mObserverId); pw.print(" timeLimit="); pw.print(mTimeLimitMs); pw.print(" used="); pw.print(mUsageTimeMs); pw.print(" lastKnownUsage="); pw.print(mLastKnownUsageTimeMs); pw.print(" mActives="); pw.print(mActives); pw.print(" observed="); pw.print(Arrays.toString(mObserved)); } } class AppUsageGroup extends UsageGroup { public AppUsageGroup(UserData user, ObserverAppData observerApp, int observerId, String[] observed, long timeLimitMs, PendingIntent limitReachedCallback) { super(user, observerApp, observerId, observed, timeLimitMs, limitReachedCallback); } @Override @GuardedBy("mLock") public void remove() { super.remove(); ObserverAppData observerApp = mObserverAppRef.get(); if (observerApp != null) { observerApp.removeAppUsageGroup(mObserverId); } } @Override @GuardedBy("mLock") public void onLimitReached() { super.onLimitReached(); // Unregister since the limit has been met and observer was informed. remove(); } } class SessionUsageGroup extends UsageGroup { private long mNewSessionThresholdMs; private PendingIntent mSessionEndCallback; public SessionUsageGroup(UserData user, ObserverAppData observerApp, int observerId, String[] observed, long timeLimitMs, PendingIntent limitReachedCallback, long newSessionThresholdMs, PendingIntent sessionEndCallback) { super(user, observerApp, observerId, observed, timeLimitMs, limitReachedCallback); this.mNewSessionThresholdMs = newSessionThresholdMs; this.mSessionEndCallback = sessionEndCallback; } @Override @GuardedBy("mLock") public void remove() { super.remove(); ObserverAppData observerApp = mObserverAppRef.get(); if (observerApp != null) { observerApp.removeSessionUsageGroup(mObserverId); } // Clear the callback, so any racy inflight messages will do nothing mSessionEndCallback = null; } @Override @GuardedBy("mLock") public void noteUsageStart(long startTimeMs, long currentTimeMs) { if (mActives == 0) { if (startTimeMs - mLastUsageEndTimeMs > mNewSessionThresholdMs) { // New session has started, clear usage time. mUsageTimeMs = 0; } AppTimeLimitController.this.cancelInformSessionEndListener(this); } super.noteUsageStart(startTimeMs, currentTimeMs); } @Override @GuardedBy("mLock") public void noteUsageStop(long stopTimeMs) { super.noteUsageStop(stopTimeMs); if (mActives == 0) { if (mUsageTimeMs >= mTimeLimitMs) { // Usage has ended. Schedule the session end callback to be triggered once // the new session threshold has been reached AppTimeLimitController.this.postInformSessionEndListenerLocked(this, mNewSessionThresholdMs); } } } @GuardedBy("mLock") public void onSessionEnd() { UserData user = mUserRef.get(); if (user == null) return; if (mListener != null) { mListener.onSessionEnd(mObserverId, user.userId, mUsageTimeMs, mSessionEndCallback); } } @Override @GuardedBy("mLock") void dump(PrintWriter pw) { super.dump(pw); pw.print(" lastUsageEndTime="); pw.print(mLastUsageEndTimeMs); pw.print(" newSessionThreshold="); pw.print(mNewSessionThresholdMs); } } class AppUsageLimitGroup extends UsageGroup { public AppUsageLimitGroup(UserData user, ObserverAppData observerApp, int observerId, String[] observed, long timeLimitMs, long timeUsedMs, PendingIntent limitReachedCallback) { super(user, observerApp, observerId, observed, timeLimitMs, limitReachedCallback); mUsageTimeMs = timeUsedMs; } @Override @GuardedBy("mLock") public void remove() { super.remove(); ObserverAppData observerApp = mObserverAppRef.get(); if (observerApp != null) { observerApp.removeAppUsageLimitGroup(mObserverId); } } @GuardedBy("mLock") long getTotaUsageLimit() { return mTimeLimitMs; } @GuardedBy("mLock") long getUsageRemaining() { // If there is currently an active session, account for its usage if (mActives > 0) { return mTimeLimitMs - mUsageTimeMs - (getUptimeMillis() - mLastKnownUsageTimeMs); } else { return mTimeLimitMs - mUsageTimeMs; } } } private class MyHandler extends Handler { static final int MSG_CHECK_TIMEOUT = 1; static final int MSG_INFORM_LIMIT_REACHED_LISTENER = 2; static final int MSG_INFORM_SESSION_END = 3; MyHandler(Looper looper) { super(looper); } @Override public void handleMessage(Message msg) { switch (msg.what) { case MSG_CHECK_TIMEOUT: synchronized (mLock) { ((UsageGroup) msg.obj).checkTimeout(getUptimeMillis()); } break; case MSG_INFORM_LIMIT_REACHED_LISTENER: synchronized (mLock) { ((UsageGroup) msg.obj).onLimitReached(); } break; case MSG_INFORM_SESSION_END: synchronized (mLock) { ((SessionUsageGroup) msg.obj).onSessionEnd(); } break; default: super.handleMessage(msg); break; } } } public AppTimeLimitController(TimeLimitCallbackListener listener, Looper looper) { mHandler = new MyHandler(looper); mListener = listener; } /** Overrideable by a test */ @VisibleForTesting protected long getUptimeMillis() { return SystemClock.uptimeMillis(); } /** Overrideable for testing purposes */ @VisibleForTesting protected long getAppUsageObserverPerUidLimit() { return MAX_OBSERVER_PER_UID; } /** Overrideable for testing purposes */ @VisibleForTesting protected long getUsageSessionObserverPerUidLimit() { return MAX_OBSERVER_PER_UID; } /** Overrideable for testing purposes */ @VisibleForTesting protected long getAppUsageLimitObserverPerUidLimit() { return MAX_OBSERVER_PER_UID; } /** Overrideable for testing purposes */ @VisibleForTesting protected long getMinTimeLimit() { return ONE_MINUTE; } @VisibleForTesting AppUsageGroup getAppUsageGroup(int observerAppUid, int observerId) { synchronized (mLock) { return getOrCreateObserverAppDataLocked(observerAppUid).appUsageGroups.get(observerId); } } @VisibleForTesting SessionUsageGroup getSessionUsageGroup(int observerAppUid, int observerId) { synchronized (mLock) { return getOrCreateObserverAppDataLocked(observerAppUid).sessionUsageGroups.get( observerId); } } @VisibleForTesting AppUsageLimitGroup getAppUsageLimitGroup(int observerAppUid, int observerId) { synchronized (mLock) { return getOrCreateObserverAppDataLocked(observerAppUid).appUsageLimitGroups.get( observerId); } } /** * Returns an object describing the app usage limit for the given package which was set via * {@link #addAppUsageLimitObserver). * If there are multiple limits that apply to the package, the one with the smallest * time remaining will be returned. */ public UsageStatsManagerInternal.AppUsageLimitData getAppUsageLimit( String packageName, UserHandle user) { synchronized (mLock) { final UserData userData = getOrCreateUserDataLocked(user.getIdentifier()); if (userData == null) { return null; } final ArrayList usageGroups = userData.observedMap.get(packageName); if (usageGroups == null || usageGroups.isEmpty()) { return null; } final ArraySet usageLimitGroups = new ArraySet<>(); for (int i = 0; i < usageGroups.size(); i++) { if (usageGroups.get(i) instanceof AppUsageLimitGroup) { final AppUsageLimitGroup group = (AppUsageLimitGroup) usageGroups.get(i); for (int j = 0; j < group.mObserved.length; j++) { if (group.mObserved[j].equals(packageName)) { usageLimitGroups.add(group); break; } } } } if (usageLimitGroups.isEmpty()) { return null; } AppUsageLimitGroup smallestGroup = usageLimitGroups.valueAt(0); for (int i = 1; i < usageLimitGroups.size(); i++) { final AppUsageLimitGroup otherGroup = usageLimitGroups.valueAt(i); if (otherGroup.getUsageRemaining() < smallestGroup.getUsageRemaining()) { smallestGroup = otherGroup; } } return new UsageStatsManagerInternal.AppUsageLimitData( smallestGroup.getTotaUsageLimit(), smallestGroup.getUsageRemaining()); } } /** Returns an existing UserData object for the given userId, or creates one */ @GuardedBy("mLock") private UserData getOrCreateUserDataLocked(int userId) { UserData userData = mUsers.get(userId); if (userData == null) { userData = new UserData(userId); mUsers.put(userId, userData); } return userData; } /** Returns an existing ObserverAppData object for the given uid, or creates one */ @GuardedBy("mLock") private ObserverAppData getOrCreateObserverAppDataLocked(int uid) { ObserverAppData appData = mObserverApps.get(uid); if (appData == null) { appData = new ObserverAppData(uid); mObserverApps.put(uid, appData); } return appData; } /** Clean up data if user is removed */ public void onUserRemoved(int userId) { synchronized (mLock) { // TODO: Remove any inflight delayed messages mUsers.remove(userId); } } /** * Check if group has any currently active entities. */ @GuardedBy("mLock") private void noteActiveLocked(UserData user, UsageGroup group, long currentTimeMs) { // 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.containsKey(group.mObserved[i])) { // Entity is currently active. Start group's usage. group.noteUsageStart(currentTimeMs); } } } /** * Registers an app usage observer with the given details. * Existing app usage observer with the same observerId will be removed. */ public void addAppUsageObserver(int requestingUid, int observerId, String[] observed, long timeLimit, PendingIntent callbackIntent, @UserIdInt int userId) { if (timeLimit < getMinTimeLimit()) { throw new IllegalArgumentException("Time limit must be >= " + getMinTimeLimit()); } synchronized (mLock) { UserData user = getOrCreateUserDataLocked(userId); ObserverAppData observerApp = getOrCreateObserverAppDataLocked(requestingUid); AppUsageGroup group = observerApp.appUsageGroups.get(observerId); if (group != null) { // Remove previous app usage group associated with observerId group.remove(); } final int observerIdCount = observerApp.appUsageGroups.size(); if (observerIdCount >= getAppUsageObserverPerUidLimit()) { throw new IllegalStateException( "Too many app usage observers added by uid " + requestingUid); } group = new AppUsageGroup(user, observerApp, observerId, observed, timeLimit, callbackIntent); observerApp.appUsageGroups.append(observerId, group); if (DEBUG) { Slog.d(TAG, "addObserver " + observed + " for " + timeLimit); } user.addUsageGroup(group); noteActiveLocked(user, group, getUptimeMillis()); } } /** * Remove a registered observer by observerId and calling uid. * * @param requestingUid The calling uid * @param observerId The unique observer id for this user * @param userId The user id of the observer */ public void removeAppUsageObserver(int requestingUid, int observerId, @UserIdInt int userId) { synchronized (mLock) { final ObserverAppData observerApp = getOrCreateObserverAppDataLocked(requestingUid); final AppUsageGroup group = observerApp.appUsageGroups.get(observerId); if (group != null) { // Remove previous app usage group associated with observerId group.remove(); } } } /** * Registers a usage session observer with the given details. * Existing usage session observer with the same observerId will be removed. */ public void addUsageSessionObserver(int requestingUid, int observerId, String[] observed, long timeLimit, long sessionThresholdTime, PendingIntent limitReachedCallbackIntent, PendingIntent sessionEndCallbackIntent, @UserIdInt int userId) { if (timeLimit < getMinTimeLimit()) { throw new IllegalArgumentException("Time limit must be >= " + getMinTimeLimit()); } synchronized (mLock) { UserData user = getOrCreateUserDataLocked(userId); ObserverAppData observerApp = getOrCreateObserverAppDataLocked(requestingUid); SessionUsageGroup group = observerApp.sessionUsageGroups.get(observerId); if (group != null) { // Remove previous session usage group associated with observerId group.remove(); } final int observerIdCount = observerApp.sessionUsageGroups.size(); if (observerIdCount >= getUsageSessionObserverPerUidLimit()) { throw new IllegalStateException( "Too many app usage observers added by uid " + requestingUid); } group = new SessionUsageGroup(user, observerApp, observerId, observed, timeLimit, limitReachedCallbackIntent, sessionThresholdTime, sessionEndCallbackIntent); observerApp.sessionUsageGroups.append(observerId, group); user.addUsageGroup(group); noteActiveLocked(user, group, getUptimeMillis()); } } /** * Remove a registered observer by observerId and calling uid. * * @param requestingUid The calling uid * @param observerId The unique observer id for this user * @param userId The user id of the observer */ public void removeUsageSessionObserver(int requestingUid, int observerId, @UserIdInt int userId) { synchronized (mLock) { final ObserverAppData observerApp = getOrCreateObserverAppDataLocked(requestingUid); final SessionUsageGroup group = observerApp.sessionUsageGroups.get(observerId); if (group != null) { // Remove previous app usage group associated with observerId group.remove(); } } } /** * Registers an app usage limit observer with the given details. * Existing app usage limit observer with the same observerId will be removed. */ public void addAppUsageLimitObserver(int requestingUid, int observerId, String[] observed, long timeLimit, long timeUsed, PendingIntent callbackIntent, @UserIdInt int userId) { if (timeLimit < getMinTimeLimit()) { throw new IllegalArgumentException("Time limit must be >= " + getMinTimeLimit()); } synchronized (mLock) { UserData user = getOrCreateUserDataLocked(userId); ObserverAppData observerApp = getOrCreateObserverAppDataLocked(requestingUid); AppUsageLimitGroup group = observerApp.appUsageLimitGroups.get(observerId); if (group != null) { // Remove previous app usage group associated with observerId group.remove(); } final int observerIdCount = observerApp.appUsageLimitGroups.size(); if (observerIdCount >= getAppUsageLimitObserverPerUidLimit()) { throw new IllegalStateException( "Too many app usage observers added by uid " + requestingUid); } group = new AppUsageLimitGroup(user, observerApp, observerId, observed, timeLimit, timeUsed, timeUsed >= timeLimit ? null : callbackIntent); observerApp.appUsageLimitGroups.append(observerId, group); if (DEBUG) { Slog.d(TAG, "addObserver " + observed + " for " + timeLimit); } user.addUsageGroup(group); noteActiveLocked(user, group, getUptimeMillis()); } } /** * Remove a registered observer by observerId and calling uid. * * @param requestingUid The calling uid * @param observerId The unique observer id for this user * @param userId The user id of the observer */ public void removeAppUsageLimitObserver(int requestingUid, int observerId, @UserIdInt int userId) { synchronized (mLock) { final ObserverAppData observerApp = getOrCreateObserverAppDataLocked(requestingUid); final AppUsageLimitGroup group = observerApp.appUsageLimitGroups.get(observerId); if (group != null) { // Remove previous app usage group associated with observerId group.remove(); } } } /** * Called when an entity becomes active. * * @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, long timeAgoMs) throws IllegalArgumentException { synchronized (mLock) { UserData user = getOrCreateUserDataLocked(userId); if (DEBUG) Slog.d(TAG, "Usage entity " + name + " became active"); 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(); user.currentlyActive.put(name, ONE); ArrayList groups = user.observedMap.get(name); if (groups == null) return; final int size = groups.size(); for (int i = 0; i < size; i++) { UsageGroup group = groups.get(i); 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 * @param userId The user */ public void noteUsageStop(String name, int userId) throws IllegalArgumentException { synchronized (mLock) { UserData user = getOrCreateUserDataLocked(userId); if (DEBUG) Slog.d(TAG, "Usage entity " + name + " became inactive"); 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 ArrayList groups = user.observedMap.get(name); if (groups == null) return; final int size = groups.size(); for (int i = 0; i < size; i++) { UsageGroup group = groups.get(i); group.noteUsageStop(currentTime); } } } @GuardedBy("mLock") private void postInformLimitReachedListenerLocked(UsageGroup group) { mHandler.sendMessage(mHandler.obtainMessage(MyHandler.MSG_INFORM_LIMIT_REACHED_LISTENER, group)); } @GuardedBy("mLock") private void postInformSessionEndListenerLocked(SessionUsageGroup group, long timeout) { mHandler.sendMessageDelayed( mHandler.obtainMessage(MyHandler.MSG_INFORM_SESSION_END, group), timeout); } @GuardedBy("mLock") private void cancelInformSessionEndListener(SessionUsageGroup group) { mHandler.removeMessages(MyHandler.MSG_INFORM_SESSION_END, group); } @GuardedBy("mLock") private void postCheckTimeoutLocked(UsageGroup group, long timeout) { mHandler.sendMessageDelayed(mHandler.obtainMessage(MyHandler.MSG_CHECK_TIMEOUT, group), timeout); } @GuardedBy("mLock") private void cancelCheckTimeoutLocked(UsageGroup group) { mHandler.removeMessages(MyHandler.MSG_CHECK_TIMEOUT, group); } void dump(String[] args, PrintWriter pw) { if (args != null) { for (int i = 0; i < args.length; i++) { String arg = args[i]; if ("actives".equals(arg)) { synchronized (mLock) { final int nUsers = mUsers.size(); for (int user = 0; user < nUsers; user++) { final ArrayMap actives = mUsers.valueAt(user).currentlyActive; final int nActive = actives.size(); for (int active = 0; active < nActive; active++) { pw.println(actives.keyAt(active)); } } } return; } } } synchronized (mLock) { pw.println("\n App Time Limits"); final int nUsers = mUsers.size(); for (int i = 0; i < nUsers; i++) { pw.print(" User "); mUsers.valueAt(i).dump(pw); } pw.println(); final int nObserverApps = mObserverApps.size(); for (int i = 0; i < nObserverApps; i++) { pw.print(" Observer App "); mObserverApps.valueAt(i).dump(pw); } } } }