Merge "App Time Limits API in UsageStats" into pi-dev
diff --git a/api/system-current.txt b/api/system-current.txt
index 41d57bc..1080669 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -729,9 +729,14 @@
   public final class UsageStatsManager {
     method public int getAppStandbyBucket(java.lang.String);
     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 setAppStandbyBucket(java.lang.String, int);
     method public void setAppStandbyBuckets(java.util.Map<java.lang.String, java.lang.Integer>);
+    method public void unregisterAppUsageObserver(int);
     method public void whitelistAppTemporarily(java.lang.String, long, android.os.UserHandle);
+    field public static final java.lang.String EXTRA_OBSERVER_ID = "android.app.usage.extra.OBSERVER_ID";
+    field public static final java.lang.String EXTRA_TIME_LIMIT = "android.app.usage.extra.TIME_LIMIT";
+    field public static final java.lang.String EXTRA_TIME_USED = "android.app.usage.extra.TIME_USED";
     field public static final int STANDBY_BUCKET_EXEMPTED = 5; // 0x5
     field public static final int STANDBY_BUCKET_NEVER = 50; // 0x32
   }
diff --git a/core/java/android/app/usage/IUsageStatsManager.aidl b/core/java/android/app/usage/IUsageStatsManager.aidl
index fff1a00..d52bd37 100644
--- a/core/java/android/app/usage/IUsageStatsManager.aidl
+++ b/core/java/android/app/usage/IUsageStatsManager.aidl
@@ -16,6 +16,7 @@
 
 package android.app.usage;
 
+import android.app.PendingIntent;
 import android.app.usage.UsageEvents;
 import android.content.pm.ParceledListSlice;
 
@@ -43,4 +44,7 @@
     void setAppStandbyBucket(String packageName, int bucket, int userId);
     ParceledListSlice getAppStandbyBuckets(String callingPackage, int userId);
     void setAppStandbyBuckets(in ParceledListSlice appBuckets, int userId);
+    void registerAppUsageObserver(int observerId, in String[] packages, long timeLimitMs,
+            in PendingIntent callback, String callingPackage);
+    void unregisterAppUsageObserver(int observerId, String callingPackage);
 }
diff --git a/core/java/android/app/usage/UsageStatsManager.java b/core/java/android/app/usage/UsageStatsManager.java
index 5f9fa43..59f001c 100644
--- a/core/java/android/app/usage/UsageStatsManager.java
+++ b/core/java/android/app/usage/UsageStatsManager.java
@@ -20,6 +20,7 @@
 import android.annotation.RequiresPermission;
 import android.annotation.SystemApi;
 import android.annotation.SystemService;
+import android.app.PendingIntent;
 import android.content.Context;
 import android.content.pm.ParceledListSlice;
 import android.os.RemoteException;
@@ -32,6 +33,7 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.TimeUnit;
 
 /**
  * Provides access to device usage history and statistics. Usage data is aggregated into
@@ -179,6 +181,31 @@
     @Retention(RetentionPolicy.SOURCE)
     public @interface StandbyBuckets {}
 
+    /**
+     * Observer id of the registered observer for the group of packages that reached the usage
+     * time limit. Included as an extra in the PendingIntent that was registered.
+     * @hide
+     */
+    @SystemApi
+    public static final String EXTRA_OBSERVER_ID = "android.app.usage.extra.OBSERVER_ID";
+
+    /**
+     * Original time limit in milliseconds specified by the registered observer for the group of
+     * packages that reached the usage time limit. Included as an extra in the PendingIntent that
+     * was registered.
+     * @hide
+     */
+    @SystemApi
+    public static final String EXTRA_TIME_LIMIT = "android.app.usage.extra.TIME_LIMIT";
+
+    /**
+     * Actual usage time in milliseconds for the group of packages that reached the specified time
+     * limit. Included as an extra in the PendingIntent that was registered.
+     * @hide
+     */
+    @SystemApi
+    public static final String EXTRA_TIME_USED = "android.app.usage.extra.TIME_USED";
+
     private static final UsageEvents sEmptyResults = new UsageEvents();
 
     private final Context mContext;
@@ -470,6 +497,53 @@
         }
     }
 
+    /**
+     * @hide
+     * Register an app usage limit observer that receives a callback on the provided intent when
+     * the sum of usages of apps in the packages array exceeds the timeLimit specified. The
+     * observer will automatically be unregistered when the time limit is reached and the intent
+     * is delivered.
+     * @param observerId A unique id associated with the group of apps to be monitored. There can
+     *                  be multiple groups with common packages and different time limits.
+     * @param packages The list of packages to observe for foreground activity time. Must include
+     *                 at least one package.
+     * @param timeLimit The total time the set of apps can be in the foreground before the
+     *                  callbackIntent is delivered. Must be greater than 0.
+     * @param timeUnit The unit for time specified in timeLimit.
+     * @param callbackIntent The PendingIntent that will be dispatched when the time limit is
+     *                       exceeded by the group of apps. The delivered Intent will also contain
+     *                       the extras {@link #EXTRA_OBSERVER_ID}, {@link #EXTRA_TIME_LIMIT} and
+     *                       {@link #EXTRA_TIME_USED}.
+     * @throws SecurityException if the caller doesn't have the PACKAGE_USAGE_STATS permission.
+     */
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.PACKAGE_USAGE_STATS)
+    public void registerAppUsageObserver(int observerId, String[] packages, long timeLimit,
+            TimeUnit timeUnit, PendingIntent callbackIntent) {
+        try {
+            mService.registerAppUsageObserver(observerId, packages, timeUnit.toMillis(timeLimit),
+                    callbackIntent, mContext.getOpPackageName());
+        } catch (RemoteException e) {
+        }
+    }
+
+    /**
+     * @hide
+     * Unregister the app usage observer specified by the observerId. This will only apply to any
+     * observer registered by this application. Unregistering an observer that was already
+     * unregistered or never registered will have no effect.
+     * @param observerId The id of the observer that was previously registered.
+     * @throws SecurityException if the caller doesn't have the PACKAGE_USAGE_STATS permission.
+     */
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.PACKAGE_USAGE_STATS)
+    public void unregisterAppUsageObserver(int observerId) {
+        try {
+            mService.unregisterAppUsageObserver(observerId, mContext.getOpPackageName());
+        } catch (RemoteException e) {
+        }
+    }
+
     /** @hide */
     public static String reasonToString(int standbyReason) {
         StringBuilder sb = new StringBuilder();
diff --git a/services/tests/servicestests/src/com/android/server/usage/AppTimeLimitControllerTests.java b/services/tests/servicestests/src/com/android/server/usage/AppTimeLimitControllerTests.java
new file mode 100644
index 0000000..6b52ee5
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/usage/AppTimeLimitControllerTests.java
@@ -0,0 +1,256 @@
+/*
+ * 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 static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.app.PendingIntent;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.support.test.filters.MediumTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(AndroidJUnit4.class)
+@MediumTest
+public class AppTimeLimitControllerTests {
+
+    private static final String PKG_SOC1 = "package.soc1";
+    private static final String PKG_SOC2 = "package.soc2";
+    private static final String PKG_GAME1 = "package.game1";
+    private static final String PKG_GAME2 = "package.game2";
+    private static final String PKG_PROD = "package.prod";
+
+    private static final int UID = 10100;
+    private static final int USER_ID = 10;
+    private static final int OBS_ID1 = 1;
+    private static final int OBS_ID2 = 2;
+    private static final int OBS_ID3 = 3;
+
+    private static final long TIME_30_MIN = 30 * 60_1000L;
+    private static final long TIME_10_MIN = 10 * 60_1000L;
+
+    private static final String[] GROUP1 = {
+            PKG_SOC1, PKG_GAME1, PKG_PROD
+    };
+
+    private static final String[] GROUP_SOC = {
+            PKG_SOC1, PKG_SOC2
+    };
+
+    private static final String[] GROUP_GAME = {
+            PKG_GAME1, PKG_GAME2
+    };
+
+    private final CountDownLatch mCountDownLatch = new CountDownLatch(1);
+
+    private AppTimeLimitController mController;
+
+    private HandlerThread mThread;
+
+    private long mUptimeMillis;
+
+    AppTimeLimitController.OnLimitReachedListener mListener
+            = new AppTimeLimitController.OnLimitReachedListener() {
+
+        @Override
+        public void onLimitReached(int observerId, int userId, long timeLimit, long timeElapsed,
+                PendingIntent callbackIntent) {
+            mCountDownLatch.countDown();
+        }
+    };
+
+    class MyAppTimeLimitController extends AppTimeLimitController {
+        MyAppTimeLimitController(AppTimeLimitController.OnLimitReachedListener listener,
+                Looper looper) {
+            super(listener, looper);
+        }
+
+        @Override
+        protected long getUptimeMillis() {
+            return mUptimeMillis;
+        }
+    }
+
+    @Before
+    public void setUp() {
+        mThread = new HandlerThread("Test");
+        mThread.start();
+        mController = new MyAppTimeLimitController(mListener, mThread.getLooper());
+    }
+
+    @After
+    public void tearDown() {
+        mThread.quit();
+    }
+
+    /** Verify observer is added */
+    @Test
+    public void testAddObserver() {
+        addObserver(OBS_ID1, GROUP1, TIME_30_MIN);
+        assertTrue("Observer wasn't added", hasObserver(OBS_ID1));
+        addObserver(OBS_ID2, GROUP_GAME, TIME_30_MIN);
+        assertTrue("Observer wasn't added", hasObserver(OBS_ID2));
+        assertTrue("Observer wasn't added", hasObserver(OBS_ID1));
+    }
+
+    /** Verify observer is removed */
+    @Test
+    public void testRemoveObserver() {
+        addObserver(OBS_ID1, GROUP1, TIME_30_MIN);
+        assertTrue("Observer wasn't added", hasObserver(OBS_ID1));
+        mController.removeObserver(UID, OBS_ID1, USER_ID);
+        assertFalse("Observer wasn't removed", hasObserver(OBS_ID1));
+    }
+
+    /** Re-adding an observer should result in only one copy */
+    @Test
+    public void testObserverReAdd() {
+        addObserver(OBS_ID1, GROUP1, TIME_30_MIN);
+        assertTrue("Observer wasn't added", hasObserver(OBS_ID1));
+        addObserver(OBS_ID1, GROUP1, TIME_10_MIN);
+        assertTrue("Observer wasn't added",
+                mController.getObserverGroup(OBS_ID1, USER_ID).timeLimit == TIME_10_MIN);
+        mController.removeObserver(UID, OBS_ID1, USER_ID);
+        assertFalse("Observer wasn't removed", hasObserver(OBS_ID1));
+    }
+
+    /** Verify that usage across different apps within a group are added up */
+    @Test
+    public void testAccumulation() throws Exception {
+        setTime(0L);
+        addObserver(OBS_ID1, GROUP1, TIME_30_MIN);
+        moveToForeground(PKG_SOC1);
+        // Add 10 mins
+        setTime(TIME_10_MIN);
+        moveToBackground(PKG_SOC1);
+
+        long timeRemaining = mController.getObserverGroup(OBS_ID1, USER_ID).timeRemaining;
+        assertEquals(TIME_10_MIN * 2, timeRemaining);
+
+        moveToForeground(PKG_SOC1);
+        setTime(TIME_10_MIN * 2);
+        moveToBackground(PKG_SOC1);
+
+        timeRemaining = mController.getObserverGroup(OBS_ID1, USER_ID).timeRemaining;
+        assertEquals(TIME_10_MIN, timeRemaining);
+
+        setTime(TIME_30_MIN);
+
+        assertFalse(mCountDownLatch.await(100L, TimeUnit.MILLISECONDS));
+
+        // Add a different package in the group
+        moveToForeground(PKG_GAME1);
+        setTime(TIME_30_MIN + TIME_10_MIN);
+        moveToBackground(PKG_GAME1);
+
+        assertEquals(0, mController.getObserverGroup(OBS_ID1, USER_ID).timeRemaining);
+        assertTrue(mCountDownLatch.await(100L, TimeUnit.MILLISECONDS));
+    }
+
+    /** Verify that time limit does not get triggered due to a different app */
+    @Test
+    public void testTimeoutOtherApp() throws Exception {
+        setTime(0L);
+        addObserver(OBS_ID1, GROUP1, 4_000L);
+        moveToForeground(PKG_SOC2);
+        assertFalse(mCountDownLatch.await(6_000L, TimeUnit.MILLISECONDS));
+        setTime(6_000L);
+        moveToBackground(PKG_SOC2);
+        assertFalse(mCountDownLatch.await(100L, TimeUnit.MILLISECONDS));
+    }
+
+    /** Verify the timeout message is delivered at the right time */
+    @Test
+    public void testTimeout() throws Exception {
+        setTime(0L);
+        addObserver(OBS_ID1, GROUP1, 4_000L);
+        moveToForeground(PKG_SOC1);
+        setTime(6_000L);
+        assertTrue(mCountDownLatch.await(6_000L, TimeUnit.MILLISECONDS));
+        moveToBackground(PKG_SOC1);
+        // Verify that the observer was removed
+        assertFalse(hasObserver(OBS_ID1));
+    }
+
+    /** If an app was already running, make sure it is partially counted towards the time limit */
+    @Test
+    public void testAlreadyRunning() throws Exception {
+        setTime(TIME_10_MIN);
+        moveToForeground(PKG_GAME1);
+        setTime(TIME_30_MIN);
+        addObserver(OBS_ID2, GROUP_GAME, TIME_30_MIN);
+        setTime(TIME_30_MIN + TIME_10_MIN);
+        moveToBackground(PKG_GAME1);
+        assertFalse(mCountDownLatch.await(1000L, TimeUnit.MILLISECONDS));
+
+        moveToForeground(PKG_GAME2);
+        setTime(TIME_30_MIN + TIME_30_MIN);
+        moveToBackground(PKG_GAME2);
+        assertTrue(mCountDownLatch.await(1000L, TimeUnit.MILLISECONDS));
+        // Verify that the observer was removed
+        assertFalse(hasObserver(OBS_ID2));
+    }
+
+    /** If watched app is already running, verify the timeout callback happens at the right time */
+    @Test
+    public void testAlreadyRunningTimeout() throws Exception {
+        setTime(0);
+        moveToForeground(PKG_SOC1);
+        setTime(TIME_10_MIN);
+        // 10 second time limit
+        addObserver(OBS_ID1, GROUP_SOC, 10_000L);
+        setTime(TIME_10_MIN + 5_000L);
+        // Shouldn't call back in 6 seconds
+        assertFalse(mCountDownLatch.await(6_000L, TimeUnit.MILLISECONDS));
+        setTime(TIME_10_MIN + 10_000L);
+        // Should call back by 11 seconds (6 earlier + 5 now)
+        assertTrue(mCountDownLatch.await(5_000L, TimeUnit.MILLISECONDS));
+        // Verify that the observer was removed
+        assertFalse(hasObserver(OBS_ID1));
+    }
+
+    private void moveToForeground(String packageName) {
+        mController.moveToForeground(packageName, "class", USER_ID);
+    }
+
+    private void moveToBackground(String packageName) {
+        mController.moveToBackground(packageName, "class", USER_ID);
+    }
+
+    private void addObserver(int observerId, String[] packages, long timeLimit) {
+        mController.addObserver(UID, observerId, packages, timeLimit, null, USER_ID);
+    }
+
+    /** Is there still an observer by that id */
+    private boolean hasObserver(int observerId) {
+        return mController.getObserverGroup(observerId, USER_ID) != null;
+    }
+
+    private void setTime(long time) {
+        mUptimeMillis = time;
+    }
+}
diff --git a/services/usage/java/com/android/server/usage/AppTimeLimitController.java b/services/usage/java/com/android/server/usage/AppTimeLimitController.java
new file mode 100644
index 0000000..9cd0593
--- /dev/null
+++ b/services/usage/java/com/android/server/usage/AppTimeLimitController.java
@@ -0,0 +1,464 @@
+/**
+ * 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.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.SystemClock;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.Slog;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.ArrayUtils;
+
+import java.io.PrintWriter;
+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 OnLimitReachedListener mListener;
+
+    @GuardedBy("mLock")
+    private final SparseArray<UserData> mUsers = new SparseArray<>();
+
+    private static class UserData {
+        /** userId of the user */
+        private @UserIdInt int userId;
+
+        /** The app that is currently in the foreground */
+        private String currentForegroundedPackage;
+
+        /** The time when the current app came to the foreground */
+        private long currentForegroundedTime;
+
+        /** The last app that was in the background */
+        private String lastBackgroundedPackage;
+
+        /** Map from package name for quick lookup */
+        private ArrayMap<String, ArrayList<TimeLimitGroup>> packageMap = new ArrayMap<>();
+
+        /** Map of observerId to details of the time limit group */
+        private SparseArray<TimeLimitGroup> groups = new SparseArray<>();
+
+        UserData(@UserIdInt int userId) {
+            this.userId = userId;
+        }
+    }
+
+    /**
+     * Listener interface for being informed when an app group's time limit is reached.
+     */
+    public interface OnLimitReachedListener {
+        /**
+         * 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);
+    }
+
+    static class TimeLimitGroup {
+        int requestingUid;
+        int observerId;
+        String[] packages;
+        long timeLimit;
+        long timeRequested;
+        long timeRemaining;
+        PendingIntent callbackIntent;
+        String currentPackage;
+        long timeCurrentPackageStarted;
+        int userId;
+    }
+
+    class MyHandler extends Handler {
+
+        static final int MSG_CHECK_TIMEOUT = 1;
+        static final int MSG_INFORM_LISTENER = 2;
+
+        MyHandler(Looper looper) {
+            super(looper);
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case MSG_CHECK_TIMEOUT:
+                    checkTimeout((TimeLimitGroup) msg.obj);
+                    break;
+                case MSG_INFORM_LISTENER:
+                    informListener((TimeLimitGroup) msg.obj);
+                    break;
+                default:
+                    super.handleMessage(msg);
+                    break;
+            }
+        }
+    }
+
+    public AppTimeLimitController(OnLimitReachedListener listener, Looper looper) {
+        mHandler = new MyHandler(looper);
+        mListener = listener;
+    }
+
+    /** Overrideable by a test */
+    @VisibleForTesting
+    protected long getUptimeMillis() {
+        return SystemClock.uptimeMillis();
+    }
+
+    /** Returns an existing UserData object for the given userId, or creates one */
+    UserData getOrCreateUserDataLocked(int userId) {
+        UserData userData = mUsers.get(userId);
+        if (userData == null) {
+            userData = new UserData(userId);
+            mUsers.put(userId, userData);
+        }
+        return userData;
+    }
+
+    /** Clean up data if user is removed */
+    public void onUserRemoved(int userId) {
+        synchronized (mLock) {
+            // TODO: Remove any inflight delayed messages
+            mUsers.remove(userId);
+        }
+    }
+
+    /**
+     * Registers an observer with the given details. Existing observer with the same observerId
+     * is removed.
+     */
+    public void addObserver(int requestingUid, int observerId, String[] packages, long timeLimit,
+            PendingIntent callbackIntent, @UserIdInt int userId) {
+        synchronized (mLock) {
+            UserData user = getOrCreateUserDataLocked(userId);
+
+            removeObserverLocked(user, requestingUid, observerId);
+
+            TimeLimitGroup group = new TimeLimitGroup();
+            group.observerId = observerId;
+            group.callbackIntent = callbackIntent;
+            group.packages = packages;
+            group.timeLimit = timeLimit;
+            group.timeRemaining = group.timeLimit;
+            group.timeRequested = getUptimeMillis();
+            group.requestingUid = requestingUid;
+            group.timeCurrentPackageStarted = -1L;
+            group.userId = userId;
+
+            user.groups.append(observerId, group);
+
+            addGroupToPackageMapLocked(user, packages, group);
+
+            if (DEBUG) {
+                Slog.d(TAG, "addObserver " + packages + " for " + timeLimit);
+            }
+            // Handle the case where a target package is already in the foreground when observer
+            // is added.
+            if (user.currentForegroundedPackage != null && inPackageList(group.packages,
+                    user.currentForegroundedPackage)) {
+                group.timeCurrentPackageStarted = group.timeRequested;
+                group.currentPackage = user.currentForegroundedPackage;
+                if (group.timeRemaining > 0) {
+                    postCheckTimeoutLocked(group, group.timeRemaining);
+                }
+            }
+        }
+    }
+
+    /**
+     * 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 removeObserver(int requestingUid, int observerId, @UserIdInt int userId) {
+        synchronized (mLock) {
+            UserData user = getOrCreateUserDataLocked(userId);
+            removeObserverLocked(user, requestingUid, observerId);
+        }
+    }
+
+    @VisibleForTesting
+    TimeLimitGroup getObserverGroup(int observerId, int userId) {
+        synchronized (mLock) {
+            return getOrCreateUserDataLocked(userId).groups.get(observerId);
+        }
+    }
+
+    private static boolean inPackageList(String[] packages, String packageName) {
+        return ArrayUtils.contains(packages, packageName);
+    }
+
+    @GuardedBy("mLock")
+    private void removeObserverLocked(UserData user, int requestingUid, int observerId) {
+        TimeLimitGroup group = user.groups.get(observerId);
+        if (group != null && group.requestingUid == requestingUid) {
+            removeGroupFromPackageMapLocked(user, group);
+            user.groups.remove(observerId);
+            mHandler.removeMessages(MyHandler.MSG_CHECK_TIMEOUT, group);
+        }
+    }
+
+    /**
+     * Called when an app has moved to the foreground.
+     * @param packageName The app that is foregrounded
+     * @param className The className of the activity
+     * @param userId The user
+     */
+    public void moveToForeground(String packageName, String className, int userId) {
+        synchronized (mLock) {
+            UserData user = getOrCreateUserDataLocked(userId);
+            if (DEBUG) Slog.d(TAG, "Setting mCurrentForegroundedPackage to " + packageName);
+            // Note the current foreground package
+            user.currentForegroundedPackage = packageName;
+            user.currentForegroundedTime = getUptimeMillis();
+
+            // Check if the last package that was backgrounded is the same as this one
+            if (!TextUtils.equals(packageName, user.lastBackgroundedPackage)) {
+                // TODO: Move this logic up to usage stats to persist there.
+                incTotalLaunchesLocked(user, packageName);
+            }
+
+            // Check if any of the groups need to watch for this package
+            maybeWatchForPackageLocked(user, packageName, user.currentForegroundedTime);
+        }
+    }
+
+    /**
+     * Called when an app is sent to the background.
+     *
+     * @param packageName
+     * @param className
+     * @param userId
+     */
+    public void moveToBackground(String packageName, String className, int userId) {
+        synchronized (mLock) {
+            UserData user = getOrCreateUserDataLocked(userId);
+            user.lastBackgroundedPackage = packageName;
+            if (!TextUtils.equals(user.currentForegroundedPackage, packageName)) {
+                Slog.w(TAG, "Eh? Last foregrounded package = " + user.currentForegroundedPackage
+                        + " and now backgrounded = " + packageName);
+                return;
+            }
+            final long stopTime = getUptimeMillis();
+
+            // Add up the usage time to all groups that contain the package
+            ArrayList<TimeLimitGroup> groups = user.packageMap.get(packageName);
+            if (groups != null) {
+                final int size = groups.size();
+                for (int i = 0; i < size; i++) {
+                    final TimeLimitGroup group = groups.get(i);
+                    // Don't continue to send
+                    if (group.timeRemaining <= 0) continue;
+
+                    final long startTime = Math.max(user.currentForegroundedTime,
+                            group.timeRequested);
+                    long diff = stopTime - startTime;
+                    group.timeRemaining -= diff;
+                    if (group.timeRemaining <= 0) {
+                        if (DEBUG) Slog.d(TAG, "MTB informing group obs=" + group.observerId);
+                        postInformListenerLocked(group);
+                    }
+                    // Reset indicators that observer was added when package was already fg
+                    group.currentPackage = null;
+                    group.timeCurrentPackageStarted = -1L;
+                    mHandler.removeMessages(MyHandler.MSG_CHECK_TIMEOUT, group);
+                }
+            }
+            user.currentForegroundedPackage = null;
+        }
+    }
+
+    private void postInformListenerLocked(TimeLimitGroup group) {
+        mHandler.sendMessage(mHandler.obtainMessage(MyHandler.MSG_INFORM_LISTENER,
+                group));
+    }
+
+    /**
+     * Inform the observer and unregister it, as the limit has been reached.
+     * @param group the observed group
+     */
+    private void informListener(TimeLimitGroup group) {
+        if (mListener != null) {
+            mListener.onLimitReached(group.observerId, group.userId, group.timeLimit,
+                    group.timeLimit - group.timeRemaining, group.callbackIntent);
+        }
+        // Unregister since the limit has been met and observer was informed.
+        synchronized (mLock) {
+            UserData user = getOrCreateUserDataLocked(group.userId);
+            removeObserverLocked(user, group.requestingUid, group.observerId);
+        }
+    }
+
+    /** Check if any of the groups care about this package and set up delayed messages */
+    @GuardedBy("mLock")
+    private void maybeWatchForPackageLocked(UserData user, String packageName, long uptimeMillis) {
+        ArrayList<TimeLimitGroup> groups = user.packageMap.get(packageName);
+        if (groups == null) return;
+
+        final int size = groups.size();
+        for (int i = 0; i < size; i++) {
+            TimeLimitGroup group = groups.get(i);
+            if (group.timeRemaining > 0) {
+                group.timeCurrentPackageStarted = uptimeMillis;
+                group.currentPackage = packageName;
+                if (DEBUG) {
+                    Slog.d(TAG, "Posting timeout for " + packageName + " for "
+                            + group.timeRemaining + "ms");
+                }
+                postCheckTimeoutLocked(group, group.timeRemaining);
+            }
+        }
+    }
+
+    private void addGroupToPackageMapLocked(UserData user, String[] packages,
+            TimeLimitGroup group) {
+        for (int i = 0; i < packages.length; i++) {
+            ArrayList<TimeLimitGroup> list = user.packageMap.get(packages[i]);
+            if (list == null) {
+                list = new ArrayList<>();
+                user.packageMap.put(packages[i], list);
+            }
+            list.add(group);
+        }
+    }
+
+    /**
+     * Remove the group reference from the package to group mapping, which is 1 to many.
+     * @param group The group to remove from the package map.
+     */
+    private void removeGroupFromPackageMapLocked(UserData user, TimeLimitGroup group) {
+        final int mapSize = user.packageMap.size();
+        for (int i = 0; i < mapSize; i++) {
+            ArrayList<TimeLimitGroup> list = user.packageMap.valueAt(i);
+            list.remove(group);
+        }
+    }
+
+    private void postCheckTimeoutLocked(TimeLimitGroup group, long timeout) {
+        mHandler.sendMessageDelayed(mHandler.obtainMessage(MyHandler.MSG_CHECK_TIMEOUT, group),
+                timeout);
+    }
+
+    /**
+     * See if the given group has reached the timeout if the current foreground app is included
+     * and it exceeds the time remaining.
+     * @param group the group of packages to check
+     */
+    void checkTimeout(TimeLimitGroup group) {
+        // For each package in the group, check if any of the currently foregrounded apps are adding
+        // up to hit the limit and inform the observer
+        synchronized (mLock) {
+            UserData user = getOrCreateUserDataLocked(group.userId);
+            // This group doesn't exist anymore, nothing to see here.
+            if (user.groups.get(group.observerId) != group) return;
+
+            if (DEBUG) Slog.d(TAG, "checkTimeout timeRemaining=" + group.timeRemaining);
+
+            // Already reached the limit, no need to report again
+            if (group.timeRemaining <= 0) return;
+
+            if (DEBUG) {
+                Slog.d(TAG, "checkTimeout foregroundedPackage="
+                        + user.currentForegroundedPackage);
+            }
+
+            if (inPackageList(group.packages, user.currentForegroundedPackage)) {
+                if (DEBUG) {
+                    Slog.d(TAG, "checkTimeout package in foreground="
+                            + user.currentForegroundedPackage);
+                }
+                if (group.timeCurrentPackageStarted < 0) {
+                    Slog.w(TAG, "startTime was not set correctly for " + group);
+                }
+                final long timeInForeground = getUptimeMillis() - group.timeCurrentPackageStarted;
+                if (group.timeRemaining <= timeInForeground) {
+                    if (DEBUG) Slog.d(TAG, "checkTimeout : Time limit reached");
+                    // Hit the limit, set timeRemaining to zero to avoid checking again
+                    group.timeRemaining -= timeInForeground;
+                    postInformListenerLocked(group);
+                    // Reset
+                    group.timeCurrentPackageStarted = -1L;
+                    group.currentPackage = null;
+                } else {
+                    if (DEBUG) Slog.d(TAG, "checkTimeout : Some more time remaining");
+                    postCheckTimeoutLocked(group, group.timeRemaining - timeInForeground);
+                }
+            }
+        }
+    }
+
+    private void incTotalLaunchesLocked(UserData user, String packageName) {
+        // TODO: Inform UsageStatsService and aggregate the counter per app
+    }
+
+    void dump(PrintWriter pw) {
+        synchronized (mLock) {
+            pw.println("\n  App Time Limits");
+            int nUsers = mUsers.size();
+            for (int i = 0; i < nUsers; i++) {
+                UserData user = mUsers.valueAt(i);
+                pw.print("   User "); pw.println(user.userId);
+                int nGroups = user.groups.size();
+                for (int j = 0; j < nGroups; j++) {
+                    TimeLimitGroup group = user.groups.valueAt(j);
+                    pw.print("    Group id="); pw.print(group.observerId);
+                    pw.print(" timeLimit="); pw.print(group.timeLimit);
+                    pw.print(" remaining="); pw.print(group.timeRemaining);
+                    pw.print(" currentPackage="); pw.print(group.currentPackage);
+                    pw.print(" timeCurrentPkgStarted="); pw.print(group.timeCurrentPackageStarted);
+                    pw.print(" packages="); pw.println(Arrays.toString(group.packages));
+                }
+                pw.println();
+                pw.print("    currentForegroundedPackage=");
+                pw.println(user.currentForegroundedPackage);
+                pw.print("    lastBackgroundedPackage="); pw.println(user.lastBackgroundedPackage);
+            }
+        }
+    }
+}
diff --git a/services/usage/java/com/android/server/usage/UsageStatsService.java b/services/usage/java/com/android/server/usage/UsageStatsService.java
index 9be9f3f..b144545c 100644
--- a/services/usage/java/com/android/server/usage/UsageStatsService.java
+++ b/services/usage/java/com/android/server/usage/UsageStatsService.java
@@ -20,6 +20,7 @@
 import android.app.ActivityManager;
 import android.app.AppOpsManager;
 import android.app.IUidObserver;
+import android.app.PendingIntent;
 import android.app.usage.AppStandbyInfo;
 import android.app.usage.ConfigurationStats;
 import android.app.usage.IUsageStatsManager;
@@ -72,6 +73,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.concurrent.TimeUnit;
 
 /**
  * A service that collects, aggregates, and persists application usage data.
@@ -117,6 +119,8 @@
 
     AppStandbyController mAppStandby;
 
+    AppTimeLimitController mAppTimeLimit;
+
     private UsageStatsManagerInternal.AppIdleStateChangeListener mStandbyChangeListener =
             new UsageStatsManagerInternal.AppIdleStateChangeListener() {
                 @Override
@@ -151,6 +155,20 @@
 
         mAppStandby = new AppStandbyController(getContext(), BackgroundThread.get().getLooper());
 
+        mAppTimeLimit = new AppTimeLimitController(
+                (observerId, userId, timeLimit, timeElapsed, callbackIntent) -> {
+                    Intent intent = new Intent();
+                    intent.putExtra(UsageStatsManager.EXTRA_OBSERVER_ID, observerId);
+                    intent.putExtra(UsageStatsManager.EXTRA_TIME_LIMIT, timeLimit);
+                    intent.putExtra(UsageStatsManager.EXTRA_TIME_USED, timeElapsed);
+                    try {
+                        callbackIntent.send(getContext(), 0, intent);
+                    } catch (PendingIntent.CanceledException e) {
+                        Slog.w(TAG, "Couldn't deliver callback: "
+                                + callbackIntent);
+                    }
+                }, mHandler.getLooper());
+
         mAppStandby.addListener(mStandbyChangeListener);
         File systemDataDir = new File(Environment.getDataDirectory(), "system");
         mUsageStatsDir = new File(systemDataDir, "usagestats");
@@ -374,6 +392,16 @@
             service.reportEvent(event);
 
             mAppStandby.reportEvent(event, elapsedRealtime, userId);
+            switch (event.mEventType) {
+                case Event.MOVE_TO_FOREGROUND:
+                    mAppTimeLimit.moveToForeground(event.getPackageName(), event.getClassName(),
+                            userId);
+                    break;
+                case Event.MOVE_TO_BACKGROUND:
+                    mAppTimeLimit.moveToBackground(event.getPackageName(), event.getClassName(),
+                            userId);
+                    break;
+            }
         }
     }
 
@@ -394,6 +422,7 @@
             Slog.i(TAG, "Removing user " + userId + " and all data.");
             mUserState.remove(userId);
             mAppStandby.onUserRemoved(userId);
+            mAppTimeLimit.onUserRemoved(userId);
             cleanUpRemovedUsersLocked();
         }
     }
@@ -549,6 +578,8 @@
                 pw.println();
                 mAppStandby.dumpState(args, pw);
             }
+
+            mAppTimeLimit.dump(pw);
         }
     }
 
@@ -927,6 +958,60 @@
 
             mHandler.obtainMessage(MSG_REPORT_EVENT, userId, 0, event).sendToTarget();
         }
+
+        @Override
+        public void registerAppUsageObserver(int observerId,
+                String[] packages, long timeLimitMs, PendingIntent
+                callbackIntent, String callingPackage) {
+            if (!hasPermission(callingPackage)) {
+                throw new SecurityException("Caller doesn't have PACKAGE_USAGE_STATS permission");
+            }
+
+            if (packages == null || packages.length == 0) {
+                throw new IllegalArgumentException("Must specify at least one package");
+            }
+            if (timeLimitMs <= 0) {
+                throw new IllegalArgumentException("Time limit must be > 0");
+            }
+            if (callbackIntent == null) {
+                throw new NullPointerException("callbackIntent can't be null");
+            }
+            final int callingUid = Binder.getCallingUid();
+            final int userId = UserHandle.getUserId(callingUid);
+            final long token = Binder.clearCallingIdentity();
+            try {
+                UsageStatsService.this.registerAppUsageObserver(callingUid, observerId,
+                        packages, timeLimitMs, callbackIntent, userId);
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
+        }
+
+        @Override
+        public void unregisterAppUsageObserver(int observerId, String callingPackage) {
+            if (!hasPermission(callingPackage)) {
+                throw new SecurityException("Caller doesn't have PACKAGE_USAGE_STATS permission");
+            }
+
+            final int callingUid = Binder.getCallingUid();
+            final int userId = UserHandle.getUserId(callingUid);
+            final long token = Binder.clearCallingIdentity();
+            try {
+                UsageStatsService.this.unregisterAppUsageObserver(callingUid, observerId, userId);
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
+        }
+    }
+
+    void registerAppUsageObserver(int callingUid, int observerId, String[] packages,
+            long timeLimitMs, PendingIntent callbackIntent, int userId) {
+        mAppTimeLimit.addObserver(callingUid, observerId, packages, timeLimitMs, callbackIntent,
+                userId);
+    }
+
+    void unregisterAppUsageObserver(int callingUid, int observerId, int userId) {
+        mAppTimeLimit.removeObserver(callingUid, observerId, userId);
     }
 
     /**
diff --git a/tests/UsageStatsTest/AndroidManifest.xml b/tests/UsageStatsTest/AndroidManifest.xml
index c27be7b..66af454 100644
--- a/tests/UsageStatsTest/AndroidManifest.xml
+++ b/tests/UsageStatsTest/AndroidManifest.xml
@@ -21,5 +21,6 @@
         </activity>
 
         <activity android:name=".UsageLogActivity" />
+
     </application>
 </manifest>
diff --git a/tests/UsageStatsTest/res/menu/main.xml b/tests/UsageStatsTest/res/menu/main.xml
index 4ccbc81..612267c 100644
--- a/tests/UsageStatsTest/res/menu/main.xml
+++ b/tests/UsageStatsTest/res/menu/main.xml
@@ -4,4 +4,6 @@
         android:title="View Log"/>
     <item android:id="@+id/call_is_app_inactive"
         android:title="Call isAppInactive()"/>
+    <item android:id="@+id/set_app_limit"
+        android:title="Set App Limit" />
 </menu>
diff --git a/tests/UsageStatsTest/src/com/android/tests/usagestats/UsageStatsActivity.java b/tests/UsageStatsTest/src/com/android/tests/usagestats/UsageStatsActivity.java
index 9429d9b..3d8ce21 100644
--- a/tests/UsageStatsTest/src/com/android/tests/usagestats/UsageStatsActivity.java
+++ b/tests/UsageStatsTest/src/com/android/tests/usagestats/UsageStatsActivity.java
@@ -18,6 +18,7 @@
 
 import android.app.AlertDialog;
 import android.app.ListActivity;
+import android.app.PendingIntent;
 import android.app.usage.UsageStats;
 import android.app.usage.UsageStatsManager;
 import android.content.Context;
@@ -36,14 +37,17 @@
 import android.widget.BaseAdapter;
 import android.widget.EditText;
 import android.widget.TextView;
+import android.widget.Toast;
 
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.Map;
+import java.util.concurrent.TimeUnit;
 
 public class UsageStatsActivity extends ListActivity {
     private static final long USAGE_STATS_PERIOD = 1000 * 60 * 60 * 24 * 14;
+    private static final String EXTRA_KEY_TIMEOUT = "com.android.tests.usagestats.extra.TIMEOUT";
     private UsageStatsManager mUsageStatsManager;
     private Adapter mAdapter;
     private Comparator<UsageStats> mComparator = new Comparator<UsageStats>() {
@@ -59,6 +63,20 @@
         mUsageStatsManager = (UsageStatsManager) getSystemService(Context.USAGE_STATS_SERVICE);
         mAdapter = new Adapter();
         setListAdapter(mAdapter);
+        Bundle extras = getIntent().getExtras();
+        if (extras != null && extras.containsKey(UsageStatsManager.EXTRA_TIME_USED)) {
+            System.err.println("UsageStatsActivity " + extras);
+            Toast.makeText(this, "Timeout of observed app\n" + extras, Toast.LENGTH_SHORT).show();
+        }
+    }
+
+    @Override
+    public void onNewIntent(Intent intent) {
+        Bundle extras = intent.getExtras();
+        if (extras != null && extras.containsKey(UsageStatsManager.EXTRA_TIME_USED)) {
+            System.err.println("UsageStatsActivity " + extras);
+            Toast.makeText(this, "Timeout of observed app\n" + extras, Toast.LENGTH_SHORT).show();
+        }
     }
 
     @Override
@@ -77,7 +95,9 @@
             case R.id.call_is_app_inactive:
                 callIsAppInactive();
                 return true;
-
+            case R.id.set_app_limit:
+                callSetAppLimit();
+                return true;
             default:
                 return super.onOptionsItemSelected(item);
         }
@@ -116,6 +136,40 @@
         builder.show();
     }
 
+    private void callSetAppLimit() {
+        final AlertDialog.Builder builder = new AlertDialog.Builder(this);
+        builder.setTitle("Enter package name");
+        final EditText input = new EditText(this);
+        input.setInputType(InputType.TYPE_CLASS_TEXT);
+        input.setHint("com.android.tests.usagestats");
+        builder.setView(input);
+
+        builder.setPositiveButton("OK", new DialogInterface.OnClickListener() {
+            @Override
+            public void onClick(DialogInterface dialog, int which) {
+                final String packageName = input.getText().toString().trim();
+                if (!TextUtils.isEmpty(packageName)) {
+                    String[] packages = packageName.split(",");
+                    Intent intent = new Intent(Intent.ACTION_MAIN);
+                    intent.setClass(UsageStatsActivity.this, UsageStatsActivity.class);
+                    intent.setPackage(getPackageName());
+                    intent.putExtra(EXTRA_KEY_TIMEOUT, true);
+                    mUsageStatsManager.registerAppUsageObserver(1, packages,
+                            30, TimeUnit.SECONDS, PendingIntent.getActivity(UsageStatsActivity.this,
+                                    1, intent, 0));
+                }
+            }
+        });
+        builder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
+            @Override
+            public void onClick(DialogInterface dialog, int which) {
+                dialog.cancel();
+            }
+        });
+
+        builder.show();
+    }
+
     private void showInactive(String packageName) {
         final AlertDialog.Builder builder = new AlertDialog.Builder(this);
         builder.setMessage(