Refactor AppTimeLimitController for Session Observers

Introducing the concept of Usage Session Observers to UsageStats. A
session observer monitors usage within individual "continuous" sessions
(brief gaps of non usage may be allowed in a session and still be
considered continuous)

The new session observer in AppTimeLimitController are both similar and
different enough from the current app usage observer to warrant
refactoring TimeLimitGroup into an OOP friendly abstract base class.

Added some Observer App handling to avoid clash between registered
observers from multiple apps.

Reworded packages to observed and usage entities to accomodate future
changes, where usage may come from more than just app usage.

Reworded moveToForeground/Background to generic usage and allow multiple
usage entities to be active at the same time to accomodate future
changes, where more than just the foreground app can be considered used.

Test: atest FrameworksServicesTests:AppTimeLimitControllerTests
Bug: 111465038
Change-Id: I63aebf8b0aa5516111bd6d5e142525d0bee6ef58
diff --git a/core/java/android/app/usage/IUsageStatsManager.aidl b/core/java/android/app/usage/IUsageStatsManager.aidl
index 9713527..4d52263 100644
--- a/core/java/android/app/usage/IUsageStatsManager.aidl
+++ b/core/java/android/app/usage/IUsageStatsManager.aidl
@@ -51,4 +51,8 @@
     void registerAppUsageObserver(int observerId, in String[] packages, long timeLimitMs,
             in PendingIntent callback, String callingPackage);
     void unregisterAppUsageObserver(int observerId, String callingPackage);
+    void registerUsageSessionObserver(int sessionObserverId, in String[] observed, long timeLimitMs,
+            long sessionThresholdTimeMs, in PendingIntent limitReachedCallbackIntent,
+            in PendingIntent sessionEndCallbackIntent, String callingPackage);
+    void unregisterUsageSessionObserver(int sessionObserverId, String callingPackage);
 }
diff --git a/services/tests/servicestests/src/com/android/server/usage/AppTimeLimitControllerTests.java b/services/tests/servicestests/src/com/android/server/usage/AppTimeLimitControllerTests.java
index 047addd..793d6b0 100644
--- a/services/tests/servicestests/src/com/android/server/usage/AppTimeLimitControllerTests.java
+++ b/services/tests/servicestests/src/com/android/server/usage/AppTimeLimitControllerTests.java
@@ -61,6 +61,7 @@
 
     private static final long TIME_30_MIN = 30 * 60_000L;
     private static final long TIME_10_MIN = 10 * 60_000L;
+    private static final long TIME_1_MIN = 10 * 60_000L;
 
     private static final long MAX_OBSERVER_PER_UID = 10;
     private static final long MIN_TIME_LIMIT = 4_000L;
@@ -77,7 +78,8 @@
             PKG_GAME1, PKG_GAME2
     };
 
-    private final CountDownLatch mCountDownLatch = new CountDownLatch(1);
+    private CountDownLatch mLimitReachedLatch = new CountDownLatch(1);
+    private CountDownLatch mSessionEndLatch = new CountDownLatch(1);
 
     private AppTimeLimitController mController;
 
@@ -85,18 +87,24 @@
 
     private long mUptimeMillis;
 
-    AppTimeLimitController.OnLimitReachedListener mListener
-            = new AppTimeLimitController.OnLimitReachedListener() {
+    AppTimeLimitController.TimeLimitCallbackListener mListener =
+            new AppTimeLimitController.TimeLimitCallbackListener() {
+                @Override
+                public void onLimitReached(int observerId, int userId, long timeLimit,
+                        long timeElapsed,
+                        PendingIntent callbackIntent) {
+                    mLimitReachedLatch.countDown();
+                }
 
-        @Override
-        public void onLimitReached(int observerId, int userId, long timeLimit, long timeElapsed,
-                PendingIntent callbackIntent) {
-            mCountDownLatch.countDown();
-        }
-    };
+                @Override
+                public void onSessionEnd(int observerId, int userId, long timeElapsed,
+                        PendingIntent callbackIntent) {
+                    mSessionEndLatch.countDown();
+                }
+            };
 
     class MyAppTimeLimitController extends AppTimeLimitController {
-        MyAppTimeLimitController(AppTimeLimitController.OnLimitReachedListener listener,
+        MyAppTimeLimitController(AppTimeLimitController.TimeLimitCallbackListener listener,
                 Looper looper) {
             super(listener, looper);
         }
@@ -107,7 +115,12 @@
         }
 
         @Override
-        protected long getObserverPerUidLimit() {
+        protected long getAppUsageObserverPerUidLimit() {
+            return MAX_OBSERVER_PER_UID;
+        }
+
+        @Override
+        protected long getUsageSessionObserverPerUidLimit() {
             return MAX_OBSERVER_PER_UID;
         }
 
@@ -129,188 +142,551 @@
         mThread.quit();
     }
 
-    /** Verify observer is added */
+    /** Verify app usage 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));
+    public void testAppUsageObserver_AddObserver() {
+        addAppUsageObserver(OBS_ID1, GROUP1, TIME_30_MIN);
+        assertTrue("Observer wasn't added", hasAppUsageObserver(UID, OBS_ID1));
+        addAppUsageObserver(OBS_ID2, GROUP_GAME, TIME_30_MIN);
+        assertTrue("Observer wasn't added", hasAppUsageObserver(UID, OBS_ID2));
+        assertTrue("Observer wasn't added", hasAppUsageObserver(UID, OBS_ID1));
     }
 
-    /** Verify observer is removed */
+    /** Verify usage session observer is added */
     @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));
+    public void testUsageSessionObserver_AddObserver() {
+        addUsageSessionObserver(OBS_ID1, GROUP1, TIME_30_MIN, TIME_1_MIN);
+        assertTrue("Observer wasn't added", hasUsageSessionObserver(UID, OBS_ID1));
+        addUsageSessionObserver(OBS_ID2, GROUP_GAME, TIME_30_MIN, TIME_1_MIN);
+        assertTrue("Observer wasn't added", hasUsageSessionObserver(UID, OBS_ID2));
+        assertTrue("Observer wasn't added", hasUsageSessionObserver(UID, OBS_ID1));
+    }
+
+    /** Verify app usage observer is removed */
+    @Test
+    public void testAppUsageObserver_RemoveObserver() {
+        addAppUsageObserver(OBS_ID1, GROUP1, TIME_30_MIN);
+        assertTrue("Observer wasn't added", hasAppUsageObserver(UID, OBS_ID1));
+        mController.removeAppUsageObserver(UID, OBS_ID1, USER_ID);
+        assertFalse("Observer wasn't removed", hasAppUsageObserver(UID, OBS_ID1));
+    }
+
+    /** Verify usage session observer is removed */
+    @Test
+    public void testUsageSessionObserver_RemoveObserver() {
+        addUsageSessionObserver(OBS_ID1, GROUP1, TIME_30_MIN, TIME_1_MIN);
+        assertTrue("Observer wasn't added", hasUsageSessionObserver(UID, OBS_ID1));
+        mController.removeUsageSessionObserver(UID, OBS_ID1, USER_ID);
+        assertFalse("Observer wasn't removed", hasUsageSessionObserver(UID, 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);
+    public void testAppUsageObserver_ObserverReAdd() {
+        addAppUsageObserver(OBS_ID1, GROUP1, TIME_30_MIN);
+        assertTrue("Observer wasn't added", hasAppUsageObserver(UID, OBS_ID1));
+        addAppUsageObserver(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));
+                mController.getAppUsageGroup(UID, OBS_ID1).getTimeLimitMs() == TIME_10_MIN);
+        mController.removeAppUsageObserver(UID, OBS_ID1, USER_ID);
+        assertFalse("Observer wasn't removed", hasAppUsageObserver(UID, OBS_ID1));
+    }
+
+    /** Re-adding an observer should result in only one copy */
+    @Test
+    public void testUsageSessionObserver_ObserverReAdd() {
+        addUsageSessionObserver(OBS_ID1, GROUP1, TIME_30_MIN, TIME_1_MIN);
+        assertTrue("Observer wasn't added", hasUsageSessionObserver(UID, OBS_ID1));
+        addUsageSessionObserver(OBS_ID1, GROUP1, TIME_10_MIN, TIME_1_MIN);
+        assertTrue("Observer wasn't added",
+                mController.getSessionUsageGroup(UID, OBS_ID1).getTimeLimitMs() == TIME_10_MIN);
+        mController.removeUsageSessionObserver(UID, OBS_ID1, USER_ID);
+        assertFalse("Observer wasn't removed", hasUsageSessionObserver(UID, OBS_ID1));
+    }
+
+    /** Different type observers can be registered to the same observerId value */
+    @Test
+    public void testAllObservers_ExclusiveObserverIds() {
+        addAppUsageObserver(OBS_ID1, GROUP1, TIME_10_MIN);
+        addUsageSessionObserver(OBS_ID1, GROUP1, TIME_30_MIN, TIME_1_MIN);
+        assertTrue("Observer wasn't added", hasAppUsageObserver(UID, OBS_ID1));
+        assertTrue("Observer wasn't added", hasUsageSessionObserver(UID, OBS_ID1));
+
+        AppTimeLimitController.UsageGroup appUsageGroup = mController.getAppUsageGroup(UID,
+                OBS_ID1);
+        AppTimeLimitController.UsageGroup sessionUsageGroup = mController.getSessionUsageGroup(UID,
+                OBS_ID1);
+
+        // Verify data still intact
+        assertEquals(TIME_10_MIN, appUsageGroup.getTimeLimitMs());
+        assertEquals(TIME_30_MIN, sessionUsageGroup.getTimeLimitMs());
     }
 
     /** Verify that usage across different apps within a group are added up */
     @Test
-    public void testAccumulation() throws Exception {
+    public void testAppUsageObserver_Accumulation() throws Exception {
         setTime(0L);
-        addObserver(OBS_ID1, GROUP1, TIME_30_MIN);
-        moveToForeground(PKG_SOC1);
+        addAppUsageObserver(OBS_ID1, GROUP1, TIME_30_MIN);
+        startUsage(PKG_SOC1);
         // Add 10 mins
         setTime(TIME_10_MIN);
-        moveToBackground(PKG_SOC1);
+        stopUsage(PKG_SOC1);
 
-        long timeRemaining = mController.getObserverGroup(OBS_ID1, USER_ID).timeRemaining;
+        AppTimeLimitController.UsageGroup group = mController.getAppUsageGroup(UID, OBS_ID1);
+
+        long timeRemaining = group.getTimeLimitMs() - group.getUsageTimeMs();
         assertEquals(TIME_10_MIN * 2, timeRemaining);
 
-        moveToForeground(PKG_SOC1);
+        startUsage(PKG_SOC1);
         setTime(TIME_10_MIN * 2);
-        moveToBackground(PKG_SOC1);
+        stopUsage(PKG_SOC1);
 
-        timeRemaining = mController.getObserverGroup(OBS_ID1, USER_ID).timeRemaining;
+        timeRemaining = group.getTimeLimitMs() - group.getUsageTimeMs();
         assertEquals(TIME_10_MIN, timeRemaining);
 
         setTime(TIME_30_MIN);
 
-        assertFalse(mCountDownLatch.await(100L, TimeUnit.MILLISECONDS));
+        assertFalse(mLimitReachedLatch.await(100L, TimeUnit.MILLISECONDS));
 
         // Add a different package in the group
-        moveToForeground(PKG_GAME1);
+        startUsage(PKG_GAME1);
         setTime(TIME_30_MIN + TIME_10_MIN);
-        moveToBackground(PKG_GAME1);
+        stopUsage(PKG_GAME1);
 
-        assertEquals(0, mController.getObserverGroup(OBS_ID1, USER_ID).timeRemaining);
-        assertTrue(mCountDownLatch.await(100L, TimeUnit.MILLISECONDS));
+        assertEquals(0, group.getTimeLimitMs() - group.getUsageTimeMs());
+        assertTrue(mLimitReachedLatch.await(100L, TimeUnit.MILLISECONDS));
+    }
+
+    /** Verify that usage across different apps within a group are added up */
+    @Test
+    public void testUsageSessionObserver_Accumulation() throws Exception {
+        setTime(0L);
+        addUsageSessionObserver(OBS_ID1, GROUP1, TIME_30_MIN, TIME_1_MIN);
+        startUsage(PKG_SOC1);
+        // Add 10 mins
+        setTime(TIME_10_MIN);
+        stopUsage(PKG_SOC1);
+
+        AppTimeLimitController.UsageGroup group = mController.getSessionUsageGroup(UID, OBS_ID1);
+
+        long timeRemaining = group.getTimeLimitMs() - group.getUsageTimeMs();
+        assertEquals(TIME_10_MIN * 2, timeRemaining);
+
+        startUsage(PKG_SOC1);
+        setTime(TIME_10_MIN * 2);
+        stopUsage(PKG_SOC1);
+
+        timeRemaining = group.getTimeLimitMs() - group.getUsageTimeMs();
+        assertEquals(TIME_10_MIN, timeRemaining);
+
+        setTime(TIME_30_MIN);
+
+        assertFalse(mLimitReachedLatch.await(100L, TimeUnit.MILLISECONDS));
+
+        // Add a different package in the group
+        startUsage(PKG_GAME1);
+        setTime(TIME_30_MIN + TIME_10_MIN);
+        stopUsage(PKG_GAME1);
+
+        assertEquals(0, group.getTimeLimitMs() - group.getUsageTimeMs());
+        assertTrue(mLimitReachedLatch.await(100L, TimeUnit.MILLISECONDS));
     }
 
     /** Verify that time limit does not get triggered due to a different app */
     @Test
-    public void testTimeoutOtherApp() throws Exception {
+    public void testAppUsageObserver_TimeoutOtherApp() throws Exception {
         setTime(0L);
-        addObserver(OBS_ID1, GROUP1, 4_000L);
-        moveToForeground(PKG_SOC2);
-        assertFalse(mCountDownLatch.await(6_000L, TimeUnit.MILLISECONDS));
+        addAppUsageObserver(OBS_ID1, GROUP1, 4_000L);
+        startUsage(PKG_SOC2);
+        assertFalse(mLimitReachedLatch.await(6_000L, TimeUnit.MILLISECONDS));
         setTime(6_000L);
-        moveToBackground(PKG_SOC2);
-        assertFalse(mCountDownLatch.await(100L, TimeUnit.MILLISECONDS));
+        stopUsage(PKG_SOC2);
+        assertFalse(mLimitReachedLatch.await(100L, TimeUnit.MILLISECONDS));
+    }
+
+    /** Verify that time limit does not get triggered due to a different app */
+    @Test
+    public void testUsageSessionObserver_TimeoutOtherApp() throws Exception {
+        setTime(0L);
+        addUsageSessionObserver(OBS_ID1, GROUP1, 4_000L, 1_000L);
+        startUsage(PKG_SOC2);
+        assertFalse(mLimitReachedLatch.await(6_000L, TimeUnit.MILLISECONDS));
+        setTime(6_000L);
+        stopUsage(PKG_SOC2);
+        assertFalse(mLimitReachedLatch.await(100L, TimeUnit.MILLISECONDS));
+
     }
 
     /** Verify the timeout message is delivered at the right time */
     @Test
-    public void testTimeout() throws Exception {
+    public void testAppUsageObserver_Timeout() throws Exception {
         setTime(0L);
-        addObserver(OBS_ID1, GROUP1, 4_000L);
-        moveToForeground(PKG_SOC1);
+        addAppUsageObserver(OBS_ID1, GROUP1, 4_000L);
+        startUsage(PKG_SOC1);
         setTime(6_000L);
-        assertTrue(mCountDownLatch.await(6_000L, TimeUnit.MILLISECONDS));
-        moveToBackground(PKG_SOC1);
+        assertTrue(mLimitReachedLatch.await(6_000L, TimeUnit.MILLISECONDS));
+        stopUsage(PKG_SOC1);
         // Verify that the observer was removed
-        assertFalse(hasObserver(OBS_ID1));
+        assertFalse(hasAppUsageObserver(UID, OBS_ID1));
+    }
+
+    /** Verify the timeout message is delivered at the right time */
+    @Test
+    public void testUsageSessionObserver_Timeout() throws Exception {
+        setTime(0L);
+        addUsageSessionObserver(OBS_ID1, GROUP1, 4_000L, 1_000L);
+        startUsage(PKG_SOC1);
+        setTime(6_000L);
+        assertTrue(mLimitReachedLatch.await(6_000L, TimeUnit.MILLISECONDS));
+        stopUsage(PKG_SOC1);
+        // Usage has stopped, Session should end in a second. Verify session end occurs in a second
+        // (+/- 100ms, which is hopefully not too slim a margin)
+        assertFalse(mSessionEndLatch.await(900L, TimeUnit.MILLISECONDS));
+        assertTrue(mSessionEndLatch.await(200L, TimeUnit.MILLISECONDS));
+        // Verify that the observer was not removed
+        assertTrue(hasUsageSessionObserver(UID, OBS_ID1));
     }
 
     /** If an app was already running, make sure it is partially counted towards the time limit */
     @Test
-    public void testAlreadyRunning() throws Exception {
+    public void testAppUsageObserver_AlreadyRunning() throws Exception {
         setTime(TIME_10_MIN);
-        moveToForeground(PKG_GAME1);
+        startUsage(PKG_GAME1);
         setTime(TIME_30_MIN);
-        addObserver(OBS_ID2, GROUP_GAME, TIME_30_MIN);
+        addAppUsageObserver(OBS_ID2, GROUP_GAME, TIME_30_MIN);
         setTime(TIME_30_MIN + TIME_10_MIN);
-        moveToBackground(PKG_GAME1);
-        assertFalse(mCountDownLatch.await(1000L, TimeUnit.MILLISECONDS));
+        stopUsage(PKG_GAME1);
+        assertFalse(mLimitReachedLatch.await(1_000L, TimeUnit.MILLISECONDS));
 
-        moveToForeground(PKG_GAME2);
+        startUsage(PKG_GAME2);
         setTime(TIME_30_MIN + TIME_30_MIN);
-        moveToBackground(PKG_GAME2);
-        assertTrue(mCountDownLatch.await(1000L, TimeUnit.MILLISECONDS));
+        stopUsage(PKG_GAME2);
+        assertTrue(mLimitReachedLatch.await(1_000L, TimeUnit.MILLISECONDS));
         // Verify that the observer was removed
-        assertFalse(hasObserver(OBS_ID2));
+        assertFalse(hasAppUsageObserver(UID, OBS_ID2));
+    }
+
+    /** If an app was already running, make sure it is partially counted towards the time limit */
+    @Test
+    public void testUsageSessionObserver_AlreadyRunning() throws Exception {
+        setTime(TIME_10_MIN);
+        startUsage(PKG_GAME1);
+        setTime(TIME_30_MIN);
+        addUsageSessionObserver(OBS_ID2, GROUP_GAME, TIME_30_MIN, TIME_1_MIN);
+        setTime(TIME_30_MIN + TIME_10_MIN);
+        stopUsage(PKG_GAME1);
+        assertFalse(mLimitReachedLatch.await(1_000L, TimeUnit.MILLISECONDS));
+
+        startUsage(PKG_GAME2);
+        setTime(TIME_30_MIN + TIME_30_MIN);
+        stopUsage(PKG_GAME2);
+        assertTrue(mLimitReachedLatch.await(1_000L, TimeUnit.MILLISECONDS));
+        // Verify that the observer was removed
+        assertTrue(hasUsageSessionObserver(UID, OBS_ID2));
     }
 
     /** If watched app is already running, verify the timeout callback happens at the right time */
     @Test
-    public void testAlreadyRunningTimeout() throws Exception {
+    public void testAppUsageObserver_AlreadyRunningTimeout() throws Exception {
         setTime(0);
-        moveToForeground(PKG_SOC1);
+        startUsage(PKG_SOC1);
         setTime(TIME_10_MIN);
         // 10 second time limit
-        addObserver(OBS_ID1, GROUP_SOC, 10_000L);
+        addAppUsageObserver(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));
+        assertFalse(mLimitReachedLatch.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));
+        assertTrue(mLimitReachedLatch.await(5_000L, TimeUnit.MILLISECONDS));
         // Verify that the observer was removed
-        assertFalse(hasObserver(OBS_ID1));
+        assertFalse(hasAppUsageObserver(UID, OBS_ID1));
     }
 
-    /** Verify that App Time Limit Controller will limit the number of observerIds */
+    /** If watched app is already running, verify the timeout callback happens at the right time */
     @Test
-    public void testMaxObserverLimit() throws Exception {
+    public void testUsageSessionObserver_AlreadyRunningTimeout() throws Exception {
+        setTime(0);
+        startUsage(PKG_SOC1);
+        setTime(TIME_10_MIN);
+        // 10 second time limit
+        addUsageSessionObserver(OBS_ID1, GROUP_SOC, 10_000L, 1_000L);
+        setTime(TIME_10_MIN + 5_000L);
+        // Shouldn't call back in 6 seconds
+        assertFalse(mLimitReachedLatch.await(6_000L, TimeUnit.MILLISECONDS));
+        setTime(TIME_10_MIN + 10_000L);
+        // Should call back by 11 seconds (6 earlier + 5 now)
+        assertTrue(mLimitReachedLatch.await(5_000L, TimeUnit.MILLISECONDS));
+        stopUsage(PKG_SOC1);
+        // Usage has stopped, Session should end in a second. Verify session end occurs in a second
+        // (+/- 100ms, which is hopefully not too slim a margin)
+        assertFalse(mSessionEndLatch.await(900L, TimeUnit.MILLISECONDS));
+        assertTrue(mSessionEndLatch.await(200L, TimeUnit.MILLISECONDS));
+        // Verify that the observer was removed
+        assertTrue(hasUsageSessionObserver(UID, OBS_ID1));
+    }
+
+    /**
+     * Verify that App Time Limit Controller will limit the number of observerIds for app usage
+     * observers
+     */
+    @Test
+    public void testAppUsageObserver_MaxObserverLimit() throws Exception {
         boolean receivedException = false;
         int ANOTHER_UID = UID + 1;
-        addObserver(OBS_ID1, GROUP1, TIME_30_MIN);
-        addObserver(OBS_ID2, GROUP1, TIME_30_MIN);
-        addObserver(OBS_ID3, GROUP1, TIME_30_MIN);
-        addObserver(OBS_ID4, GROUP1, TIME_30_MIN);
-        addObserver(OBS_ID5, GROUP1, TIME_30_MIN);
-        addObserver(OBS_ID6, GROUP1, TIME_30_MIN);
-        addObserver(OBS_ID7, GROUP1, TIME_30_MIN);
-        addObserver(OBS_ID8, GROUP1, TIME_30_MIN);
-        addObserver(OBS_ID9, GROUP1, TIME_30_MIN);
-        addObserver(OBS_ID10, GROUP1, TIME_30_MIN);
+        addAppUsageObserver(OBS_ID1, GROUP1, TIME_30_MIN);
+        addAppUsageObserver(OBS_ID2, GROUP1, TIME_30_MIN);
+        addAppUsageObserver(OBS_ID3, GROUP1, TIME_30_MIN);
+        addAppUsageObserver(OBS_ID4, GROUP1, TIME_30_MIN);
+        addAppUsageObserver(OBS_ID5, GROUP1, TIME_30_MIN);
+        addAppUsageObserver(OBS_ID6, GROUP1, TIME_30_MIN);
+        addAppUsageObserver(OBS_ID7, GROUP1, TIME_30_MIN);
+        addAppUsageObserver(OBS_ID8, GROUP1, TIME_30_MIN);
+        addAppUsageObserver(OBS_ID9, GROUP1, TIME_30_MIN);
+        addAppUsageObserver(OBS_ID10, GROUP1, TIME_30_MIN);
         // Readding an observer should not cause an IllegalStateException
-        addObserver(OBS_ID5, GROUP1, TIME_30_MIN);
+        addAppUsageObserver(OBS_ID5, GROUP1, TIME_30_MIN);
         // Adding an observer for a different uid shouldn't cause an IllegalStateException
-        mController.addObserver(ANOTHER_UID, OBS_ID11, GROUP1, TIME_30_MIN, null, USER_ID);
+        mController.addAppUsageObserver(ANOTHER_UID, OBS_ID11, GROUP1, TIME_30_MIN, null, USER_ID);
         try {
-            addObserver(OBS_ID11, GROUP1, TIME_30_MIN);
+            addAppUsageObserver(OBS_ID11, GROUP1, TIME_30_MIN);
         } catch (IllegalStateException ise) {
             receivedException = true;
         }
         assertTrue("Should have caused an IllegalStateException", receivedException);
     }
 
-    /** Verify that addObserver minimum time limit is one minute */
+    /**
+     * Verify that App Time Limit Controller will limit the number of observerIds for usage session
+     * observers
+     */
     @Test
-    public void testMinimumTimeLimit() throws Exception {
+    public void testUsageSessionObserver_MaxObserverLimit() throws Exception {
+        addUsageSessionObserver(OBS_ID1, GROUP1, TIME_30_MIN, TIME_1_MIN);
+        boolean receivedException = false;
+        int ANOTHER_UID = UID + 1;
+        addUsageSessionObserver(OBS_ID2, GROUP1, TIME_30_MIN, TIME_1_MIN);
+        addUsageSessionObserver(OBS_ID3, GROUP1, TIME_30_MIN, TIME_1_MIN);
+        addUsageSessionObserver(OBS_ID4, GROUP1, TIME_30_MIN, TIME_1_MIN);
+        addUsageSessionObserver(OBS_ID5, GROUP1, TIME_30_MIN, TIME_1_MIN);
+        addUsageSessionObserver(OBS_ID6, GROUP1, TIME_30_MIN, TIME_1_MIN);
+        addUsageSessionObserver(OBS_ID7, GROUP1, TIME_30_MIN, TIME_1_MIN);
+        addUsageSessionObserver(OBS_ID8, GROUP1, TIME_30_MIN, TIME_1_MIN);
+        addUsageSessionObserver(OBS_ID9, GROUP1, TIME_30_MIN, TIME_1_MIN);
+        addUsageSessionObserver(OBS_ID10, GROUP1, TIME_30_MIN, TIME_1_MIN);
+        // Readding an observer should not cause an IllegalStateException
+        addUsageSessionObserver(OBS_ID5, GROUP1, TIME_30_MIN, TIME_1_MIN);
+        // Adding an observer for a different uid shouldn't cause an IllegalStateException
+        mController.addUsageSessionObserver(ANOTHER_UID, OBS_ID11, GROUP1, TIME_30_MIN, TIME_1_MIN,
+                null, null, USER_ID);
+        try {
+            addUsageSessionObserver(OBS_ID11, GROUP1, TIME_30_MIN, TIME_1_MIN);
+        } catch (IllegalStateException ise) {
+            receivedException = true;
+        }
+        assertTrue("Should have caused an IllegalStateException", receivedException);
+    }
+
+    /** Verify that addAppUsageObserver minimum time limit is one minute */
+    @Test
+    public void testAppUsageObserver_MinimumTimeLimit() throws Exception {
         boolean receivedException = false;
         // adding an observer with a one minute time limit should not cause an exception
-        addObserver(OBS_ID1, GROUP1, MIN_TIME_LIMIT);
+        addAppUsageObserver(OBS_ID1, GROUP1, MIN_TIME_LIMIT);
         try {
-            addObserver(OBS_ID1, GROUP1, MIN_TIME_LIMIT - 1);
+            addAppUsageObserver(OBS_ID1, GROUP1, MIN_TIME_LIMIT - 1);
         } catch (IllegalArgumentException iae) {
             receivedException = true;
         }
         assertTrue("Should have caused an IllegalArgumentException", receivedException);
     }
 
-    private void moveToForeground(String packageName) {
-        mController.moveToForeground(packageName, "class", USER_ID);
+    /** Verify that addUsageSessionObserver minimum time limit is one minute */
+    @Test
+    public void testUsageSessionObserver_MinimumTimeLimit() throws Exception {
+        boolean receivedException = false;
+        // test also for session observers
+        addUsageSessionObserver(OBS_ID10, GROUP1, MIN_TIME_LIMIT, TIME_1_MIN);
+        try {
+            addUsageSessionObserver(OBS_ID10, GROUP1, MIN_TIME_LIMIT - 1, TIME_1_MIN);
+        } catch (IllegalArgumentException iae) {
+            receivedException = true;
+        }
+        assertTrue("Should have caused an IllegalArgumentException", receivedException);
     }
 
-    private void moveToBackground(String packageName) {
-        mController.moveToBackground(packageName, "class", USER_ID);
+    /** Verify that concurrent usage from multiple apps in the same group will counted correctly */
+    @Test
+    public void testAppUsageObserver_ConcurrentUsage() throws Exception {
+        setTime(0L);
+        addAppUsageObserver(OBS_ID1, GROUP1, TIME_30_MIN);
+        AppTimeLimitController.UsageGroup group = mController.getAppUsageGroup(UID, OBS_ID1);
+        startUsage(PKG_SOC1);
+        // Add 10 mins
+        setTime(TIME_10_MIN);
+
+        // Add a different package in the group will first package is still in use
+        startUsage(PKG_GAME1);
+        setTime(TIME_10_MIN * 2);
+        // Stop first package usage
+        stopUsage(PKG_SOC1);
+
+        setTime(TIME_30_MIN);
+        stopUsage(PKG_GAME1);
+
+        assertEquals(TIME_30_MIN, group.getUsageTimeMs());
+        assertTrue(mLimitReachedLatch.await(100L, TimeUnit.MILLISECONDS));
     }
 
-    private void addObserver(int observerId, String[] packages, long timeLimit) {
-        mController.addObserver(UID, observerId, packages, timeLimit, null, USER_ID);
+    /** Verify that concurrent usage from multiple apps in the same group will counted correctly */
+    @Test
+    public void testUsageSessionObserver_ConcurrentUsage() throws Exception {
+        setTime(0L);
+        addUsageSessionObserver(OBS_ID1, GROUP1, TIME_30_MIN, TIME_1_MIN);
+        AppTimeLimitController.UsageGroup group = mController.getSessionUsageGroup(UID, OBS_ID1);
+        startUsage(PKG_SOC1);
+        // Add 10 mins
+        setTime(TIME_10_MIN);
+
+        // Add a different package in the group will first package is still in use
+        startUsage(PKG_GAME1);
+        setTime(TIME_10_MIN * 2);
+        // Stop first package usage
+        stopUsage(PKG_SOC1);
+
+        setTime(TIME_30_MIN);
+        stopUsage(PKG_GAME1);
+
+        assertEquals(TIME_30_MIN, group.getUsageTimeMs());
+        assertTrue(mLimitReachedLatch.await(100L, TimeUnit.MILLISECONDS));
     }
 
-    /** Is there still an observer by that id */
-    private boolean hasObserver(int observerId) {
-        return mController.getObserverGroup(observerId, USER_ID) != null;
+    /** Verify that a session will continue if usage starts again within the session threshold */
+    @Test
+    public void testUsageSessionObserver_ContinueSession() throws Exception {
+        setTime(0L);
+        addUsageSessionObserver(OBS_ID1, GROUP1, 10_000L, 2_000L);
+        startUsage(PKG_SOC1);
+        setTime(6_000L);
+        stopUsage(PKG_SOC1);
+        // Wait momentarily, Session should not end
+        assertFalse(mSessionEndLatch.await(1_000L, TimeUnit.MILLISECONDS));
+
+        setTime(7_000L);
+        startUsage(PKG_SOC1);
+        setTime(10_500L);
+        stopUsage(PKG_SOC1);
+        // Total usage time has not reached the limit. Time limit callback should not fire yet
+        assertFalse(mLimitReachedLatch.await(100L, TimeUnit.MILLISECONDS));
+
+        setTime(10_600L);
+        startUsage(PKG_SOC1);
+        setTime(12_000L);
+        assertTrue(mLimitReachedLatch.await(1_000L, TimeUnit.MILLISECONDS));
+        stopUsage(PKG_SOC1);
+        // Usage has stopped, Session should end in 2 seconds. Verify session end occurs
+        // (+/- 100ms, which is hopefully not too slim a margin)
+        assertFalse(mSessionEndLatch.await(1_900L, TimeUnit.MILLISECONDS));
+        assertTrue(mSessionEndLatch.await(200L, TimeUnit.MILLISECONDS));
+        // Verify that the observer was not removed
+        assertTrue(hasUsageSessionObserver(UID, OBS_ID1));
+    }
+
+    /** Verify that a new session will start if next usage starts after the session threshold */
+    @Test
+    public void testUsageSessionObserver_NewSession() throws Exception {
+        setTime(0L);
+        addUsageSessionObserver(OBS_ID1, GROUP1, 10_000L, 1_000L);
+        startUsage(PKG_SOC1);
+        setTime(6_000L);
+        stopUsage(PKG_SOC1);
+        // Wait for longer than the session threshold. Session end callback should not be triggered
+        // because the usage timelimit hasn't been triggered.
+        assertFalse(mSessionEndLatch.await(1_500L, TimeUnit.MILLISECONDS));
+
+        setTime(7_500L);
+        // This should be the start of a new session
+        startUsage(PKG_SOC1);
+        setTime(16_000L);
+        stopUsage(PKG_SOC1);
+        // Total usage has exceed the timelimit, but current session time has not
+        assertFalse(mLimitReachedLatch.await(100L, TimeUnit.MILLISECONDS));
+
+        setTime(16_100L);
+        startUsage(PKG_SOC1);
+        setTime(18_000L);
+        assertTrue(mLimitReachedLatch.await(2000L, TimeUnit.MILLISECONDS));
+        stopUsage(PKG_SOC1);
+        // Usage has stopped, Session should end in 2 seconds. Verify session end occurs
+        // (+/- 100ms, which is hopefully not too slim a margin)
+        assertFalse(mSessionEndLatch.await(900L, TimeUnit.MILLISECONDS));
+        assertTrue(mSessionEndLatch.await(200L, TimeUnit.MILLISECONDS));
+        // Verify that the observer was not removed
+        assertTrue(hasUsageSessionObserver(UID, OBS_ID1));
+    }
+
+    /** Verify that the callbacks will be triggered for multiple sessions */
+    @Test
+    public void testUsageSessionObserver_RepeatSessions() throws Exception {
+        setTime(0L);
+        addUsageSessionObserver(OBS_ID1, GROUP1, 10_000L, 1_000L);
+        startUsage(PKG_SOC1);
+        setTime(9_000L);
+        stopUsage(PKG_SOC1);
+        // Stutter usage here, to reduce real world time needed trigger limit reached callback
+        startUsage(PKG_SOC1);
+        setTime(11_000L);
+        assertTrue(mLimitReachedLatch.await(2_000L, TimeUnit.MILLISECONDS));
+        stopUsage(PKG_SOC1);
+        // Usage has stopped, Session should end in 1 seconds. Verify session end occurs
+        // (+/- 100ms, which is hopefully not too slim a margin)
+        assertFalse(mSessionEndLatch.await(900L, TimeUnit.MILLISECONDS));
+        assertTrue(mSessionEndLatch.await(200L, TimeUnit.MILLISECONDS));
+
+        // Rearm the countdown latches
+        mLimitReachedLatch = new CountDownLatch(1);
+        mSessionEndLatch = new CountDownLatch(1);
+
+        // New session start
+        setTime(20_000L);
+        startUsage(PKG_SOC1);
+        setTime(29_000L);
+        stopUsage(PKG_SOC1);
+        startUsage(PKG_SOC1);
+        setTime(31_000L);
+        assertTrue(mLimitReachedLatch.await(2_000L, TimeUnit.MILLISECONDS));
+        stopUsage(PKG_SOC1);
+        assertFalse(mSessionEndLatch.await(900L, TimeUnit.MILLISECONDS));
+        assertTrue(mSessionEndLatch.await(200L, TimeUnit.MILLISECONDS));
+        assertTrue(hasUsageSessionObserver(UID, OBS_ID1));
+    }
+
+    private void startUsage(String packageName) {
+        mController.noteUsageStart(packageName, USER_ID);
+    }
+
+    private void stopUsage(String packageName) {
+        mController.noteUsageStop(packageName, USER_ID);
+    }
+
+    private void addAppUsageObserver(int observerId, String[] packages, long timeLimit) {
+        mController.addAppUsageObserver(UID, observerId, packages, timeLimit, null, USER_ID);
+    }
+
+    private void addUsageSessionObserver(int observerId, String[] packages, long timeLimit,
+            long sessionThreshold) {
+        mController.addUsageSessionObserver(UID, observerId, packages, timeLimit, sessionThreshold,
+                null, null, USER_ID);
+    }
+
+    /** Is there still an app usage observer by that id */
+    private boolean hasAppUsageObserver(int uid, int observerId) {
+        return mController.getAppUsageGroup(uid, observerId) != null;
+    }
+
+    /** Is there still an usage session observer by that id */
+    private boolean hasUsageSessionObserver(int uid, int observerId) {
+        return mController.getSessionUsageGroup(uid, observerId) != null;
     }
 
     private void setTime(long time) {
diff --git a/services/usage/java/com/android/server/usage/AppTimeLimitController.java b/services/usage/java/com/android/server/usage/AppTimeLimitController.java
index 5916b04c..eaaf9b2 100644
--- a/services/usage/java/com/android/server/usage/AppTimeLimitController.java
+++ b/services/usage/java/com/android/server/usage/AppTimeLimitController.java
@@ -22,17 +22,16 @@
 import android.os.Looper;
 import android.os.Message;
 import android.os.SystemClock;
-import android.text.TextUtils;
 import android.util.ArrayMap;
+import android.util.ArraySet;
 import android.util.Slog;
 import android.util.SparseArray;
-import android.util.SparseIntArray;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.util.ArrayUtils;
 
 import java.io.PrintWriter;
+import java.lang.ref.WeakReference;
 import java.util.ArrayList;
 import java.util.Arrays;
 
@@ -57,72 +56,432 @@
 
     private final MyHandler mHandler;
 
-    private OnLimitReachedListener mListener;
+    private TimeLimitCallbackListener mListener;
 
     private static final long MAX_OBSERVER_PER_UID = 1000;
 
     private static final long ONE_MINUTE = 60_000L;
 
+    /** Collection of data for each user that has reported usage */
     @GuardedBy("mLock")
     private final SparseArray<UserData> mUsers = new SparseArray<>();
 
-    private static class UserData {
+    /**
+     * 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;
+        private @UserIdInt
+        int userId;
 
-        /** The app that is currently in the foreground */
-        private String currentForegroundedPackage;
+        /** Set of the currently active entities */
+        private final ArraySet<String> currentlyActive = new ArraySet<>();
 
-        /** The time when the current app came to the foreground */
-        private long currentForegroundedTime;
-
-        /** 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<>();
-
-        /** Map of the number of observerIds registered by uid */
-        private SparseIntArray observerIdCounts = new SparseIntArray();
+        /** Map from entity name for quick lookup */
+        private 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.contains(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 ArrayList<UsageGroup> list = observedMap.get(group.mObserved[i]);
+                if (list != null) {
+                    list.remove(group);
+                }
+            }
+        }
+
+        @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.valueAt(i));
+                pw.print(", ");
+            }
+            pw.println();
+            pw.print(" Observed Entities:");
+            final int nEntities = currentlyActive.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<>();
+
+        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 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 = appUsageGroups.size();
+            for (int i = 0; i < nSessionUsageGroups; i++) {
+                sessionUsageGroups.valueAt(i).dump(pw);
+                pw.println();
+            }
+        }
     }
 
     /**
      * Listener interface for being informed when an app group's time limit is reached.
      */
-    public interface OnLimitReachedListener {
+    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 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);
     }
 
-    static class TimeLimitGroup {
-        int requestingUid;
-        int observerId;
-        String[] packages;
-        long timeLimit;
-        long timeRequested;
-        long timeRemaining;
-        PendingIntent callbackIntent;
-        String currentPackage;
-        long timeCurrentPackageStarted;
-        int userId;
+    abstract class UsageGroup {
+        protected int mObserverId;
+        protected String[] mObserved;
+        protected long mTimeLimitMs;
+        protected long mUsageTimeMs;
+        protected int mActives;
+        protected long mLastKnownUsageTimeMs;
+        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) {
+                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.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;
+                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.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 mLastUsageEndTimeMs;
+        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) {
+                mLastUsageEndTimeMs = stopTimeMs;
+                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);
+        }
+    }
+
+
     private class MyHandler extends Handler {
-
         static final int MSG_CHECK_TIMEOUT = 1;
-        static final int MSG_INFORM_LISTENER = 2;
+        static final int MSG_INFORM_LIMIT_REACHED_LISTENER = 2;
+        static final int MSG_INFORM_SESSION_END = 3;
 
         MyHandler(Looper looper) {
             super(looper);
@@ -132,10 +491,19 @@
         public void handleMessage(Message msg) {
             switch (msg.what) {
                 case MSG_CHECK_TIMEOUT:
-                    checkTimeout((TimeLimitGroup) msg.obj);
+                    synchronized (mLock) {
+                        ((UsageGroup) msg.obj).checkTimeout(getUptimeMillis());
+                    }
                     break;
-                case MSG_INFORM_LISTENER:
-                    informListener((TimeLimitGroup) msg.obj);
+                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);
@@ -144,7 +512,7 @@
         }
     }
 
-    public AppTimeLimitController(OnLimitReachedListener listener, Looper looper) {
+    public AppTimeLimitController(TimeLimitCallbackListener listener, Looper looper) {
         mHandler = new MyHandler(looper);
         mListener = listener;
     }
@@ -157,7 +525,13 @@
 
     /** Overrideable for testing purposes */
     @VisibleForTesting
-    protected long getObserverPerUidLimit() {
+    protected long getAppUsageObserverPerUidLimit() {
+        return MAX_OBSERVER_PER_UID;
+    }
+
+    /** Overrideable for testing purposes */
+    @VisibleForTesting
+    protected long getUsageSessionObserverPerUidLimit() {
         return MAX_OBSERVER_PER_UID;
     }
 
@@ -167,6 +541,21 @@
         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);
+        }
+    }
+
     /** Returns an existing UserData object for the given userId, or creates one */
     @GuardedBy("mLock")
     private UserData getOrCreateUserDataLocked(int userId) {
@@ -178,6 +567,17 @@
         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) {
@@ -187,300 +587,219 @@
     }
 
     /**
-     * Registers an observer with the given details. Existing observer with the same observerId
-     * is removed.
+     * Check if group has any currently active entities.
      */
-    public void addObserver(int requestingUid, int observerId, String[] packages, long timeLimit,
-            PendingIntent callbackIntent, @UserIdInt int userId) {
+    @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.contains(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);
-            removeObserverLocked(user, requestingUid, observerId, /*readding =*/ true);
-
-            final int observerIdCount = user.observerIdCounts.get(requestingUid, 0);
-            if (observerIdCount >= getObserverPerUidLimit()) {
-                throw new IllegalStateException(
-                        "Too many observers added by uid " + requestingUid);
+            ObserverAppData observerApp = getOrCreateObserverAppDataLocked(requestingUid);
+            AppUsageGroup group = observerApp.appUsageGroups.get(observerId);
+            if (group != null) {
+                // Remove previous app usage group associated with observerId
+                observerApp.appUsageGroups.get(observerId).remove();
             }
-            user.observerIdCounts.put(requestingUid, observerIdCount + 1);
 
-            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);
+            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 " + packages + " for " + timeLimit);
+                Slog.d(TAG, "addObserver " + observed + " 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);
-                }
-            }
+
+            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
+     * @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) {
+    public void removeAppUsageObserver(int requestingUid, int observerId, @UserIdInt int userId) {
+        synchronized (mLock) {
+            ObserverAppData observerApp = getOrCreateObserverAppDataLocked(requestingUid);
+            observerApp.appUsageGroups.get(observerId).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);
-            removeObserverLocked(user, requestingUid, observerId, /*readding =*/ false);
+            ObserverAppData observerApp = getOrCreateObserverAppDataLocked(requestingUid);
+            SessionUsageGroup group = observerApp.sessionUsageGroups.get(observerId);
+            if (group != null) {
+                // Remove previous app usage group associated with observerId
+                observerApp.sessionUsageGroups.get(observerId).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());
         }
     }
 
-    @VisibleForTesting
-    TimeLimitGroup getObserverGroup(int observerId, int userId) {
+    /**
+     * 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) {
-            return getOrCreateUserDataLocked(userId).groups.get(observerId);
+            ObserverAppData observerApp = getOrCreateObserverAppDataLocked(requestingUid);
+            observerApp.sessionUsageGroups.get(observerId).remove();
         }
     }
 
-    private static boolean inPackageList(String[] packages, String packageName) {
-        return ArrayUtils.contains(packages, packageName);
+    /**
+     * 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 {
+        synchronized (mLock) {
+            UserData user = getOrCreateUserDataLocked(userId);
+            if (DEBUG) Slog.d(TAG, "Usage entity " + name + " became active");
+            if (user.currentlyActive.contains(name)) {
+                throw new IllegalArgumentException(
+                        "Unable to start usage for " + name + ", already in use");
+            }
+            final long currentTime = getUptimeMillis();
+
+            // Add to the list of active entities
+            user.currentlyActive.add(name);
+
+            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);
+            }
+        }
+    }
+
+    /**
+     * 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");
+            if (!user.currentlyActive.remove(name)) {
+                throw new IllegalArgumentException(
+                        "Unable to stop usage for " + name + ", not in use");
+            }
+            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 removeObserverLocked(UserData user, int requestingUid, int observerId,
-            boolean readding) {
-        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);
-            final int observerIdCount = user.observerIdCounts.get(requestingUid);
-            if (observerIdCount <= 1 && !readding) {
-                user.observerIdCounts.delete(requestingUid);
-            } else {
-                user.observerIdCounts.put(requestingUid, observerIdCount - 1);
-            }
-        }
-    }
-
-    /**
-     * 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 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);
-            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,
+    private void postInformLimitReachedListenerLocked(UsageGroup group) {
+        mHandler.sendMessage(mHandler.obtainMessage(MyHandler.MSG_INFORM_LIMIT_REACHED_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, false);
-        }
-    }
-
-    /** 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 postInformSessionEndListenerLocked(SessionUsageGroup group, long timeout) {
+        mHandler.sendMessageDelayed(mHandler.obtainMessage(MyHandler.MSG_INFORM_SESSION_END, group),
+                timeout);
     }
 
-    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);
-        }
+    @GuardedBy("mLock")
+    private void cancelInformSessionEndListener(SessionUsageGroup group) {
+        mHandler.removeMessages(MyHandler.MSG_INFORM_SESSION_END, 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) {
+    @GuardedBy("mLock")
+    private void postCheckTimeoutLocked(UsageGroup 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);
-                }
-            }
-        }
+    @GuardedBy("mLock")
+    private void cancelCheckTimeoutLocked(UsageGroup group) {
+        mHandler.removeMessages(MyHandler.MSG_CHECK_TIMEOUT, group);
     }
 
     void dump(PrintWriter pw) {
         synchronized (mLock) {
             pw.println("\n  App Time Limits");
-            int nUsers = mUsers.size();
+            final 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("   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);
             }
         }
     }
diff --git a/services/usage/java/com/android/server/usage/UsageStatsService.java b/services/usage/java/com/android/server/usage/UsageStatsService.java
index dd1ddfa..2621252 100644
--- a/services/usage/java/com/android/server/usage/UsageStatsService.java
+++ b/services/usage/java/com/android/server/usage/UsageStatsService.java
@@ -165,16 +165,36 @@
         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);
+                new AppTimeLimitController.TimeLimitCallbackListener() {
+                    @Override
+                    public void onLimitReached(int observerId, int userId, long timeLimit,
+                            long timeElapsed, PendingIntent callbackIntent) {
+                        if (callbackIntent == null) return;
+                        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);
+                        }
+                    }
+
+                    @Override
+                    public void onSessionEnd(int observerId, int userId, long timeElapsed,
+                            PendingIntent callbackIntent) {
+                        if (callbackIntent == null) return;
+                        Intent intent = new Intent();
+                        intent.putExtra(UsageStatsManager.EXTRA_OBSERVER_ID, observerId);
+                        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());
 
@@ -412,12 +432,18 @@
             mAppStandby.reportEvent(event, elapsedRealtime, userId);
             switch (event.mEventType) {
                 case Event.MOVE_TO_FOREGROUND:
-                    mAppTimeLimit.moveToForeground(event.getPackageName(), event.getClassName(),
-                            userId);
+                    try {
+                        mAppTimeLimit.noteUsageStart(event.getPackageName(), userId);
+                    } catch (IllegalArgumentException iae) {
+                        Slog.e(TAG, "Failed to note usage start", iae);
+                    }
                     break;
                 case Event.MOVE_TO_BACKGROUND:
-                    mAppTimeLimit.moveToBackground(event.getPackageName(), event.getClassName(),
-                            userId);
+                    try {
+                        mAppTimeLimit.noteUsageStop(event.getPackageName(), userId);
+                    } catch (IllegalArgumentException iae) {
+                        Slog.e(TAG, "Failed to note usage stop", iae);
+                    }
                     break;
             }
         }
@@ -1151,16 +1177,70 @@
                 Binder.restoreCallingIdentity(token);
             }
         }
+
+        @Override
+        public void registerUsageSessionObserver(int sessionObserverId, String[] observed,
+                long timeLimitMs, long sessionThresholdTimeMs,
+                PendingIntent limitReachedCallbackIntent, PendingIntent sessionEndCallbackIntent,
+                String callingPackage) {
+            if (!hasObserverPermission(callingPackage)) {
+                throw new SecurityException("Caller doesn't have OBSERVE_APP_USAGE permission");
+            }
+
+            if (observed == null || observed.length == 0) {
+                throw new IllegalArgumentException("Must specify at least one observed entity");
+            }
+            if (limitReachedCallbackIntent == null) {
+                throw new NullPointerException("limitReachedCallbackIntent can't be null");
+            }
+            final int callingUid = Binder.getCallingUid();
+            final int userId = UserHandle.getUserId(callingUid);
+            final long token = Binder.clearCallingIdentity();
+            try {
+                UsageStatsService.this.registerUsageSessionObserver(callingUid, sessionObserverId,
+                        observed, timeLimitMs, sessionThresholdTimeMs, limitReachedCallbackIntent,
+                        sessionEndCallbackIntent, userId);
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
+        }
+
+        @Override
+        public void unregisterUsageSessionObserver(int sessionObserverId, String callingPackage) {
+            if (!hasObserverPermission(callingPackage)) {
+                throw new SecurityException("Caller doesn't have OBSERVE_APP_USAGE permission");
+            }
+
+            final int callingUid = Binder.getCallingUid();
+            final int userId = UserHandle.getUserId(callingUid);
+            final long token = Binder.clearCallingIdentity();
+            try {
+                UsageStatsService.this.unregisterUsageSessionObserver(callingUid, sessionObserverId, 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,
+        mAppTimeLimit.addAppUsageObserver(callingUid, observerId, packages, timeLimitMs, callbackIntent,
                 userId);
     }
 
     void unregisterAppUsageObserver(int callingUid, int observerId, int userId) {
-        mAppTimeLimit.removeObserver(callingUid, observerId, userId);
+        mAppTimeLimit.removeAppUsageObserver(callingUid, observerId, userId);
+    }
+
+    void registerUsageSessionObserver(int callingUid, int observerId, String[] observed,
+            long timeLimitMs, long sessionThresholdTime, PendingIntent limitReachedCallbackIntent,
+            PendingIntent sessionEndCallbackIntent, int userId) {
+        mAppTimeLimit.addUsageSessionObserver(callingUid, observerId, observed, timeLimitMs,
+                sessionThresholdTime, limitReachedCallbackIntent, sessionEndCallbackIntent, userId);
+    }
+
+    void unregisterUsageSessionObserver(int callingUid, int sessionObserverId, int userId) {
+        mAppTimeLimit.removeUsageSessionObserver(callingUid, sessionObserverId, userId);
     }
 
     /**