Merge "Add Usage Reporting Api to UsageStatsManager"
This commit is contained in:
committed by
Android (Google) Code Review
commit
266dd3bfd7
@@ -927,6 +927,9 @@ package android.app.usage {
|
||||
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 registerUsageSessionObserver(int, java.lang.String[], long, java.util.concurrent.TimeUnit, long, java.util.concurrent.TimeUnit, android.app.PendingIntent, android.app.PendingIntent);
|
||||
method public void reportUsageStart(android.app.Activity, java.lang.String);
|
||||
method public void reportUsageStart(android.app.Activity, java.lang.String, long);
|
||||
method public void reportUsageStop(android.app.Activity, java.lang.String);
|
||||
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);
|
||||
|
||||
@@ -55,4 +55,8 @@ interface IUsageStatsManager {
|
||||
long sessionThresholdTimeMs, in PendingIntent limitReachedCallbackIntent,
|
||||
in PendingIntent sessionEndCallbackIntent, String callingPackage);
|
||||
void unregisterUsageSessionObserver(int sessionObserverId, String callingPackage);
|
||||
void reportUsageStart(in IBinder activity, String token, String callingPackage);
|
||||
void reportPastUsageStart(in IBinder activity, String token, long timeAgoMs,
|
||||
String callingPackage);
|
||||
void reportUsageStop(in IBinder activity, String token, String callingPackage);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import android.annotation.RequiresPermission;
|
||||
import android.annotation.SystemApi;
|
||||
import android.annotation.SystemService;
|
||||
import android.annotation.UnsupportedAppUsage;
|
||||
import android.app.Activity;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.pm.ParceledListSlice;
|
||||
@@ -579,15 +580,18 @@ 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 {@code timeLimit} specified. The
|
||||
* observer will automatically be unregistered when the time limit is reached and the intent
|
||||
* is delivered. Registering an {@code observerId} that was already registered will override
|
||||
* the previous one. No more than 1000 unique {@code observerId} may be registered by a single
|
||||
* uid at any one time.
|
||||
* the sum of usages of apps and tokens in the {@code observed} array exceeds the
|
||||
* {@code timeLimit} specified. The structure of a token is a String with the reporting
|
||||
* package's name and a token the reporting app will use, separated by the forward slash
|
||||
* character. Example: com.reporting.package/5OM3*0P4QU3-7OK3N
|
||||
* The observer will automatically be unregistered when the time limit is reached and the
|
||||
* intent is delivered. Registering an {@code observerId} that was already registered will
|
||||
* override the previous one. No more than 1000 unique {@code observerId} may be registered by
|
||||
* a single uid at any one time.
|
||||
* @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. Cannot be null
|
||||
* and must include at least one package.
|
||||
* @param observedEntities The list of packages and token to observe for usage time. Cannot be
|
||||
* null and must include at least one package or token.
|
||||
* @param timeLimit The total time the set of apps can be in the foreground before the
|
||||
* callbackIntent is delivered. Must be at least one minute.
|
||||
* @param timeUnit The unit for time specified in {@code timeLimit}. Cannot be null.
|
||||
@@ -600,11 +604,11 @@ public final class UsageStatsManager {
|
||||
*/
|
||||
@SystemApi
|
||||
@RequiresPermission(android.Manifest.permission.OBSERVE_APP_USAGE)
|
||||
public void registerAppUsageObserver(int observerId, @NonNull String[] packages, long timeLimit,
|
||||
@NonNull TimeUnit timeUnit, @NonNull PendingIntent callbackIntent) {
|
||||
public void registerAppUsageObserver(int observerId, @NonNull String[] observedEntities,
|
||||
long timeLimit, @NonNull TimeUnit timeUnit, @NonNull PendingIntent callbackIntent) {
|
||||
try {
|
||||
mService.registerAppUsageObserver(observerId, packages, timeUnit.toMillis(timeLimit),
|
||||
callbackIntent, mContext.getOpPackageName());
|
||||
mService.registerAppUsageObserver(observerId, observedEntities,
|
||||
timeUnit.toMillis(timeLimit), callbackIntent, mContext.getOpPackageName());
|
||||
} catch (RemoteException e) {
|
||||
throw e.rethrowFromSystemServer();
|
||||
}
|
||||
@@ -631,18 +635,21 @@ public final class UsageStatsManager {
|
||||
|
||||
/**
|
||||
* Register a usage session observer that receives a callback on the provided {@code
|
||||
* limitReachedCallbackIntent} when the sum of usages of apps in the packages array exceeds
|
||||
* the {@code timeLimit} specified within a usage session. After the {@code timeLimit} has
|
||||
* been reached, the usage session observer will receive a callback on the provided {@code
|
||||
* sessionEndCallbackIntent} when the usage session ends. Registering another session
|
||||
* observer against a {@code sessionObserverId} that has already been registered will
|
||||
* override the previous session observer.
|
||||
* limitReachedCallbackIntent} when the sum of usages of apps and tokens in the {@code
|
||||
* observed} array exceeds the {@code timeLimit} specified within a usage session. The
|
||||
* structure of a token is a String with the reporting packages' name and a token the
|
||||
* reporting app will use, separated by the forward slash character.
|
||||
* Example: com.reporting.package/5OM3*0P4QU3-7OK3N
|
||||
* After the {@code timeLimit} has been reached, the usage session observer will receive a
|
||||
* callback on the provided {@code sessionEndCallbackIntent} when the usage session ends.
|
||||
* Registering another session observer against a {@code sessionObserverId} that has already
|
||||
* been registered will override the previous session observer.
|
||||
*
|
||||
* @param sessionObserverId 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. Cannot be null
|
||||
* and must include at least one package.
|
||||
* @param observedEntities The list of packages and token to observe for usage time. Cannot be
|
||||
* null and must include at least one package or token.
|
||||
* @param timeLimit The total time the set of apps can be used continuously before the {@code
|
||||
* limitReachedCallbackIntent} is delivered. Must be at least one minute.
|
||||
* @param timeUnit The unit for time specified in {@code timeLimit}. Cannot be null.
|
||||
@@ -668,13 +675,13 @@ public final class UsageStatsManager {
|
||||
*/
|
||||
@SystemApi
|
||||
@RequiresPermission(android.Manifest.permission.OBSERVE_APP_USAGE)
|
||||
public void registerUsageSessionObserver(int sessionObserverId, @NonNull String[] packages,
|
||||
long timeLimit, @NonNull TimeUnit timeUnit, long sessionThresholdTime,
|
||||
@NonNull TimeUnit sessionThresholdTimeUnit,
|
||||
public void registerUsageSessionObserver(int sessionObserverId,
|
||||
@NonNull String[] observedEntities, long timeLimit, @NonNull TimeUnit timeUnit,
|
||||
long sessionThresholdTime, @NonNull TimeUnit sessionThresholdTimeUnit,
|
||||
@NonNull PendingIntent limitReachedCallbackIntent,
|
||||
@Nullable PendingIntent sessionEndCallbackIntent) {
|
||||
try {
|
||||
mService.registerUsageSessionObserver(sessionObserverId, packages,
|
||||
mService.registerUsageSessionObserver(sessionObserverId, observedEntities,
|
||||
timeUnit.toMillis(timeLimit),
|
||||
sessionThresholdTimeUnit.toMillis(sessionThresholdTime),
|
||||
limitReachedCallbackIntent, sessionEndCallbackIntent,
|
||||
@@ -704,6 +711,71 @@ public final class UsageStatsManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Report usage associated with a particular {@code token} has started. Tokens are app defined
|
||||
* strings used to represent usage of in-app features. Apps with the {@link
|
||||
* android.Manifest.permission#OBSERVE_APP_USAGE} permission can register time limit observers
|
||||
* to monitor the usage of a token. In app usage can only associated with an {@code activity}
|
||||
* and usage will be considered stopped if the activity stops or crashes.
|
||||
* @see #registerAppUsageObserver
|
||||
* @see #registerUsageSessionObserver
|
||||
*
|
||||
* @param activity The activity {@code token} is associated with.
|
||||
* @param token The token to report usage against.
|
||||
* @hide
|
||||
*/
|
||||
@SystemApi
|
||||
public void reportUsageStart(@NonNull Activity activity, @NonNull String token) {
|
||||
try {
|
||||
mService.reportUsageStart(activity.getActivityToken(), token,
|
||||
mContext.getOpPackageName());
|
||||
} catch (RemoteException e) {
|
||||
throw e.rethrowFromSystemServer();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Report usage associated with a particular {@code token} had started some amount of time in
|
||||
* the past. Tokens are app defined strings used to represent usage of in-app features. Apps
|
||||
* with the {@link android.Manifest.permission#OBSERVE_APP_USAGE} permission can register time
|
||||
* limit observers to monitor the usage of a token. In app usage can only associated with an
|
||||
* {@code activity} and usage will be considered stopped if the activity stops or crashes.
|
||||
* @see #registerAppUsageObserver
|
||||
* @see #registerUsageSessionObserver
|
||||
*
|
||||
* @param activity The activity {@code token} is associated with.
|
||||
* @param token The token to report usage against.
|
||||
* @param timeAgoMs How long ago the start of usage took place
|
||||
* @hide
|
||||
*/
|
||||
@SystemApi
|
||||
public void reportUsageStart(@NonNull Activity activity, @NonNull String token,
|
||||
long timeAgoMs) {
|
||||
try {
|
||||
mService.reportPastUsageStart(activity.getActivityToken(), token, timeAgoMs,
|
||||
mContext.getOpPackageName());
|
||||
} catch (RemoteException e) {
|
||||
throw e.rethrowFromSystemServer();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Report the usage associated with a particular {@code token} has stopped.
|
||||
*
|
||||
* @param activity The activity {@code token} is associated with.
|
||||
* @param token The token to report usage against.
|
||||
* @hide
|
||||
*/
|
||||
@SystemApi
|
||||
public void reportUsageStop(@NonNull Activity activity, @NonNull String token) {
|
||||
try {
|
||||
mService.reportUsageStop(activity.getActivityToken(), token,
|
||||
mContext.getOpPackageName());
|
||||
} catch (RemoteException e) {
|
||||
throw e.rethrowFromSystemServer();
|
||||
}
|
||||
}
|
||||
|
||||
/** @hide */
|
||||
public static String reasonToString(int standbyReason) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
@@ -699,10 +699,52 @@ public class AppTimeLimitControllerTests {
|
||||
assertTrue(hasUsageSessionObserver(UID, OBS_ID1));
|
||||
}
|
||||
|
||||
/** Verify the timeout message is delivered at the right time after past usage was reported */
|
||||
@Test
|
||||
public void testAppUsageObserver_PastUsage() throws Exception {
|
||||
setTime(10_000L);
|
||||
addAppUsageObserver(OBS_ID1, GROUP1, 6_000L);
|
||||
setTime(20_000L);
|
||||
startPastUsage(PKG_SOC1, 5_000);
|
||||
setTime(21_000L);
|
||||
assertTrue(mLimitReachedLatch.await(2_000L, TimeUnit.MILLISECONDS));
|
||||
stopUsage(PKG_SOC1);
|
||||
// Verify that the observer was removed
|
||||
assertFalse(hasAppUsageObserver(UID, OBS_ID1));
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the timeout message is delivered at the right time after past usage was reported
|
||||
* that overlaps with already known usage
|
||||
*/
|
||||
@Test
|
||||
public void testAppUsageObserver_PastUsageOverlap() throws Exception {
|
||||
setTime(0L);
|
||||
addAppUsageObserver(OBS_ID1, GROUP1, 20_000L);
|
||||
setTime(10_000L);
|
||||
startUsage(PKG_SOC1);
|
||||
setTime(20_000L);
|
||||
stopUsage(PKG_SOC1);
|
||||
setTime(25_000L);
|
||||
startPastUsage(PKG_SOC1, 9_000);
|
||||
setTime(26_000L);
|
||||
// the 4 seconds of overlapped usage should not be counted
|
||||
assertFalse(mLimitReachedLatch.await(2_000L, TimeUnit.MILLISECONDS));
|
||||
setTime(30_000L);
|
||||
assertTrue(mLimitReachedLatch.await(4_000L, TimeUnit.MILLISECONDS));
|
||||
stopUsage(PKG_SOC1);
|
||||
// Verify that the observer was removed
|
||||
assertFalse(hasAppUsageObserver(UID, OBS_ID1));
|
||||
}
|
||||
|
||||
private void startUsage(String packageName) {
|
||||
mController.noteUsageStart(packageName, USER_ID);
|
||||
}
|
||||
|
||||
private void startPastUsage(String packageName, int timeAgo) {
|
||||
mController.noteUsageStart(packageName, USER_ID, timeAgo);
|
||||
}
|
||||
|
||||
private void stopUsage(String packageName) {
|
||||
mController.noteUsageStop(packageName, USER_ID);
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ import android.os.Looper;
|
||||
import android.os.Message;
|
||||
import android.os.SystemClock;
|
||||
import android.util.ArrayMap;
|
||||
import android.util.ArraySet;
|
||||
import android.util.Slog;
|
||||
import android.util.SparseArray;
|
||||
|
||||
@@ -62,6 +61,8 @@ public class AppTimeLimitController {
|
||||
|
||||
private static final long ONE_MINUTE = 60_000L;
|
||||
|
||||
private static final Integer ONE = new Integer(1);
|
||||
|
||||
/** Collection of data for each user that has reported usage */
|
||||
@GuardedBy("mLock")
|
||||
private final SparseArray<UserData> mUsers = new SparseArray<>();
|
||||
@@ -79,11 +80,11 @@ public class AppTimeLimitController {
|
||||
private @UserIdInt
|
||||
int userId;
|
||||
|
||||
/** Set of the currently active entities */
|
||||
private final ArraySet<String> currentlyActive = new ArraySet<>();
|
||||
/** Count of the currently active entities */
|
||||
public final ArrayMap<String, Integer> currentlyActive = new ArrayMap<>();
|
||||
|
||||
/** Map from entity name for quick lookup */
|
||||
private final ArrayMap<String, ArrayList<UsageGroup>> observedMap = new ArrayMap<>();
|
||||
public final ArrayMap<String, ArrayList<UsageGroup>> observedMap = new ArrayMap<>();
|
||||
|
||||
private UserData(@UserIdInt int userId) {
|
||||
this.userId = userId;
|
||||
@@ -94,7 +95,7 @@ public class AppTimeLimitController {
|
||||
// TODO: Consider using a bloom filter here if number of actives becomes large
|
||||
final int size = entities.length;
|
||||
for (int i = 0; i < size; i++) {
|
||||
if (currentlyActive.contains(entities[i])) {
|
||||
if (currentlyActive.containsKey(entities[i])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -137,7 +138,7 @@ public class AppTimeLimitController {
|
||||
pw.print(" Currently Active:");
|
||||
final int nActive = currentlyActive.size();
|
||||
for (int i = 0; i < nActive; i++) {
|
||||
pw.print(currentlyActive.valueAt(i));
|
||||
pw.print(currentlyActive.keyAt(i));
|
||||
pw.print(", ");
|
||||
}
|
||||
pw.println();
|
||||
@@ -233,6 +234,7 @@ public class AppTimeLimitController {
|
||||
protected long mUsageTimeMs;
|
||||
protected int mActives;
|
||||
protected long mLastKnownUsageTimeMs;
|
||||
protected long mLastUsageEndTimeMs;
|
||||
protected WeakReference<UserData> mUserRef;
|
||||
protected WeakReference<ObserverAppData> mObserverAppRef;
|
||||
protected PendingIntent mLimitReachedCallback;
|
||||
@@ -271,9 +273,15 @@ public class AppTimeLimitController {
|
||||
@GuardedBy("mLock")
|
||||
void noteUsageStart(long startTimeMs, long currentTimeMs) {
|
||||
if (mActives++ == 0) {
|
||||
// If last known usage ended after the start of this usage, there is overlap
|
||||
// between the last usage session and this one. Avoid double counting by only
|
||||
// counting from the end of the last session. This has a rare side effect that some
|
||||
// usage will not be accounted for if the previous session started and stopped
|
||||
// within this current usage.
|
||||
startTimeMs = mLastUsageEndTimeMs > startTimeMs ? mLastUsageEndTimeMs : startTimeMs;
|
||||
mLastKnownUsageTimeMs = startTimeMs;
|
||||
final long timeRemaining =
|
||||
mTimeLimitMs - mUsageTimeMs + currentTimeMs - startTimeMs;
|
||||
mTimeLimitMs - mUsageTimeMs - currentTimeMs + startTimeMs;
|
||||
if (timeRemaining > 0) {
|
||||
if (DEBUG) {
|
||||
Slog.d(TAG, "Posting timeout for " + mObserverId + " for "
|
||||
@@ -287,7 +295,7 @@ public class AppTimeLimitController {
|
||||
mActives = mObserved.length;
|
||||
final UserData user = mUserRef.get();
|
||||
if (user == null) return;
|
||||
final Object[] array = user.currentlyActive.toArray();
|
||||
final Object[] array = user.currentlyActive.keySet().toArray();
|
||||
Slog.e(TAG,
|
||||
"Too many noted usage starts! Observed entities: " + Arrays.toString(
|
||||
mObserved) + " Active Entities: " + Arrays.toString(array));
|
||||
@@ -300,6 +308,8 @@ public class AppTimeLimitController {
|
||||
if (--mActives == 0) {
|
||||
final boolean limitNotCrossed = mUsageTimeMs < mTimeLimitMs;
|
||||
mUsageTimeMs += stopTimeMs - mLastKnownUsageTimeMs;
|
||||
|
||||
mLastUsageEndTimeMs = stopTimeMs;
|
||||
if (limitNotCrossed && mUsageTimeMs >= mTimeLimitMs) {
|
||||
// Crossed the limit
|
||||
if (DEBUG) Slog.d(TAG, "MTB informing group obs=" + mObserverId);
|
||||
@@ -312,7 +322,7 @@ public class AppTimeLimitController {
|
||||
mActives = 0;
|
||||
final UserData user = mUserRef.get();
|
||||
if (user == null) return;
|
||||
final Object[] array = user.currentlyActive.toArray();
|
||||
final Object[] array = user.currentlyActive.keySet().toArray();
|
||||
Slog.e(TAG,
|
||||
"Too many noted usage stops! Observed entities: " + Arrays.toString(
|
||||
mObserved) + " Active Entities: " + Arrays.toString(array));
|
||||
@@ -409,7 +419,6 @@ public class AppTimeLimitController {
|
||||
}
|
||||
|
||||
class SessionUsageGroup extends UsageGroup {
|
||||
private long mLastUsageEndTimeMs;
|
||||
private long mNewSessionThresholdMs;
|
||||
private PendingIntent mSessionEndCallback;
|
||||
|
||||
@@ -451,7 +460,6 @@ public class AppTimeLimitController {
|
||||
public void noteUsageStop(long stopTimeMs) {
|
||||
super.noteUsageStop(stopTimeMs);
|
||||
if (mActives == 0) {
|
||||
mLastUsageEndTimeMs = stopTimeMs;
|
||||
if (mUsageTimeMs >= mTimeLimitMs) {
|
||||
// Usage has ended. Schedule the session end callback to be triggered once
|
||||
// the new session threshold has been reached
|
||||
@@ -467,7 +475,10 @@ public class AppTimeLimitController {
|
||||
UserData user = mUserRef.get();
|
||||
if (user == null) return;
|
||||
if (mListener != null) {
|
||||
mListener.onSessionEnd(mObserverId, user.userId, mUsageTimeMs, mSessionEndCallback);
|
||||
mListener.onSessionEnd(mObserverId,
|
||||
user.userId,
|
||||
mUsageTimeMs,
|
||||
mSessionEndCallback);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -599,7 +610,7 @@ public class AppTimeLimitController {
|
||||
// TODO: Consider using a bloom filter here if number of actives becomes large
|
||||
final int size = group.mObserved.length;
|
||||
for (int i = 0; i < size; i++) {
|
||||
if (user.currentlyActive.contains(group.mObserved[i])) {
|
||||
if (user.currentlyActive.containsKey(group.mObserved[i])) {
|
||||
// Entity is currently active. Start group's usage.
|
||||
group.noteUsageStart(currentTimeMs);
|
||||
}
|
||||
@@ -717,21 +728,28 @@ public class AppTimeLimitController {
|
||||
/**
|
||||
* Called when an entity becomes active.
|
||||
*
|
||||
* @param name The entity that became active
|
||||
* @param userId The user
|
||||
* @param name The entity that became active
|
||||
* @param userId The user
|
||||
* @param timeAgoMs Time since usage was started
|
||||
*/
|
||||
public void noteUsageStart(String name, int userId) throws IllegalArgumentException {
|
||||
public void noteUsageStart(String name, int userId, long timeAgoMs)
|
||||
throws IllegalArgumentException {
|
||||
synchronized (mLock) {
|
||||
UserData user = getOrCreateUserDataLocked(userId);
|
||||
if (DEBUG) Slog.d(TAG, "Usage entity " + name + " became active");
|
||||
if (user.currentlyActive.contains(name)) {
|
||||
throw new IllegalArgumentException(
|
||||
"Unable to start usage for " + name + ", already in use");
|
||||
|
||||
final int index = user.currentlyActive.indexOfKey(name);
|
||||
if (index >= 0) {
|
||||
final Integer count = user.currentlyActive.valueAt(index);
|
||||
if (count != null) {
|
||||
// There are multiple instances of this entity. Just increment the count.
|
||||
user.currentlyActive.setValueAt(index, count + 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
final long currentTime = getUptimeMillis();
|
||||
|
||||
// Add to the list of active entities
|
||||
user.currentlyActive.add(name);
|
||||
user.currentlyActive.put(name, ONE);
|
||||
|
||||
ArrayList<UsageGroup> groups = user.observedMap.get(name);
|
||||
if (groups == null) return;
|
||||
@@ -739,11 +757,21 @@ public class AppTimeLimitController {
|
||||
final int size = groups.size();
|
||||
for (int i = 0; i < size; i++) {
|
||||
UsageGroup group = groups.get(i);
|
||||
group.noteUsageStart(currentTime);
|
||||
group.noteUsageStart(currentTime - timeAgoMs, currentTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when an entity becomes active.
|
||||
*
|
||||
* @param name The entity that became active
|
||||
* @param userId The user
|
||||
*/
|
||||
public void noteUsageStart(String name, int userId) throws IllegalArgumentException {
|
||||
noteUsageStart(name, userId, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when an entity becomes inactive.
|
||||
*
|
||||
@@ -754,10 +782,21 @@ public class AppTimeLimitController {
|
||||
synchronized (mLock) {
|
||||
UserData user = getOrCreateUserDataLocked(userId);
|
||||
if (DEBUG) Slog.d(TAG, "Usage entity " + name + " became inactive");
|
||||
if (!user.currentlyActive.remove(name)) {
|
||||
|
||||
final int index = user.currentlyActive.indexOfKey(name);
|
||||
if (index < 0) {
|
||||
throw new IllegalArgumentException(
|
||||
"Unable to stop usage for " + name + ", not in use");
|
||||
}
|
||||
|
||||
final Integer count = user.currentlyActive.valueAt(index);
|
||||
if (!count.equals(ONE)) {
|
||||
// There are multiple instances of this entity. Just decrement the count.
|
||||
user.currentlyActive.setValueAt(index, count - 1);
|
||||
return;
|
||||
}
|
||||
|
||||
user.currentlyActive.removeAt(index);
|
||||
final long currentTime = getUptimeMillis();
|
||||
|
||||
// Check if any of the groups need to watch for this entity
|
||||
@@ -769,6 +808,7 @@ public class AppTimeLimitController {
|
||||
UsageGroup group = groups.get(i);
|
||||
group.noteUsageStop(currentTime);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -780,7 +820,8 @@ public class AppTimeLimitController {
|
||||
|
||||
@GuardedBy("mLock")
|
||||
private void postInformSessionEndListenerLocked(SessionUsageGroup group, long timeout) {
|
||||
mHandler.sendMessageDelayed(mHandler.obtainMessage(MyHandler.MSG_INFORM_SESSION_END, group),
|
||||
mHandler.sendMessageDelayed(
|
||||
mHandler.obtainMessage(MyHandler.MSG_INFORM_SESSION_END, group),
|
||||
timeout);
|
||||
}
|
||||
|
||||
@@ -800,7 +841,27 @@ public class AppTimeLimitController {
|
||||
mHandler.removeMessages(MyHandler.MSG_CHECK_TIMEOUT, group);
|
||||
}
|
||||
|
||||
void dump(PrintWriter pw) {
|
||||
void dump(String[] args, PrintWriter pw) {
|
||||
if (args != null) {
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
String arg = args[i];
|
||||
if ("actives".equals(arg)) {
|
||||
synchronized (mLock) {
|
||||
final int nUsers = mUsers.size();
|
||||
for (int user = 0; user < nUsers; user++) {
|
||||
final ArrayMap<String, Integer> actives =
|
||||
mUsers.valueAt(user).currentlyActive;
|
||||
final int nActive = actives.size();
|
||||
for (int active = 0; active < nActive; active++) {
|
||||
pw.println(actives.keyAt(active));
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
synchronized (mLock) {
|
||||
pw.println("\n App Time Limits");
|
||||
final int nUsers = mUsers.size();
|
||||
|
||||
@@ -53,6 +53,7 @@ import android.os.Binder;
|
||||
import android.os.Environment;
|
||||
import android.os.FileUtils;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.os.IDeviceIdleController;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
@@ -105,6 +106,8 @@ public class UsageStatsService extends SystemService implements
|
||||
private static final boolean ENABLE_KERNEL_UPDATES = true;
|
||||
private static final File KERNEL_COUNTER_FILE = new File("/proc/uid_procstat/set");
|
||||
|
||||
private static final char TOKEN_DELIMITER = '/';
|
||||
|
||||
// Handler message types.
|
||||
static final int MSG_REPORT_EVENT = 0;
|
||||
static final int MSG_FLUSH_TO_DISK = 1;
|
||||
@@ -135,6 +138,10 @@ public class UsageStatsService extends SystemService implements
|
||||
/** Manages app time limit observers */
|
||||
AppTimeLimitController mAppTimeLimit;
|
||||
|
||||
final SparseArray<ArraySet<String>> mUsageReporters = new SparseArray();
|
||||
final SparseArray<String> mVisibleActivities = new SparseArray();
|
||||
|
||||
|
||||
private UsageStatsManagerInternal.AppIdleStateChangeListener mStandbyChangeListener =
|
||||
new UsageStatsManagerInternal.AppIdleStateChangeListener() {
|
||||
@Override
|
||||
@@ -270,7 +277,7 @@ public class UsageStatsService extends SystemService implements
|
||||
mHandler.obtainMessage(MSG_REMOVE_USER, userId, 0).sendToTarget();
|
||||
}
|
||||
} else if (Intent.ACTION_USER_STARTED.equals(action)) {
|
||||
if (userId >=0) {
|
||||
if (userId >= 0) {
|
||||
mAppStandby.postCheckIdleStates(userId);
|
||||
}
|
||||
}
|
||||
@@ -434,17 +441,46 @@ public class UsageStatsService extends SystemService implements
|
||||
mAppStandby.reportEvent(event, elapsedRealtime, userId);
|
||||
switch (event.mEventType) {
|
||||
case Event.ACTIVITY_RESUMED:
|
||||
try {
|
||||
mAppTimeLimit.noteUsageStart(event.getPackageName(), userId);
|
||||
} catch (IllegalArgumentException iae) {
|
||||
Slog.e(TAG, "Failed to note usage start", iae);
|
||||
synchronized (mVisibleActivities) {
|
||||
mVisibleActivities.put(event.mInstanceId, event.getClassName());
|
||||
try {
|
||||
mAppTimeLimit.noteUsageStart(event.getPackageName(), userId);
|
||||
} catch (IllegalArgumentException iae) {
|
||||
Slog.e(TAG, "Failed to note usage start", iae);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case Event.ACTIVITY_PAUSED:
|
||||
try {
|
||||
mAppTimeLimit.noteUsageStop(event.getPackageName(), userId);
|
||||
} catch (IllegalArgumentException iae) {
|
||||
Slog.e(TAG, "Failed to note usage stop", iae);
|
||||
case Event.ACTIVITY_STOPPED:
|
||||
case Event.ACTIVITY_DESTROYED:
|
||||
ArraySet<String> tokens;
|
||||
synchronized (mUsageReporters) {
|
||||
tokens = mUsageReporters.removeReturnOld(event.mInstanceId);
|
||||
}
|
||||
if (tokens != null) {
|
||||
synchronized (tokens) {
|
||||
final int size = tokens.size();
|
||||
// Stop usage on behalf of a UsageReporter that stopped
|
||||
for (int i = 0; i < size; i++) {
|
||||
final String token = tokens.valueAt(i);
|
||||
try {
|
||||
mAppTimeLimit.noteUsageStop(
|
||||
buildFullToken(event.getPackageName(), token), userId);
|
||||
} catch (IllegalArgumentException iae) {
|
||||
Slog.w(TAG, "Failed to stop usage for during reporter death: "
|
||||
+ iae);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
synchronized (mVisibleActivities) {
|
||||
if (mVisibleActivities.removeReturnOld(event.mInstanceId) != null) {
|
||||
try {
|
||||
mAppTimeLimit.noteUsageStop(event.getPackageName(), userId);
|
||||
} catch (IllegalArgumentException iae) {
|
||||
Slog.w(TAG, "Failed to note usage stop", iae);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -599,6 +635,14 @@ public class UsageStatsService extends SystemService implements
|
||||
return beginTime <= currentTime && beginTime < endTime;
|
||||
}
|
||||
|
||||
private String buildFullToken(String packageName, String token) {
|
||||
final StringBuilder sb = new StringBuilder(packageName.length() + token.length() + 1);
|
||||
sb.append(packageName);
|
||||
sb.append(TOKEN_DELIMITER);
|
||||
sb.append(token);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private void flushToDiskLocked() {
|
||||
final int userCount = mUserState.size();
|
||||
for (int i = 0; i < userCount; i++) {
|
||||
@@ -627,8 +671,7 @@ public class UsageStatsService extends SystemService implements
|
||||
String arg = args[i];
|
||||
if ("--checkin".equals(arg)) {
|
||||
checkin = true;
|
||||
} else
|
||||
if ("-c".equals(arg)) {
|
||||
} else if ("-c".equals(arg)) {
|
||||
compact = true;
|
||||
} else if ("flush".equals(arg)) {
|
||||
flushToDiskLocked();
|
||||
@@ -637,6 +680,15 @@ public class UsageStatsService extends SystemService implements
|
||||
} else if ("is-app-standby-enabled".equals(arg)) {
|
||||
pw.println(mAppStandby.mAppIdleEnabled);
|
||||
return;
|
||||
} else if ("apptimelimit".equals(arg)) {
|
||||
if (i + 1 >= args.length) {
|
||||
mAppTimeLimit.dump(null, pw);
|
||||
} else {
|
||||
final String[] remainingArgs =
|
||||
Arrays.copyOfRange(args, i + 1, args.length);
|
||||
mAppTimeLimit.dump(remainingArgs, pw);
|
||||
}
|
||||
return;
|
||||
} else if (arg != null && !arg.startsWith("-")) {
|
||||
// Anything else that doesn't start with '-' is a pkg to filter
|
||||
pkg = arg;
|
||||
@@ -666,7 +718,7 @@ public class UsageStatsService extends SystemService implements
|
||||
mAppStandby.dumpState(args, pw);
|
||||
}
|
||||
|
||||
mAppTimeLimit.dump(pw);
|
||||
mAppTimeLimit.dump(null, pw);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1231,16 +1283,82 @@ public class UsageStatsService extends SystemService implements
|
||||
final int userId = UserHandle.getUserId(callingUid);
|
||||
final long token = Binder.clearCallingIdentity();
|
||||
try {
|
||||
UsageStatsService.this.unregisterUsageSessionObserver(callingUid, sessionObserverId, userId);
|
||||
UsageStatsService.this.unregisterUsageSessionObserver(callingUid, sessionObserverId,
|
||||
userId);
|
||||
} finally {
|
||||
Binder.restoreCallingIdentity(token);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reportUsageStart(IBinder activity, String token, String callingPackage) {
|
||||
reportPastUsageStart(activity, token, 0, callingPackage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reportPastUsageStart(IBinder activity, String token, long timeAgoMs,
|
||||
String callingPackage) {
|
||||
|
||||
final int callingUid = Binder.getCallingUid();
|
||||
final int userId = UserHandle.getUserId(callingUid);
|
||||
final long binderToken = Binder.clearCallingIdentity();
|
||||
try {
|
||||
ArraySet<String> tokens;
|
||||
synchronized (mUsageReporters) {
|
||||
tokens = mUsageReporters.get(activity.hashCode());
|
||||
if (tokens == null) {
|
||||
tokens = new ArraySet();
|
||||
mUsageReporters.put(activity.hashCode(), tokens);
|
||||
}
|
||||
}
|
||||
|
||||
synchronized (tokens) {
|
||||
if (!tokens.add(token)) {
|
||||
throw new IllegalArgumentException(token + " for " + callingPackage
|
||||
+ " is already reported as started for this activity");
|
||||
}
|
||||
}
|
||||
|
||||
mAppTimeLimit.noteUsageStart(buildFullToken(callingPackage, token),
|
||||
userId, timeAgoMs);
|
||||
} finally {
|
||||
Binder.restoreCallingIdentity(binderToken);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reportUsageStop(IBinder activity, String token, String callingPackage) {
|
||||
final int callingUid = Binder.getCallingUid();
|
||||
final int userId = UserHandle.getUserId(callingUid);
|
||||
final long binderToken = Binder.clearCallingIdentity();
|
||||
try {
|
||||
ArraySet<String> tokens;
|
||||
synchronized (mUsageReporters) {
|
||||
tokens = mUsageReporters.get(activity.hashCode());
|
||||
if (tokens == null) {
|
||||
throw new IllegalArgumentException(
|
||||
"Unknown reporter trying to stop token " + token + " for "
|
||||
+ callingPackage);
|
||||
}
|
||||
}
|
||||
|
||||
synchronized (tokens) {
|
||||
if (!tokens.remove(token)) {
|
||||
throw new IllegalArgumentException(token + " for " + callingPackage
|
||||
+ " is already reported as stopped for this activity");
|
||||
}
|
||||
}
|
||||
mAppTimeLimit.noteUsageStop(buildFullToken(callingPackage, token), userId);
|
||||
} finally {
|
||||
Binder.restoreCallingIdentity(binderToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void registerAppUsageObserver(int callingUid, int observerId, String[] packages,
|
||||
long timeLimitMs, PendingIntent callbackIntent, int userId) {
|
||||
mAppTimeLimit.addAppUsageObserver(callingUid, observerId, packages, timeLimitMs, callbackIntent,
|
||||
mAppTimeLimit.addAppUsageObserver(callingUid, observerId, packages, timeLimitMs,
|
||||
callbackIntent,
|
||||
userId);
|
||||
}
|
||||
|
||||
|
||||
17
tests/UsageReportingTest/Android.mk
Normal file
17
tests/UsageReportingTest/Android.mk
Normal file
@@ -0,0 +1,17 @@
|
||||
LOCAL_PATH:= $(call my-dir)
|
||||
include $(CLEAR_VARS)
|
||||
|
||||
LOCAL_MODULE_TAGS := tests
|
||||
|
||||
# Only compile source java files in this apk.
|
||||
LOCAL_SRC_FILES := $(call all-java-files-under, src)
|
||||
|
||||
LOCAL_USE_AAPT2 := true
|
||||
LOCAL_STATIC_ANDROID_LIBRARIES := androidx.legacy_legacy-support-v4
|
||||
|
||||
LOCAL_CERTIFICATE := platform
|
||||
|
||||
LOCAL_PACKAGE_NAME := UsageReportingTest
|
||||
LOCAL_PRIVATE_PLATFORM_APIS := true
|
||||
|
||||
include $(BUILD_PACKAGE)
|
||||
22
tests/UsageReportingTest/AndroidManifest.xml
Normal file
22
tests/UsageReportingTest/AndroidManifest.xml
Normal file
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!--
|
||||
Note: Add android:sharedUserId="android.uid.system" to the root element to simulate the system UID
|
||||
caller case.
|
||||
-->
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.android.tests.usagereporter"
|
||||
>
|
||||
|
||||
<application android:label="@string/reporter_app">
|
||||
<activity android:name="UsageReporterActivity"
|
||||
android:label="UsageReporter">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
</application>
|
||||
</manifest>
|
||||
36
tests/UsageReportingTest/res/layout/row_item.xml
Normal file
36
tests/UsageReportingTest/res/layout/row_item.xml
Normal file
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 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.
|
||||
-->
|
||||
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingLeft="16dp"
|
||||
android:paddingRight="16dp"
|
||||
android:background="@color/inactive_color">
|
||||
|
||||
<TextView android:id="@+id/token"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:textStyle="bold"/>
|
||||
|
||||
<Button android:id="@+id/start" style="@style/ActionButton"
|
||||
android:text="@string/start" />
|
||||
|
||||
<Button android:id="@+id/stop" style="@style/ActionButton"
|
||||
android:text="@string/stop" />
|
||||
</LinearLayout>
|
||||
28
tests/UsageReportingTest/res/menu/main.xml
Normal file
28
tests/UsageReportingTest/res/menu/main.xml
Normal file
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<!-- 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.
|
||||
-->
|
||||
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:id="@+id/add_token"
|
||||
android:title="@string/add_token"/>
|
||||
<item android:id="@+id/add_many_tokens"
|
||||
android:title="@string/add_many_tokens"/>
|
||||
<item android:id="@+id/stop_all"
|
||||
android:title="@string/stop_all_tokens"/>
|
||||
<group android:checkableBehavior="all">
|
||||
<item android:id="@+id/restore_on_start"
|
||||
android:title="@string/restore_tokens_on_start"/>
|
||||
</group>
|
||||
</menu>
|
||||
19
tests/UsageReportingTest/res/values/colors.xml
Normal file
19
tests/UsageReportingTest/res/values/colors.xml
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 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.
|
||||
-->
|
||||
<resources>
|
||||
<color name="active_color">#FFF</color>
|
||||
<color name="inactive_color">#AAA</color>
|
||||
</resources>
|
||||
47
tests/UsageReportingTest/res/values/strings.xml
Normal file
47
tests/UsageReportingTest/res/values/strings.xml
Normal file
@@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 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.
|
||||
-->
|
||||
|
||||
<resources>
|
||||
<!-- Do not translate -->
|
||||
<string name="reporter_app">Usage Reporter App</string>
|
||||
<!-- Do not translate -->
|
||||
<string name="start">Start</string>
|
||||
<!-- Do not translate -->
|
||||
<string name="stop">Stop</string>
|
||||
<!-- Do not translate -->
|
||||
<string name="default_token">SuperSecretToken</string>
|
||||
|
||||
<!-- Do not translate -->
|
||||
<string name="add_token">Add Token</string>
|
||||
<!-- Do not translate -->
|
||||
<string name="add_many_tokens">Add Many Tokens</string>
|
||||
<!-- Do not translate -->
|
||||
<string name="stop_all_tokens">Stop All</string>
|
||||
<!-- Do not translate -->
|
||||
<string name="restore_tokens_on_start">Readd Tokens on Start</string>
|
||||
|
||||
|
||||
<!-- Do not translate -->
|
||||
<string name="token_query">Enter token(s) (delimit tokens with commas)</string>
|
||||
<!-- Do not translate -->
|
||||
<string name="many_tokens_query">Generate how many tokens?</string>
|
||||
<!-- Do not translate -->
|
||||
<string name="stop_all_tokens_query">Stop all tokens?</string>
|
||||
<!-- Do not translate -->
|
||||
<string name="ok">OK</string>
|
||||
<!-- Do not translate -->
|
||||
<string name="cancel">Cancel</string>
|
||||
</resources>
|
||||
31
tests/UsageReportingTest/res/values/styles.xml
Normal file
31
tests/UsageReportingTest/res/values/styles.xml
Normal file
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 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.
|
||||
-->
|
||||
|
||||
<resources>
|
||||
<style name="ActionButton">
|
||||
<item name="android:layout_width">wrap_content</item>
|
||||
<item name="android:layout_height">wrap_content</item>
|
||||
<item name="android:textAppearance">@style/TextAppearance.ActionButton</item>
|
||||
</style>
|
||||
|
||||
<style name="TextAppearance" parent="android:TextAppearance">
|
||||
</style>
|
||||
|
||||
<style name="TextAppearance.ActionButton">
|
||||
<item name="android:textStyle">italic</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
@@ -0,0 +1,320 @@
|
||||
/*
|
||||
* 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.tests.usagereporter;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.app.ListActivity;
|
||||
import android.app.usage.UsageStatsManager;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
import android.text.InputType;
|
||||
import android.text.TextUtils;
|
||||
import android.util.ArraySet;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.BaseAdapter;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class UsageReporterActivity extends ListActivity {
|
||||
|
||||
private Activity mActivity;
|
||||
private final ArrayList<String> mTokens = new ArrayList();
|
||||
private final ArraySet<String> mActives = new ArraySet();
|
||||
private UsageStatsManager mUsageStatsManager;
|
||||
private Adapter mAdapter;
|
||||
private boolean mRestoreOnStart = false;
|
||||
private static Context sContext;
|
||||
|
||||
/** Called with the activity is first created. */
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
sContext = getApplicationContext();
|
||||
|
||||
mUsageStatsManager = (UsageStatsManager) getSystemService(Context.USAGE_STATS_SERVICE);
|
||||
|
||||
mAdapter = new Adapter();
|
||||
setListAdapter(mAdapter);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
mActivity = this;
|
||||
|
||||
|
||||
if (mRestoreOnStart) {
|
||||
ArrayList<String> removed = null;
|
||||
for (String token : mActives) {
|
||||
try {
|
||||
mUsageStatsManager.reportUsageStart(mActivity, token);
|
||||
} catch (Exception e) {
|
||||
// Somthing went wrong, recover and move on
|
||||
if (removed == null) {
|
||||
removed = new ArrayList();
|
||||
}
|
||||
removed.add(token);
|
||||
}
|
||||
}
|
||||
if (removed != null) {
|
||||
for (String token : removed) {
|
||||
mActives.remove(token);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
mActives.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the activity is about to start interacting with the user.
|
||||
*/
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
mAdapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Called when your activity's options menu needs to be created.
|
||||
*/
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
MenuInflater inflater = getMenuInflater();
|
||||
inflater.inflate(R.menu.main, menu);
|
||||
return super.onCreateOptionsMenu(menu);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Called when a menu item is selected.
|
||||
*/
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.add_token:
|
||||
callAddToken();
|
||||
return true;
|
||||
case R.id.add_many_tokens:
|
||||
callAddManyTokens();
|
||||
return true;
|
||||
case R.id.stop_all:
|
||||
callStopAll();
|
||||
return true;
|
||||
case R.id.restore_on_start:
|
||||
mRestoreOnStart = !mRestoreOnStart;
|
||||
item.setChecked(mRestoreOnStart);
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
private void callAddToken() {
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||
builder.setTitle(getString(R.string.token_query));
|
||||
final EditText input = new EditText(this);
|
||||
input.setInputType(InputType.TYPE_CLASS_TEXT);
|
||||
input.setHint(getString(R.string.default_token));
|
||||
builder.setView(input);
|
||||
|
||||
builder.setPositiveButton(getString(R.string.ok), new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
String tokenNames = input.getText().toString().trim();
|
||||
if (TextUtils.isEmpty(tokenNames)) {
|
||||
tokenNames = getString(R.string.default_token);
|
||||
}
|
||||
String[] tokens = tokenNames.split(",");
|
||||
for (String token : tokens) {
|
||||
if (mTokens.contains(token)) continue;
|
||||
mTokens.add(token);
|
||||
}
|
||||
mAdapter.notifyDataSetChanged();
|
||||
|
||||
}
|
||||
});
|
||||
builder.setNegativeButton(getString(R.string.cancel),
|
||||
new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
dialog.cancel();
|
||||
}
|
||||
});
|
||||
|
||||
builder.show();
|
||||
}
|
||||
|
||||
private void callAddManyTokens() {
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||
builder.setTitle(getString(R.string.many_tokens_query));
|
||||
final EditText input = new EditText(this);
|
||||
input.setInputType(InputType.TYPE_CLASS_NUMBER);
|
||||
builder.setView(input);
|
||||
|
||||
builder.setPositiveButton(getString(R.string.ok),
|
||||
new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
String val = input.getText().toString().trim();
|
||||
if (TextUtils.isEmpty(val)) return;
|
||||
int n = Integer.parseInt(val);
|
||||
for (int i = 0; i < n; i++) {
|
||||
final String token = getString(R.string.default_token) + i;
|
||||
if (mTokens.contains(token)) continue;
|
||||
mTokens.add(token);
|
||||
}
|
||||
mAdapter.notifyDataSetChanged();
|
||||
|
||||
}
|
||||
});
|
||||
builder.setNegativeButton(getString(R.string.cancel),
|
||||
new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
dialog.cancel();
|
||||
}
|
||||
});
|
||||
|
||||
builder.show();
|
||||
}
|
||||
|
||||
private void callStopAll() {
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||
builder.setTitle(getString(R.string.stop_all_tokens_query));
|
||||
|
||||
builder.setPositiveButton(getString(R.string.ok), new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
for (String token : mActives) {
|
||||
mUsageStatsManager.reportUsageStop(mActivity, token);
|
||||
}
|
||||
mActives.clear();
|
||||
mAdapter.notifyDataSetChanged();
|
||||
}
|
||||
});
|
||||
builder.setNegativeButton(getString(R.string.cancel),
|
||||
new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
dialog.cancel();
|
||||
}
|
||||
});
|
||||
builder.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* A call-back for when the user presses the back button.
|
||||
*/
|
||||
OnClickListener mStartListener = new OnClickListener() {
|
||||
public void onClick(View v) {
|
||||
final View parent = (View) v.getParent();
|
||||
final String token = ((TextView) parent.findViewById(R.id.token)).getText().toString();
|
||||
try {
|
||||
mUsageStatsManager.reportUsageStart(mActivity, token);
|
||||
} catch (Exception e) {
|
||||
Toast.makeText(sContext, e.toString(), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
parent.setBackgroundColor(getColor(R.color.active_color));
|
||||
mActives.add(token);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* A call-back for when the user presses the clear button.
|
||||
*/
|
||||
OnClickListener mStopListener = new OnClickListener() {
|
||||
public void onClick(View v) {
|
||||
final View parent = (View) v.getParent();
|
||||
|
||||
final String token = ((TextView) parent.findViewById(R.id.token)).getText().toString();
|
||||
try {
|
||||
mUsageStatsManager.reportUsageStop(mActivity, token);
|
||||
} catch (Exception e) {
|
||||
Toast.makeText(sContext, e.toString(), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
parent.setBackgroundColor(getColor(R.color.inactive_color));
|
||||
mActives.remove(token);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
private class Adapter extends BaseAdapter {
|
||||
@Override
|
||||
public int getCount() {
|
||||
return mTokens.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getItem(int position) {
|
||||
return mTokens.get(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView(int position, View convertView, ViewGroup parent) {
|
||||
final ViewHolder holder;
|
||||
if (convertView == null) {
|
||||
convertView = LayoutInflater.from(UsageReporterActivity.this)
|
||||
.inflate(R.layout.row_item, parent, false);
|
||||
holder = new ViewHolder();
|
||||
holder.tokenName = (TextView) convertView.findViewById(R.id.token);
|
||||
|
||||
holder.startButton = ((Button) convertView.findViewById(R.id.start));
|
||||
holder.startButton.setOnClickListener(mStartListener);
|
||||
holder.stopButton = ((Button) convertView.findViewById(R.id.stop));
|
||||
holder.stopButton.setOnClickListener(mStopListener);
|
||||
convertView.setTag(holder);
|
||||
} else {
|
||||
holder = (ViewHolder) convertView.getTag();
|
||||
}
|
||||
|
||||
final String token = mTokens.get(position);
|
||||
holder.tokenName.setText(mTokens.get(position));
|
||||
if (mActives.contains(token)) {
|
||||
convertView.setBackgroundColor(getColor(R.color.active_color));
|
||||
} else {
|
||||
convertView.setBackgroundColor(getColor(R.color.inactive_color));
|
||||
}
|
||||
return convertView;
|
||||
}
|
||||
}
|
||||
|
||||
private static class ViewHolder {
|
||||
public TextView tokenName;
|
||||
public Button startButton;
|
||||
public Button stopButton;
|
||||
}
|
||||
}
|
||||
@@ -155,7 +155,7 @@ public class UsageStatsActivity extends ListActivity {
|
||||
intent.setPackage(getPackageName());
|
||||
intent.putExtra(EXTRA_KEY_TIMEOUT, true);
|
||||
mUsageStatsManager.registerAppUsageObserver(1, packages,
|
||||
30, TimeUnit.SECONDS, PendingIntent.getActivity(UsageStatsActivity.this,
|
||||
60, TimeUnit.SECONDS, PendingIntent.getActivity(UsageStatsActivity.this,
|
||||
1, intent, 0));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user