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