Merge "App Time Limits API in UsageStats" into pi-dev

This commit is contained in:
TreeHugger Robot
2018-03-16 22:40:46 +00:00
committed by Android (Google) Code Review
9 changed files with 946 additions and 1 deletions

View File

@@ -729,9 +729,14 @@ package android.app.usage {
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
}

View File

@@ -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 @@ interface IUsageStatsManager {
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);
}

View File

@@ -20,6 +20,7 @@ import android.annotation.IntDef;
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.ArrayList;
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 @@ public final class UsageStatsManager {
@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 @@ public final class UsageStatsManager {
}
}
/**
* @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();

View File

@@ -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;
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -20,6 +20,7 @@ import android.Manifest;
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.Arrays;
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 @@ public class UsageStatsService extends SystemService implements
AppStandbyController mAppStandby;
AppTimeLimitController mAppTimeLimit;
private UsageStatsManagerInternal.AppIdleStateChangeListener mStandbyChangeListener =
new UsageStatsManagerInternal.AppIdleStateChangeListener() {
@Override
@@ -151,6 +155,20 @@ 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);
}
}, mHandler.getLooper());
mAppStandby.addListener(mStandbyChangeListener);
File systemDataDir = new File(Environment.getDataDirectory(), "system");
mUsageStatsDir = new File(systemDataDir, "usagestats");
@@ -374,6 +392,16 @@ public class UsageStatsService extends SystemService implements
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 @@ public class UsageStatsService extends SystemService implements
Slog.i(TAG, "Removing user " + userId + " and all data.");
mUserState.remove(userId);
mAppStandby.onUserRemoved(userId);
mAppTimeLimit.onUserRemoved(userId);
cleanUpRemovedUsersLocked();
}
}
@@ -549,6 +578,8 @@ public class UsageStatsService extends SystemService implements
pw.println();
mAppStandby.dumpState(args, pw);
}
mAppTimeLimit.dump(pw);
}
}
@@ -927,6 +958,60 @@ public class UsageStatsService extends SystemService implements
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);
}
/**

View File

@@ -21,5 +21,6 @@
</activity>
<activity android:name=".UsageLogActivity" />
</application>
</manifest>

View File

@@ -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>

View File

@@ -18,6 +18,7 @@ package com.android.tests.usagestats;
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.view.ViewGroup;
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 @@ public class UsageStatsActivity extends ListActivity {
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 @@ public class UsageStatsActivity extends ListActivity {
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 @@ public class UsageStatsActivity extends ListActivity {
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(