summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMichael Wachenschwanz <mwachens@google.com>2018-11-12 11:06:19 -0800
committerMichael Wachenschwanz <mwachens@google.com>2019-01-07 14:56:06 -0800
commit36778525bacc646742f42e74a83fe2f563e4d0ef (patch)
tree1ccb3a130f725269e36f1ff7b65160bbbf951212
parent183bdcf1d3e764dcf19fb9da38b96bed7f7f52a4 (diff)
Add Usage Reporting Api to UsageStatsManager
The Usage Reporting Api allows apps to report usage within the app to platform. Apps with the the OBSERVE_APP_USAGE permission may register observers that use the reported in-app usage. Test: manual (using the included Usage Reporter App) Test: atest CtsUsageStatsTestCases:UsageReportingTest Test: atest FrameworksServicesTests:AppTimeLimitControllerTests Bug: 112486938 Change-Id: Iddd6f0993bbbf68a2032b34d473ef8d67da7747a
-rw-r--r--api/system-current.txt3
-rw-r--r--core/java/android/app/usage/IUsageStatsManager.aidl4
-rw-r--r--core/java/android/app/usage/UsageStatsManager.java118
-rw-r--r--services/tests/servicestests/src/com/android/server/usage/AppTimeLimitControllerTests.java42
-rw-r--r--services/usage/java/com/android/server/usage/AppTimeLimitController.java111
-rw-r--r--services/usage/java/com/android/server/usage/UsageStatsService.java148
-rw-r--r--tests/UsageReportingTest/Android.mk17
-rw-r--r--tests/UsageReportingTest/AndroidManifest.xml22
-rw-r--r--tests/UsageReportingTest/res/layout/row_item.xml36
-rw-r--r--tests/UsageReportingTest/res/menu/main.xml28
-rw-r--r--tests/UsageReportingTest/res/values/colors.xml19
-rw-r--r--tests/UsageReportingTest/res/values/strings.xml47
-rw-r--r--tests/UsageReportingTest/res/values/styles.xml31
-rw-r--r--tests/UsageReportingTest/src/com/android/tests/usagereporter/UsageReporterActivity.java320
-rw-r--r--tests/UsageStatsTest/src/com/android/tests/usagestats/UsageStatsActivity.java2
15 files changed, 884 insertions, 64 deletions
diff --git a/api/system-current.txt b/api/system-current.txt
index 09dca1e1b298..ecff03994a92 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -917,6 +917,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);
diff --git a/core/java/android/app/usage/IUsageStatsManager.aidl b/core/java/android/app/usage/IUsageStatsManager.aidl
index 4d52263c1d78..bbae7d3463ae 100644
--- a/core/java/android/app/usage/IUsageStatsManager.aidl
+++ b/core/java/android/app/usage/IUsageStatsManager.aidl
@@ -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);
}
diff --git a/core/java/android/app/usage/UsageStatsManager.java b/core/java/android/app/usage/UsageStatsManager.java
index 3f348033a8aa..724a71c202f3 100644
--- a/core/java/android/app/usage/UsageStatsManager.java
+++ b/core/java/android/app/usage/UsageStatsManager.java
@@ -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();
diff --git a/services/tests/servicestests/src/com/android/server/usage/AppTimeLimitControllerTests.java b/services/tests/servicestests/src/com/android/server/usage/AppTimeLimitControllerTests.java
index 8496a961959d..b348aeef802e 100644
--- a/services/tests/servicestests/src/com/android/server/usage/AppTimeLimitControllerTests.java
+++ b/services/tests/servicestests/src/com/android/server/usage/AppTimeLimitControllerTests.java
@@ -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);
}
diff --git a/services/usage/java/com/android/server/usage/AppTimeLimitController.java b/services/usage/java/com/android/server/usage/AppTimeLimitController.java
index 8e1ede116abf..2ed11fe92e15 100644
--- a/services/usage/java/com/android/server/usage/AppTimeLimitController.java
+++ b/services/usage/java/com/android/server/usage/AppTimeLimitController.java
@@ -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,12 +757,22 @@ 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.
*
* @param name The entity that became 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();
diff --git a/services/usage/java/com/android/server/usage/UsageStatsService.java b/services/usage/java/com/android/server/usage/UsageStatsService.java
index 57dc08fcd253..f146370b01d7 100644
--- a/services/usage/java/com/android/server/usage/UsageStatsService.java
+++ b/services/usage/java/com/android/server/usage/UsageStatsService.java
@@ -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);
}
diff --git a/tests/UsageReportingTest/Android.mk b/tests/UsageReportingTest/Android.mk
new file mode 100644
index 000000000000..afb6e16b1fdf
--- /dev/null
+++ b/tests/UsageReportingTest/Android.mk
@@ -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)
diff --git a/tests/UsageReportingTest/AndroidManifest.xml b/tests/UsageReportingTest/AndroidManifest.xml
new file mode 100644
index 000000000000..be0b09e972a5
--- /dev/null
+++ b/tests/UsageReportingTest/AndroidManifest.xml
@@ -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>
diff --git a/tests/UsageReportingTest/res/layout/row_item.xml b/tests/UsageReportingTest/res/layout/row_item.xml
new file mode 100644
index 000000000000..1eb2dab29124
--- /dev/null
+++ b/tests/UsageReportingTest/res/layout/row_item.xml
@@ -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>
diff --git a/tests/UsageReportingTest/res/menu/main.xml b/tests/UsageReportingTest/res/menu/main.xml
new file mode 100644
index 000000000000..9847c2dce8f2
--- /dev/null
+++ b/tests/UsageReportingTest/res/menu/main.xml
@@ -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> \ No newline at end of file
diff --git a/tests/UsageReportingTest/res/values/colors.xml b/tests/UsageReportingTest/res/values/colors.xml
new file mode 100644
index 000000000000..03bcf8a60182
--- /dev/null
+++ b/tests/UsageReportingTest/res/values/colors.xml
@@ -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> \ No newline at end of file
diff --git a/tests/UsageReportingTest/res/values/strings.xml b/tests/UsageReportingTest/res/values/strings.xml
new file mode 100644
index 000000000000..015290e732a0
--- /dev/null
+++ b/tests/UsageReportingTest/res/values/strings.xml
@@ -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> \ No newline at end of file
diff --git a/tests/UsageReportingTest/res/values/styles.xml b/tests/UsageReportingTest/res/values/styles.xml
new file mode 100644
index 000000000000..e5b86c5e836b
--- /dev/null
+++ b/tests/UsageReportingTest/res/values/styles.xml
@@ -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>
diff --git a/tests/UsageReportingTest/src/com/android/tests/usagereporter/UsageReporterActivity.java b/tests/UsageReportingTest/src/com/android/tests/usagereporter/UsageReporterActivity.java
new file mode 100644
index 000000000000..946be8fe93d3
--- /dev/null
+++ b/tests/UsageReportingTest/src/com/android/tests/usagereporter/UsageReporterActivity.java
@@ -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;
+ }
+}
diff --git a/tests/UsageStatsTest/src/com/android/tests/usagestats/UsageStatsActivity.java b/tests/UsageStatsTest/src/com/android/tests/usagestats/UsageStatsActivity.java
index 3d8ce21a2c00..3c628f6e0013 100644
--- a/tests/UsageStatsTest/src/com/android/tests/usagestats/UsageStatsActivity.java
+++ b/tests/UsageStatsTest/src/com/android/tests/usagestats/UsageStatsActivity.java
@@ -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));
}
}