diff --git a/core/java/android/app/usage/IUsageStatsManager.aidl b/core/java/android/app/usage/IUsageStatsManager.aidl index 971352783dcb7..4d52263c1d78d 100644 --- a/core/java/android/app/usage/IUsageStatsManager.aidl +++ b/core/java/android/app/usage/IUsageStatsManager.aidl @@ -51,4 +51,8 @@ interface IUsageStatsManager { 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 047adddd794db..793d6b0639a8f 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 @@ public class AppTimeLimitControllerTests { 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 @@ public class AppTimeLimitControllerTests { 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 @@ public class AppTimeLimitControllerTests { 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 @@ public class AppTimeLimitControllerTests { } @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 @@ public class AppTimeLimitControllerTests { 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 5916b04c079a3..eaaf9b2210db8 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.Handler; 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 @@ public class AppTimeLimitController { 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 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 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 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> packageMap = new ArrayMap<>(); - - /** Map of observerId to details of the time limit group */ - private SparseArray 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> 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 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 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 appUsageGroups = new SparseArray<>(); + + /** Map of observerId to details of the time limit group */ + SparseArray 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 mUserRef; + protected WeakReference mObserverAppRef; + protected PendingIntent mLimitReachedCallback; + + UsageGroup(UserData user, ObserverAppData observerApp, int observerId, String[] observed, + long timeLimitMs, PendingIntent limitReachedCallback) { + mUserRef = new WeakReference<>(user); + mObserverAppRef = new WeakReference<>(observerApp); + mObserverId = observerId; + mObserved = observed; + mTimeLimitMs = timeLimitMs; + mLimitReachedCallback = limitReachedCallback; + } + + @GuardedBy("mLock") + public long getTimeLimitMs() { return mTimeLimitMs; } + + @GuardedBy("mLock") + public long getUsageTimeMs() { return mUsageTimeMs; } + + @GuardedBy("mLock") + public void remove() { + UserData user = mUserRef.get(); + if (user != null) { + user.removeUsageGroup(this); + } + // Clear the callback, so any racy inflight message will do nothing + mLimitReachedCallback = null; + } + + @GuardedBy("mLock") + void noteUsageStart(long startTimeMs) { + noteUsageStart(startTimeMs, startTimeMs); + } + + @GuardedBy("mLock") + void noteUsageStart(long startTimeMs, long currentTimeMs) { + if (mActives++ == 0) { + 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 class AppTimeLimitController { 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 class AppTimeLimitController { } } - public AppTimeLimitController(OnLimitReachedListener listener, Looper looper) { + public AppTimeLimitController(TimeLimitCallbackListener listener, Looper looper) { mHandler = new MyHandler(looper); mListener = listener; } @@ -157,7 +525,13 @@ public class AppTimeLimitController { /** 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 @@ public class AppTimeLimitController { 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 @@ public class AppTimeLimitController { 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 @@ public class AppTimeLimitController { } /** - * 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); - } - // 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); - } + Slog.d(TAG, "addObserver " + observed + " for " + timeLimit); } + + user.addUsageGroup(group); + noteActiveLocked(user, group, getUptimeMillis()); } } /** * Remove a registered observer by observerId and calling uid. + * * @param requestingUid The calling uid - * @param observerId The unique observer id for this user - * @param userId The user id of the observer + * @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 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 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 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 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 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 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 dd1ddfaf73420..262125212c145 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 @@ public class UsageStatsService extends SystemService implements 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 @@ public class UsageStatsService extends SystemService implements 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 @@ public class UsageStatsService extends SystemService implements 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); } /**