summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAmith Yamasani <yamasani@google.com>2018-03-20 19:37:46 -0700
committerAmith Yamasani <yamasani@google.com>2018-03-22 13:51:57 -0700
commitbc813eb26e3027856114a26312e36e4bad86bd86 (patch)
treea19f4c10792816809309bf64bba518ea676f7372
parent9531b70cae96e8e59d38bd91d636cbe084574f3a (diff)
Provide app launch count in UsageStats
This counts the number of times the app was launched from outside the app and ignores intra-app activity transitions. Introduce a new permission for registering to observe app usage. Fixes a bug where Settings couldn't force the app into another bucket if it was recently launched. Bug: 74335821 Fixes: 76100712 Test: Manual test using Settings Test: UsageStatsTest to verify permission change Change-Id: Ibd343c1cfa37089a3ac6fc30ba3194e21a9be499
-rw-r--r--api/system-current.txt5
-rw-r--r--core/java/android/app/usage/UsageStats.java20
-rw-r--r--core/java/android/app/usage/UsageStatsManager.java32
-rw-r--r--core/res/AndroidManifest.xml5
-rw-r--r--services/usage/java/com/android/server/usage/AppTimeLimitController.java21
-rw-r--r--services/usage/java/com/android/server/usage/IntervalStats.java5
-rw-r--r--services/usage/java/com/android/server/usage/UsageStatsService.java31
-rw-r--r--services/usage/java/com/android/server/usage/UsageStatsXmlV1.java5
-rw-r--r--services/usage/java/com/android/server/usage/UserUsageStatsService.java16
-rw-r--r--tests/UsageStatsTest/AndroidManifest.xml1
10 files changed, 103 insertions, 38 deletions
diff --git a/api/system-current.txt b/api/system-current.txt
index 1080669e6ca5..fb24dcb195e1 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -118,6 +118,7 @@ package android {
field public static final java.lang.String MOVE_PACKAGE = "android.permission.MOVE_PACKAGE";
field public static final java.lang.String NOTIFICATION_DURING_SETUP = "android.permission.NOTIFICATION_DURING_SETUP";
field public static final java.lang.String NOTIFY_TV_INPUTS = "android.permission.NOTIFY_TV_INPUTS";
+ field public static final java.lang.String OBSERVE_APP_USAGE = "android.permission.OBSERVE_APP_USAGE";
field public static final java.lang.String OVERRIDE_WIFI_CONFIG = "android.permission.OVERRIDE_WIFI_CONFIG";
field public static final java.lang.String PACKAGE_USAGE_STATS = "android.permission.PACKAGE_USAGE_STATS";
field public static final java.lang.String PACKAGE_VERIFICATION_AGENT = "android.permission.PACKAGE_VERIFICATION_AGENT";
@@ -726,6 +727,10 @@ package android.app.usage {
field public static final int NOTIFICATION_SEEN = 10; // 0xa
}
+ public final class UsageStats implements android.os.Parcelable {
+ method public int getAppLaunchCount();
+ }
+
public final class UsageStatsManager {
method public int getAppStandbyBucket(java.lang.String);
method public java.util.Map<java.lang.String, java.lang.Integer> getAppStandbyBuckets();
diff --git a/core/java/android/app/usage/UsageStats.java b/core/java/android/app/usage/UsageStats.java
index 7eef85c44139..2b14841fb34b 100644
--- a/core/java/android/app/usage/UsageStats.java
+++ b/core/java/android/app/usage/UsageStats.java
@@ -16,6 +16,7 @@
package android.app.usage;
+import android.annotation.SystemApi;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
@@ -61,6 +62,11 @@ public final class UsageStats implements Parcelable {
/**
* {@hide}
*/
+ public int mAppLaunchCount;
+
+ /**
+ * {@hide}
+ */
public int mLastEvent;
/**
@@ -81,6 +87,7 @@ public final class UsageStats implements Parcelable {
mLastTimeUsed = stats.mLastTimeUsed;
mTotalTimeInForeground = stats.mTotalTimeInForeground;
mLaunchCount = stats.mLaunchCount;
+ mAppLaunchCount = stats.mAppLaunchCount;
mLastEvent = stats.mLastEvent;
mChooserCounts = stats.mChooserCounts;
}
@@ -137,6 +144,16 @@ public final class UsageStats implements Parcelable {
}
/**
+ * Returns the number of times the app was launched as an activity from outside of the app.
+ * Excludes intra-app activity transitions.
+ * @hide
+ */
+ @SystemApi
+ public int getAppLaunchCount() {
+ return mAppLaunchCount;
+ }
+
+ /**
* Add the statistics from the right {@link UsageStats} to the left. The package name for
* both {@link UsageStats} objects must be the same.
* @param right The {@link UsageStats} object to merge into this one.
@@ -161,6 +178,7 @@ public final class UsageStats implements Parcelable {
mEndTimeStamp = Math.max(mEndTimeStamp, right.mEndTimeStamp);
mTotalTimeInForeground += right.mTotalTimeInForeground;
mLaunchCount += right.mLaunchCount;
+ mAppLaunchCount += right.mAppLaunchCount;
if (mChooserCounts == null) {
mChooserCounts = right.mChooserCounts;
} else if (right.mChooserCounts != null) {
@@ -196,6 +214,7 @@ public final class UsageStats implements Parcelable {
dest.writeLong(mLastTimeUsed);
dest.writeLong(mTotalTimeInForeground);
dest.writeInt(mLaunchCount);
+ dest.writeInt(mAppLaunchCount);
dest.writeInt(mLastEvent);
Bundle allCounts = new Bundle();
if (mChooserCounts != null) {
@@ -224,6 +243,7 @@ public final class UsageStats implements Parcelable {
stats.mLastTimeUsed = in.readLong();
stats.mTotalTimeInForeground = in.readLong();
stats.mLaunchCount = in.readInt();
+ stats.mAppLaunchCount = in.readInt();
stats.mLastEvent = in.readInt();
Bundle allCounts = in.readBundle();
if (allCounts != null) {
diff --git a/core/java/android/app/usage/UsageStatsManager.java b/core/java/android/app/usage/UsageStatsManager.java
index 59f001c5a7c3..642c5b4df940 100644
--- a/core/java/android/app/usage/UsageStatsManager.java
+++ b/core/java/android/app/usage/UsageStatsManager.java
@@ -17,6 +17,7 @@
package android.app.usage;
import android.annotation.IntDef;
+import android.annotation.NonNull;
import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.annotation.SystemService;
@@ -500,26 +501,28 @@ public final class UsageStatsManager {
/**
* @hide
* Register an app usage limit observer that receives a callback on the provided intent when
- * the sum of usages of apps in the packages array exceeds the timeLimit specified. The
+ * 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.
+ * is delivered. Registering an {@code observerId} that was already registered will override
+ * the previous one.
* @param observerId A unique id associated with the group of apps to be monitored. There can
* be multiple groups with common packages and different time limits.
- * @param packages The list of packages to observe for foreground activity time. Must include
- * at least one package.
+ * @param packages The list of packages to observe for foreground activity time. Cannot be null
+ * and must include at least one package.
* @param timeLimit The total time the set of apps can be in the foreground before the
* callbackIntent is delivered. Must be greater than 0.
- * @param timeUnit The unit for time specified in timeLimit.
+ * @param timeUnit The unit for time specified in {@code timeLimit}. Cannot be null.
* @param callbackIntent The PendingIntent that will be dispatched when the time limit is
* exceeded by the group of apps. The delivered Intent will also contain
* the extras {@link #EXTRA_OBSERVER_ID}, {@link #EXTRA_TIME_LIMIT} and
- * {@link #EXTRA_TIME_USED}.
- * @throws SecurityException if the caller doesn't have the PACKAGE_USAGE_STATS permission.
+ * {@link #EXTRA_TIME_USED}. Cannot be null.
+ * @throws SecurityException if the caller doesn't have the OBSERVE_APP_USAGE permission or
+ * is not the profile owner of this user.
*/
@SystemApi
- @RequiresPermission(android.Manifest.permission.PACKAGE_USAGE_STATS)
- public void registerAppUsageObserver(int observerId, String[] packages, long timeLimit,
- TimeUnit timeUnit, PendingIntent callbackIntent) {
+ @RequiresPermission(android.Manifest.permission.OBSERVE_APP_USAGE)
+ public void registerAppUsageObserver(int observerId, @NonNull String[] packages, long timeLimit,
+ @NonNull TimeUnit timeUnit, @NonNull PendingIntent callbackIntent) {
try {
mService.registerAppUsageObserver(observerId, packages, timeUnit.toMillis(timeLimit),
callbackIntent, mContext.getOpPackageName());
@@ -529,14 +532,15 @@ public final class UsageStatsManager {
/**
* @hide
- * Unregister the app usage observer specified by the observerId. This will only apply to any
- * observer registered by this application. Unregistering an observer that was already
+ * Unregister the app usage observer specified by the {@code observerId}. This will only apply
+ * to any observer registered by this application. Unregistering an observer that was already
* unregistered or never registered will have no effect.
* @param observerId The id of the observer that was previously registered.
- * @throws SecurityException if the caller doesn't have the PACKAGE_USAGE_STATS permission.
+ * @throws SecurityException if the caller doesn't have the OBSERVE_APP_USAGE permission or is
+ * not the profile owner of this user.
*/
@SystemApi
- @RequiresPermission(android.Manifest.permission.PACKAGE_USAGE_STATS)
+ @RequiresPermission(android.Manifest.permission.OBSERVE_APP_USAGE)
public void unregisterAppUsageObserver(int observerId) {
try {
mService.unregisterAppUsageObserver(observerId, mContext.getOpPackageName());
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 9b11a33593bd..64c50ca6daa4 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -3274,6 +3274,11 @@
android:protectionLevel="signature|privileged|development|appop" />
<uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" />
+ <!-- @hide @SystemApi Allows an application to observe usage time of apps. The app can register
+ for callbacks when apps reach a certain usage time limit, etc. -->
+ <permission android:name="android.permission.OBSERVE_APP_USAGE"
+ android:protectionLevel="signature|privileged" />
+
<!-- @hide @SystemApi Allows an application to change the app idle state of an app.
<p>Not for use by third-party applications. -->
<permission android:name="android.permission.CHANGE_APP_IDLE_STATE"
diff --git a/services/usage/java/com/android/server/usage/AppTimeLimitController.java b/services/usage/java/com/android/server/usage/AppTimeLimitController.java
index 9cd05933d845..e20185114261 100644
--- a/services/usage/java/com/android/server/usage/AppTimeLimitController.java
+++ b/services/usage/java/com/android/server/usage/AppTimeLimitController.java
@@ -71,16 +71,13 @@ public class AppTimeLimitController {
/** The time when the current app came to the foreground */
private long currentForegroundedTime;
- /** The last app that was in the background */
- private String lastBackgroundedPackage;
-
/** Map from package name for quick lookup */
private ArrayMap<String, ArrayList<TimeLimitGroup>> packageMap = new ArrayMap<>();
/** Map of observerId to details of the time limit group */
private SparseArray<TimeLimitGroup> groups = new SparseArray<>();
- UserData(@UserIdInt int userId) {
+ private UserData(@UserIdInt int userId) {
this.userId = userId;
}
}
@@ -114,7 +111,7 @@ public class AppTimeLimitController {
int userId;
}
- class MyHandler extends Handler {
+ private class MyHandler extends Handler {
static final int MSG_CHECK_TIMEOUT = 1;
static final int MSG_INFORM_LISTENER = 2;
@@ -151,7 +148,7 @@ public class AppTimeLimitController {
}
/** Returns an existing UserData object for the given userId, or creates one */
- UserData getOrCreateUserDataLocked(int userId) {
+ private UserData getOrCreateUserDataLocked(int userId) {
UserData userData = mUsers.get(userId);
if (userData == null) {
userData = new UserData(userId);
@@ -258,12 +255,6 @@ public class AppTimeLimitController {
user.currentForegroundedPackage = packageName;
user.currentForegroundedTime = getUptimeMillis();
- // Check if the last package that was backgrounded is the same as this one
- if (!TextUtils.equals(packageName, user.lastBackgroundedPackage)) {
- // TODO: Move this logic up to usage stats to persist there.
- incTotalLaunchesLocked(user, packageName);
- }
-
// Check if any of the groups need to watch for this package
maybeWatchForPackageLocked(user, packageName, user.currentForegroundedTime);
}
@@ -279,7 +270,6 @@ public class AppTimeLimitController {
public void moveToBackground(String packageName, String className, int userId) {
synchronized (mLock) {
UserData user = getOrCreateUserDataLocked(userId);
- user.lastBackgroundedPackage = packageName;
if (!TextUtils.equals(user.currentForegroundedPackage, packageName)) {
Slog.w(TAG, "Eh? Last foregrounded package = " + user.currentForegroundedPackage
+ " and now backgrounded = " + packageName);
@@ -433,10 +423,6 @@ public class AppTimeLimitController {
}
}
- private void incTotalLaunchesLocked(UserData user, String packageName) {
- // TODO: Inform UsageStatsService and aggregate the counter per app
- }
-
void dump(PrintWriter pw) {
synchronized (mLock) {
pw.println("\n App Time Limits");
@@ -457,7 +443,6 @@ public class AppTimeLimitController {
pw.println();
pw.print(" currentForegroundedPackage=");
pw.println(user.currentForegroundedPackage);
- pw.print(" lastBackgroundedPackage="); pw.println(user.lastBackgroundedPackage);
}
}
}
diff --git a/services/usage/java/com/android/server/usage/IntervalStats.java b/services/usage/java/com/android/server/usage/IntervalStats.java
index 4d458b027429..00826e057a86 100644
--- a/services/usage/java/com/android/server/usage/IntervalStats.java
+++ b/services/usage/java/com/android/server/usage/IntervalStats.java
@@ -166,6 +166,11 @@ class IntervalStats {
endTime = timeStamp;
}
+ void incrementAppLaunchCount(String packageName) {
+ UsageStats usageStats = getOrCreateUsageStats(packageName);
+ usageStats.mAppLaunchCount += 1;
+ }
+
private String getCachedStringRef(String str) {
final int index = mStringCache.indexOf(str);
if (index < 0) {
diff --git a/services/usage/java/com/android/server/usage/UsageStatsService.java b/services/usage/java/com/android/server/usage/UsageStatsService.java
index b144545c693a..b8317b470d01 100644
--- a/services/usage/java/com/android/server/usage/UsageStatsService.java
+++ b/services/usage/java/com/android/server/usage/UsageStatsService.java
@@ -21,6 +21,8 @@ import android.app.ActivityManager;
import android.app.AppOpsManager;
import android.app.IUidObserver;
import android.app.PendingIntent;
+import android.app.admin.DeviceAdminInfo;
+import android.app.admin.DevicePolicyManagerInternal;
import android.app.usage.AppStandbyInfo;
import android.app.usage.ConfigurationStats;
import android.app.usage.IUsageStatsManager;
@@ -110,6 +112,7 @@ public class UsageStatsService extends SystemService implements
PackageManager mPackageManager;
PackageManagerInternal mPackageManagerInternal;
IDeviceIdleController mDeviceIdleController;
+ DevicePolicyManagerInternal mDpmInternal;
private final SparseArray<UserUsageStatsService> mUserState = new SparseArray<>();
private final SparseIntArray mUidToKernelCounter = new SparseIntArray();
@@ -117,8 +120,10 @@ public class UsageStatsService extends SystemService implements
long mRealTimeSnapshot;
long mSystemTimeSnapshot;
+ /** Manages the standby state of apps. */
AppStandbyController mAppStandby;
+ /** Manages app time limit observers */
AppTimeLimitController mAppTimeLimit;
private UsageStatsManagerInternal.AppIdleStateChangeListener mStandbyChangeListener =
@@ -151,6 +156,7 @@ public class UsageStatsService extends SystemService implements
mUserManager = (UserManager) getContext().getSystemService(Context.USER_SERVICE);
mPackageManager = getContext().getPackageManager();
mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class);
+ mDpmInternal = LocalServices.getService(DevicePolicyManagerInternal.class);
mHandler = new H(BackgroundThread.get().getLooper());
mAppStandby = new AppStandbyController(getContext(), BackgroundThread.get().getLooper());
@@ -647,6 +653,19 @@ public class UsageStatsService extends SystemService implements
return mode == AppOpsManager.MODE_ALLOWED;
}
+ private boolean hasObserverPermission(String callingPackage) {
+ final int callingUid = Binder.getCallingUid();
+ if (callingUid == Process.SYSTEM_UID
+ || (mDpmInternal != null
+ && mDpmInternal.isActiveAdminWithPolicy(callingUid,
+ DeviceAdminInfo.USES_POLICY_PROFILE_OWNER))) {
+ // Caller is the system or the profile owner, so proceed.
+ return true;
+ }
+ return getContext().checkCallingPermission(Manifest.permission.OBSERVE_APP_USAGE)
+ == PackageManager.PERMISSION_GRANTED;
+ }
+
@Override
public ParceledListSlice<UsageStats> queryUsageStats(int bucketType, long beginTime,
long endTime, String callingPackage) {
@@ -821,8 +840,8 @@ public class UsageStatsService extends SystemService implements
} catch (RemoteException re) {
throw re.rethrowFromSystemServer();
}
- final boolean shellCaller = callingUid == 0 || callingUid == Process.SHELL_UID;
- final int reason = shellCaller
+ final boolean systemCaller = UserHandle.isCore(callingUid);
+ final int reason = systemCaller
? UsageStatsManager.REASON_MAIN_FORCED
: UsageStatsManager.REASON_MAIN_PREDICTED;
final long token = Binder.clearCallingIdentity();
@@ -963,8 +982,8 @@ public class UsageStatsService extends SystemService implements
public void registerAppUsageObserver(int observerId,
String[] packages, long timeLimitMs, PendingIntent
callbackIntent, String callingPackage) {
- if (!hasPermission(callingPackage)) {
- throw new SecurityException("Caller doesn't have PACKAGE_USAGE_STATS permission");
+ if (!hasObserverPermission(callingPackage)) {
+ throw new SecurityException("Caller doesn't have OBSERVE_APP_USAGE permission");
}
if (packages == null || packages.length == 0) {
@@ -989,8 +1008,8 @@ public class UsageStatsService extends SystemService implements
@Override
public void unregisterAppUsageObserver(int observerId, String callingPackage) {
- if (!hasPermission(callingPackage)) {
- throw new SecurityException("Caller doesn't have PACKAGE_USAGE_STATS permission");
+ if (!hasObserverPermission(callingPackage)) {
+ throw new SecurityException("Caller doesn't have OBSERVE_APP_USAGE permission");
}
final int callingUid = Binder.getCallingUid();
diff --git a/services/usage/java/com/android/server/usage/UsageStatsXmlV1.java b/services/usage/java/com/android/server/usage/UsageStatsXmlV1.java
index 5e7e80db410b..bcfc42169374 100644
--- a/services/usage/java/com/android/server/usage/UsageStatsXmlV1.java
+++ b/services/usage/java/com/android/server/usage/UsageStatsXmlV1.java
@@ -62,6 +62,7 @@ final class UsageStatsXmlV1 {
private static final String TYPE_ATTR = "type";
private static final String SHORTCUT_ID_ATTR = "shortcutId";
private static final String STANDBY_BUCKET_ATTR = "standbyBucket";
+ private static final String APP_LAUNCH_COUNT_ATTR = "appLaunchCount";
// Time attributes stored as an offset of the beginTime.
private static final String LAST_TIME_ACTIVE_ATTR = "lastTimeActive";
@@ -81,6 +82,7 @@ final class UsageStatsXmlV1 {
parser, LAST_TIME_ACTIVE_ATTR);
stats.mTotalTimeInForeground = XmlUtils.readLongAttribute(parser, TOTAL_TIME_ACTIVE_ATTR);
stats.mLastEvent = XmlUtils.readIntAttribute(parser, LAST_EVENT_ATTR);
+ stats.mAppLaunchCount = XmlUtils.readIntAttribute(parser, APP_LAUNCH_COUNT_ATTR, 0);
int eventCode;
while ((eventCode = parser.next()) != XmlPullParser.END_DOCUMENT) {
final String tag = parser.getName();
@@ -193,6 +195,9 @@ final class UsageStatsXmlV1 {
XmlUtils.writeStringAttribute(xml, PACKAGE_ATTR, usageStats.mPackageName);
XmlUtils.writeLongAttribute(xml, TOTAL_TIME_ACTIVE_ATTR, usageStats.mTotalTimeInForeground);
XmlUtils.writeIntAttribute(xml, LAST_EVENT_ATTR, usageStats.mLastEvent);
+ if (usageStats.mAppLaunchCount > 0) {
+ XmlUtils.writeIntAttribute(xml, APP_LAUNCH_COUNT_ATTR, usageStats.mAppLaunchCount);
+ }
writeChooserCounts(xml, usageStats);
xml.endTag(null, PACKAGE_TAG);
}
diff --git a/services/usage/java/com/android/server/usage/UserUsageStatsService.java b/services/usage/java/com/android/server/usage/UserUsageStatsService.java
index d9742825ac70..2452d139dc86 100644
--- a/services/usage/java/com/android/server/usage/UserUsageStatsService.java
+++ b/services/usage/java/com/android/server/usage/UserUsageStatsService.java
@@ -61,6 +61,7 @@ class UserUsageStatsService {
private final UnixCalendar mDailyExpiryDate;
private final StatsUpdatedListener mListener;
private final String mLogPrefix;
+ private String mLastBackgroundedPackage;
private final int mUserId;
private static final long[] INTERVAL_LENGTH = new long[] {
@@ -178,6 +179,17 @@ class UserUsageStatsService {
currentDailyStats.events.put(event.mTimeStamp, event);
}
+ boolean incrementAppLaunch = false;
+ if (event.mEventType == UsageEvents.Event.MOVE_TO_FOREGROUND) {
+ if (event.mPackage != null && !event.mPackage.equals(mLastBackgroundedPackage)) {
+ incrementAppLaunch = true;
+ }
+ } else if (event.mEventType == UsageEvents.Event.MOVE_TO_BACKGROUND) {
+ if (event.mPackage != null) {
+ mLastBackgroundedPackage = event.mPackage;
+ }
+ }
+
for (IntervalStats stats : mCurrentStats) {
if (event.mEventType == UsageEvents.Event.CONFIGURATION_CHANGE) {
stats.updateConfigurationStats(newFullConfig, event.mTimeStamp);
@@ -191,6 +203,9 @@ class UserUsageStatsService {
}
} else {
stats.update(event.mPackage, event.mTimeStamp, event.mEventType);
+ if (incrementAppLaunch) {
+ stats.incrementAppLaunchCount(event.mPackage);
+ }
}
}
@@ -649,6 +664,7 @@ class UserUsageStatsService {
pw.printPair("totalTime",
formatElapsedTime(usageStats.mTotalTimeInForeground, prettyDates));
pw.printPair("lastTime", formatDateTime(usageStats.mLastTimeUsed, prettyDates));
+ pw.printPair("appLaunchCount", usageStats.mAppLaunchCount);
pw.println();
}
pw.decreaseIndent();
diff --git a/tests/UsageStatsTest/AndroidManifest.xml b/tests/UsageStatsTest/AndroidManifest.xml
index 66af45424fba..4b1c1bd69920 100644
--- a/tests/UsageStatsTest/AndroidManifest.xml
+++ b/tests/UsageStatsTest/AndroidManifest.xml
@@ -10,6 +10,7 @@
>
<uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" />
+ <uses-permission android:name="android.permission.OBSERVE_APP_USAGE" />
<application android:label="Usage Access Test">
<activity android:name=".UsageStatsActivity"