OSDN Git Service

Provide app launch count in UsageStats
authorAmith Yamasani <yamasani@google.com>
Wed, 21 Mar 2018 02:37:46 +0000 (19:37 -0700)
committerAmith Yamasani <yamasani@google.com>
Thu, 22 Mar 2018 20:51:57 +0000 (13:51 -0700)
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

api/system-current.txt
core/java/android/app/usage/UsageStats.java
core/java/android/app/usage/UsageStatsManager.java
core/res/AndroidManifest.xml
services/usage/java/com/android/server/usage/AppTimeLimitController.java
services/usage/java/com/android/server/usage/IntervalStats.java
services/usage/java/com/android/server/usage/UsageStatsService.java
services/usage/java/com/android/server/usage/UsageStatsXmlV1.java
services/usage/java/com/android/server/usage/UserUsageStatsService.java
tests/UsageStatsTest/AndroidManifest.xml

index 1080669..fb24dcb 100644 (file)
@@ -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();
index 7eef85c..2b14841 100644 (file)
@@ -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) {
index 59f001c..642c5b4 100644 (file)
@@ -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());
index 9b11a33..64c50ca 100644 (file)
         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"
index 9cd0593..e201851 100644 (file)
@@ -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);
             }
         }
     }
index 4d458b0..00826e0 100644 (file)
@@ -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) {
index b144545..b8317b4 100644 (file)
@@ -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();
index 5e7e80d..bcfc421 100644 (file)
@@ -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);
     }
index d974282..2452d13 100644 (file)
@@ -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();
index 66af454..4b1c1bd 100644 (file)
@@ -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"