Merge "App Time Limits API in UsageStats" into pi-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
a8bb9d255f
@@ -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
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -21,5 +21,6 @@
|
||||
</activity>
|
||||
|
||||
<activity android:name=".UsageLogActivity" />
|
||||
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user