OSDN Git Service

ShortcutManager: Implement usage report API
authorMakoto Onuki <omakoto@google.com>
Fri, 20 May 2016 23:39:42 +0000 (16:39 -0700)
committerMakoto Onuki <omakoto@google.com>
Thu, 2 Jun 2016 22:57:40 +0000 (15:57 -0700)
Bug 28536054

Change-Id: I293e8eaad523e3b0d76d562fc381601633451ed3

12 files changed:
api/current.txt
api/system-current.txt
api/test-current.txt
core/java/android/app/usage/UsageEvents.java
core/java/android/app/usage/UsageStatsManagerInternal.java
core/java/android/content/pm/ShortcutManager.java
services/core/java/com/android/server/pm/ShortcutService.java
services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java
services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest2.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

index 559c2ff..f6057c6 100644 (file)
@@ -6493,11 +6493,13 @@ package android.app.usage {
     method public android.content.res.Configuration getConfiguration();
     method public int getEventType();
     method public java.lang.String getPackageName();
+    method public java.lang.String getShortcutId();
     method public long getTimeStamp();
     field public static final int CONFIGURATION_CHANGE = 5; // 0x5
     field public static final int MOVE_TO_BACKGROUND = 2; // 0x2
     field public static final int MOVE_TO_FOREGROUND = 1; // 0x1
     field public static final int NONE = 0; // 0x0
+    field public static final int SHORTCUT_INVOCATION = 8; // 0x8
     field public static final int USER_INTERACTION = 7; // 0x7
   }
 
index 360eabc..65e1017 100644 (file)
@@ -6774,11 +6774,13 @@ package android.app.usage {
     method public android.content.res.Configuration getConfiguration();
     method public int getEventType();
     method public java.lang.String getPackageName();
+    method public java.lang.String getShortcutId();
     method public long getTimeStamp();
     field public static final int CONFIGURATION_CHANGE = 5; // 0x5
     field public static final int MOVE_TO_BACKGROUND = 2; // 0x2
     field public static final int MOVE_TO_FOREGROUND = 1; // 0x1
     field public static final int NONE = 0; // 0x0
+    field public static final int SHORTCUT_INVOCATION = 8; // 0x8
     field public static final int USER_INTERACTION = 7; // 0x7
   }
 
index 72da2b4..e450020 100644 (file)
@@ -6499,11 +6499,13 @@ package android.app.usage {
     method public android.content.res.Configuration getConfiguration();
     method public int getEventType();
     method public java.lang.String getPackageName();
+    method public java.lang.String getShortcutId();
     method public long getTimeStamp();
     field public static final int CONFIGURATION_CHANGE = 5; // 0x5
     field public static final int MOVE_TO_BACKGROUND = 2; // 0x2
     field public static final int MOVE_TO_FOREGROUND = 1; // 0x1
     field public static final int NONE = 0; // 0x0
+    field public static final int SHORTCUT_INVOCATION = 8; // 0x8
     field public static final int USER_INTERACTION = 7; // 0x7
   }
 
index 24647f3..a0da258 100644 (file)
@@ -79,6 +79,13 @@ public final class UsageEvents implements Parcelable {
         public static final int USER_INTERACTION = 7;
 
         /**
+         * An event type denoting that an action equivalent to a ShortcutInfo is taken by the user.
+         *
+         * @see android.content.pm.ShortcutManager#reportShortcutUsed(String)
+         */
+        public static final int SHORTCUT_INVOCATION = 8;
+
+        /**
          * {@hide}
          */
         public String mPackage;
@@ -105,6 +112,13 @@ public final class UsageEvents implements Parcelable {
         public Configuration mConfiguration;
 
         /**
+         * ID of the shortcut.
+         * Only present for {@link #SHORTCUT_INVOCATION} event types.
+         * {@hide}
+         */
+        public String mShortcutId;
+
+        /**
          * The package name of the source of this event.
          */
         public String getPackageName() {
@@ -145,6 +159,16 @@ public final class UsageEvents implements Parcelable {
         public Configuration getConfiguration() {
             return mConfiguration;
         }
+
+        /**
+         * Returns the ID of a {@link android.content.pm.ShortcutInfo} for this event
+         * if the event is of type {@link #SHORTCUT_INVOCATION}, otherwise it returns null.
+         *
+         * @see android.content.pm.ShortcutManager#reportShortcutUsed(String)
+         */
+        public String getShortcutId() {
+            return mShortcutId;
+        }
     }
 
     // Only used when creating the resulting events. Not used for reading/unparceling.
@@ -276,8 +300,13 @@ public final class UsageEvents implements Parcelable {
         p.writeInt(event.mEventType);
         p.writeLong(event.mTimeStamp);
 
-        if (event.mEventType == Event.CONFIGURATION_CHANGE) {
-            event.mConfiguration.writeToParcel(p, flags);
+        switch (event.mEventType) {
+            case Event.CONFIGURATION_CHANGE:
+                event.mConfiguration.writeToParcel(p, flags);
+                break;
+            case Event.SHORTCUT_INVOCATION:
+                p.writeString(event.mShortcutId);
+                break;
         }
     }
 
@@ -301,11 +330,18 @@ public final class UsageEvents implements Parcelable {
         eventOut.mEventType = p.readInt();
         eventOut.mTimeStamp = p.readLong();
 
-        // Extract the configuration for configuration change events.
-        if (eventOut.mEventType == Event.CONFIGURATION_CHANGE) {
-            eventOut.mConfiguration = Configuration.CREATOR.createFromParcel(p);
-        } else {
-            eventOut.mConfiguration = null;
+        // Fill out the event-dependant fields.
+        eventOut.mConfiguration = null;
+        eventOut.mShortcutId = null;
+
+        switch (eventOut.mEventType) {
+            case Event.CONFIGURATION_CHANGE:
+                // Extract the configuration for configuration change events.
+                eventOut.mConfiguration = Configuration.CREATOR.createFromParcel(p);
+                break;
+            case Event.SHORTCUT_INVOCATION:
+                eventOut.mShortcutId = p.readString();
+                break;
         }
     }
 
index b6f1567..a6f91fe 100644 (file)
@@ -56,6 +56,17 @@ public abstract class UsageStatsManagerInternal {
     public abstract void reportConfigurationChange(Configuration config, int userId);
 
     /**
+     * Reports that an action equivalent to a ShortcutInfo is taken by the user.
+     *
+     * @param packageName The package name of the shortcut publisher
+     * @param shortcutId The ID of the shortcut in question
+     * @param userId The user in which the content provider was accessed.
+     *
+     * @see android.content.pm.ShortcutManager#reportShortcutUsed(String)
+     */
+    public abstract void reportShortcutUsage(String packageName, String shortcutId, int userId);
+
+    /**
      * Reports that a content provider has been accessed by a foreground app.
      * @param name The authority of the content provider
      * @param pkgName The package name of the content provider
index 3d214a8..eaccb3a 100644 (file)
@@ -17,6 +17,7 @@ package android.content.pm;
 
 import android.annotation.NonNull;
 import android.annotation.TestApi;
+import android.app.usage.UsageStatsManager;
 import android.content.Context;
 import android.os.RemoteException;
 import android.os.ServiceManager;
@@ -357,6 +358,18 @@ public class ShortcutManager {
         }
     }
 
+    /**
+     * Applications that publish shortcuts should call this method whenever an action that's
+     * equivalent to an existing shortcut has been taken by the user.  This includes not only when
+     * the user manually taps a shortcut, but when the user takes an equivalent action within the
+     * application -- for example, when a music player application has a shortcut to playlist X,
+     * then the application should not only report it when the playlist is opened from the
+     * shortcut, but also when the user plays the playlist within the application.
+     *
+     * <p>The information is accessible via {@link UsageStatsManager#queryEvents}
+     * Typically, launcher applications use this information to build a prediction model
+     * so that they can promote the shortcuts that are likely to be used at the moment.
+     */
     public void reportShortcutUsed(String shortcutId) {
         try {
             mService.reportShortcutUsed(mContext.getPackageName(), shortcutId,
index 6f12741..7835231 100644 (file)
@@ -23,6 +23,7 @@ import android.app.ActivityManager;
 import android.app.ActivityManagerNative;
 import android.app.AppGlobals;
 import android.app.IUidObserver;
+import android.app.usage.UsageStatsManagerInternal;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
@@ -67,6 +68,7 @@ import android.text.format.Time;
 import android.util.ArraySet;
 import android.util.AtomicFile;
 import android.util.KeyValueListParser;
+import android.util.Log;
 import android.util.Slog;
 import android.util.SparseArray;
 import android.util.SparseIntArray;
@@ -277,6 +279,7 @@ public class ShortcutService extends IShortcutService.Stub {
     private final IPackageManager mIPackageManager;
     private final PackageManagerInternal mPackageManagerInternal;
     private final UserManager mUserManager;
+    private final UsageStatsManagerInternal mUsageStatsManagerInternal;
 
     @GuardedBy("mLock")
     final SparseIntArray mUidState = new SparseIntArray();
@@ -353,8 +356,11 @@ public class ShortcutService extends IShortcutService.Stub {
         LocalServices.addService(ShortcutServiceInternal.class, new LocalService());
         mHandler = new Handler(looper);
         mIPackageManager = AppGlobals.getPackageManager();
-        mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class);
-        mUserManager = context.getSystemService(UserManager.class);
+        mPackageManagerInternal = Preconditions.checkNotNull(
+                LocalServices.getService(PackageManagerInternal.class));
+        mUserManager = Preconditions.checkNotNull(context.getSystemService(UserManager.class));
+        mUsageStatsManagerInternal = Preconditions.checkNotNull(
+                LocalServices.getService(UsageStatsManagerInternal.class));
 
         mPackageMonitor.register(context, looper, UserHandle.ALL, /* externalStorage= */ false);
 
@@ -1740,7 +1746,28 @@ public class ShortcutService extends IShortcutService.Stub {
     public void reportShortcutUsed(String packageName, String shortcutId, int userId) {
         verifyCaller(packageName, userId);
 
-        // TODO Implement it.
+        Preconditions.checkNotNull(shortcutId);
+
+        if (DEBUG) {
+            Slog.d(TAG, String.format("reportShortcutUsed: Shortcut %s package %s used on user %d",
+                    shortcutId, packageName, userId));
+        }
+
+        synchronized (mLock) {
+            final ShortcutPackage ps = getPackageShortcutsLocked(packageName, userId);
+            if (ps.findShortcutById(shortcutId) == null) {
+                Log.w(TAG, String.format("reportShortcutUsed: package %s doesn't have shortcut %s",
+                        packageName, shortcutId));
+                return;
+            }
+        }
+
+        final long token = injectClearCallingIdentity();
+        try {
+            mUsageStatsManagerInternal.reportShortcutUsage(packageName, shortcutId, userId);
+        } finally {
+            injectRestoreCallingIdentity(token);
+        }
     }
 
     /**
index 6d75df2..ed53b77 100644 (file)
@@ -36,6 +36,7 @@ import android.annotation.RequiresPermission;
 import android.annotation.UserIdInt;
 import android.app.Activity;
 import android.app.IUidObserver;
+import android.app.usage.UsageStatsManagerInternal;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.Context;
@@ -450,6 +451,7 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase {
     protected PackageManager mMockPackageManager;
     protected PackageManagerInternal mMockPackageManagerInternal;
     protected UserManager mMockUserManager;
+    protected UsageStatsManagerInternal mMockUsageStatsManagerInternal;
 
     protected static final String CALLING_PACKAGE_1 = "com.android.test.1";
     protected static final int CALLING_UID_1 = 10001;
@@ -538,6 +540,12 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase {
         mMockPackageManager = mock(PackageManager.class);
         mMockPackageManagerInternal = mock(PackageManagerInternal.class);
         mMockUserManager = mock(UserManager.class);
+        mMockUsageStatsManagerInternal = mock(UsageStatsManagerInternal.class);
+
+        LocalServices.removeServiceForTest(PackageManagerInternal.class);
+        LocalServices.addService(PackageManagerInternal.class, mMockPackageManagerInternal);
+        LocalServices.removeServiceForTest(UsageStatsManagerInternal.class);
+        LocalServices.addService(UsageStatsManagerInternal.class, mMockUsageStatsManagerInternal);
 
         // Prepare injection values.
 
index 5011182..1702ca4 100644 (file)
@@ -20,6 +20,13 @@ import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils
 import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.parceled;
 import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.set;
 
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
 import android.Manifest.permission;
 import android.app.ActivityManager;
 import android.content.ComponentName;
@@ -1418,4 +1425,46 @@ public class ShortcutManagerTest2 extends BaseShortcutManagerTest {
             assertEquals(3, mManager.getRemainingCallCount());
         });
     }
+
+    public void testReportShortcutUsed() {
+        runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
+            reset(mMockUsageStatsManagerInternal);
+
+            // Report with an nonexistent shortcut.
+            mManager.reportShortcutUsed("s1");
+            verify(mMockUsageStatsManagerInternal, times(0)).reportShortcutUsage(
+                    anyString(), anyString(), anyInt());
+
+            // Publish s2, but s1 still doesn't exist.
+            mManager.setDynamicShortcuts(list(makeShortcut("s2")));
+            mManager.reportShortcutUsed("s1");
+            verify(mMockUsageStatsManagerInternal, times(0)).reportShortcutUsage(
+                    anyString(), anyString(), anyInt());
+
+            mManager.reportShortcutUsed("s2");
+            verify(mMockUsageStatsManagerInternal, times(1)).reportShortcutUsage(
+                    eq(CALLING_PACKAGE_1), eq("s2"), eq(USER_10));
+
+        });
+        runWithCaller(CALLING_PACKAGE_2, USER_10, () -> {
+            // Try with a different package.
+            reset(mMockUsageStatsManagerInternal);
+
+            // Report with an nonexistent shortcut.
+            mManager.reportShortcutUsed("s2");
+            verify(mMockUsageStatsManagerInternal, times(0)).reportShortcutUsage(
+                    anyString(), anyString(), anyInt());
+
+            // Publish s2, but s1 still doesn't exist.
+            mManager.setDynamicShortcuts(list(makeShortcut("s3")));
+            mManager.reportShortcutUsed("s2");
+            verify(mMockUsageStatsManagerInternal, times(0)).reportShortcutUsage(
+                    anyString(), anyString(), anyInt());
+
+            mManager.reportShortcutUsed("s3");
+            verify(mMockUsageStatsManagerInternal, times(1)).reportShortcutUsage(
+                    eq(CALLING_PACKAGE_2), eq("s3"), eq(USER_10));
+
+        });
+    }
 }
index ecfeff9..eb3c665 100644 (file)
@@ -1412,6 +1412,24 @@ public class UsageStatsService extends SystemService implements
         }
 
         @Override
+        public void reportShortcutUsage(String packageName, String shortcutId, int userId) {
+            if (packageName == null || shortcutId == null) {
+                Slog.w(TAG, "Event reported without a package name or a shortcut ID");
+                return;
+            }
+
+            UsageEvents.Event event = new UsageEvents.Event();
+            event.mPackage = packageName.intern();
+            event.mShortcutId = shortcutId.intern();
+
+            // This will later be converted to system time.
+            event.mTimeStamp = SystemClock.elapsedRealtime();
+
+            event.mEventType = Event.SHORTCUT_INVOCATION;
+            mHandler.obtainMessage(MSG_REPORT_EVENT, userId, 0, event).sendToTarget();
+        }
+
+        @Override
         public void reportContentProviderUsage(String name, String packageName, int userId) {
             SomeArgs args = SomeArgs.obtain();
             args.arg1 = name;
index c95ff23..03cee9c 100644 (file)
@@ -51,6 +51,7 @@ final class UsageStatsXmlV1 {
     private static final String ACTIVE_ATTR = "active";
     private static final String LAST_EVENT_ATTR = "lastEvent";
     private static final String TYPE_ATTR = "type";
+    private static final String SHORTCUT_ID_ATTR = "shortcutId";
 
     // Time attributes stored as an offset of the beginTime.
     private static final String LAST_TIME_ACTIVE_ATTR = "lastTimeActive";
@@ -106,9 +107,15 @@ final class UsageStatsXmlV1 {
         event.mTimeStamp = statsOut.beginTime + XmlUtils.readLongAttribute(parser, TIME_ATTR);
 
         event.mEventType = XmlUtils.readIntAttribute(parser, TYPE_ATTR);
-        if (event.mEventType == UsageEvents.Event.CONFIGURATION_CHANGE) {
-            event.mConfiguration = new Configuration();
-            Configuration.readXmlAttrs(parser, event.mConfiguration);
+        switch (event.mEventType) {
+            case UsageEvents.Event.CONFIGURATION_CHANGE:
+                event.mConfiguration = new Configuration();
+                Configuration.readXmlAttrs(parser, event.mConfiguration);
+                break;
+            case UsageEvents.Event.SHORTCUT_INVOCATION:
+                final String id = XmlUtils.readStringAttribute(parser, SHORTCUT_ID_ATTR);
+                event.mShortcutId = (id != null) ? id.intern() : null;
+                break;
         }
 
         if (statsOut.events == null) {
@@ -165,9 +172,17 @@ final class UsageStatsXmlV1 {
         }
         XmlUtils.writeIntAttribute(xml, TYPE_ATTR, event.mEventType);
 
-        if (event.mEventType == UsageEvents.Event.CONFIGURATION_CHANGE
-                && event.mConfiguration != null) {
-            Configuration.writeXmlAttrs(xml, event.mConfiguration);
+        switch (event.mEventType) {
+            case UsageEvents.Event.CONFIGURATION_CHANGE:
+                if (event.mConfiguration != null) {
+                    Configuration.writeXmlAttrs(xml, event.mConfiguration);
+                }
+                break;
+            case UsageEvents.Event.SHORTCUT_INVOCATION:
+                if (event.mShortcutId != null) {
+                    XmlUtils.writeStringAttribute(xml, SHORTCUT_ID_ATTR, event.mShortcutId);
+                }
+                break;
         }
 
         xml.endTag(null, EVENT_TAG);
index 7d003f3..59e4c80 100644 (file)
@@ -549,6 +549,9 @@ class UserUsageStatsService {
             if (event.mConfiguration != null) {
                 pw.printPair("config", Configuration.resourceQualifierString(event.mConfiguration));
             }
+            if (event.mShortcutId != null) {
+                pw.printPair("shortcutId", event.mShortcutId);
+            }
             pw.println();
         }
         pw.decreaseIndent();
@@ -588,6 +591,8 @@ class UserUsageStatsService {
                 return "SYSTEM_INTERACTION";
             case UsageEvents.Event.USER_INTERACTION:
                 return "USER_INTERACTION";
+            case UsageEvents.Event.SHORTCUT_INVOCATION:
+                return "SHORTCUT_INVOCATION";
             default:
                 return "UNKNOWN";
         }