content: /**
   * 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<UserData> 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<ObserverAppData> mObserverApps = new SparseArray<>();
  
      private class UserData {
          /** userId of the user */
          private @UserIdInt
          int userId;
  
          /** Count of the currently active entities */
          public final ArrayMap<String, Integer> currentlyActive = new ArrayMap<>();
  
          /** Map from entity name for quick lookup */
          public final ArrayMap<String, ArrayList<UsageGroup>> 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<UsageGroup> 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<UsageGroup> 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<AppUsageGroup> appUsageGroups = new SparseArray<>();
  
          /** Map of observerId to details of the time limit group */
          SparseArray<SessionUsageGroup> sessionUsageGroups = new SparseArray<>();
  
          /** Map of observerId to details of the app usage limit group */
          SparseArray<AppUsageLimitGroup> 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<UserData> mUserRef;
          protected WeakReference<ObserverAppData> 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<UsageGroup> usageGroups = userData.observedMap.get(packageName);
              if (usageGroups == null || usageGroups.isEmpty()) {
                  return null;
              }
  
              final ArraySet<AppUsageLimitGroup> 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<UsageGroup> 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<UsageGroup> 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<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();
              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);
              }
          }
      }
  }
  