diff --git a/api/current.txt b/api/current.txt index 4f5a58ad12fb0..0460b074ca03c 100644 --- a/api/current.txt +++ b/api/current.txt @@ -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 } diff --git a/api/system-current.txt b/api/system-current.txt index d5ec375c113fd..60abe46dde70f 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -6780,11 +6780,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 } diff --git a/api/test-current.txt b/api/test-current.txt index fc91a8e39e46b..079f4ca423446 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -6505,11 +6505,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 } diff --git a/core/java/android/app/usage/UsageEvents.java b/core/java/android/app/usage/UsageEvents.java index 24647f388fc12..a0da258bdf1cd 100644 --- a/core/java/android/app/usage/UsageEvents.java +++ b/core/java/android/app/usage/UsageEvents.java @@ -78,6 +78,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} */ @@ -104,6 +111,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. */ @@ -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; } } diff --git a/core/java/android/app/usage/UsageStatsManagerInternal.java b/core/java/android/app/usage/UsageStatsManagerInternal.java index b6f1567568c29..a6f91fe172275 100644 --- a/core/java/android/app/usage/UsageStatsManagerInternal.java +++ b/core/java/android/app/usage/UsageStatsManagerInternal.java @@ -55,6 +55,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 diff --git a/core/java/android/content/pm/ShortcutManager.java b/core/java/android/content/pm/ShortcutManager.java index 3d214a84b1907..eaccb3aeab55a 100644 --- a/core/java/android/content/pm/ShortcutManager.java +++ b/core/java/android/content/pm/ShortcutManager.java @@ -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. + * + *
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, diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java index 6f1274170b716..78352319e1567 100644 --- a/services/core/java/com/android/server/pm/ShortcutService.java +++ b/services/core/java/com/android/server/pm/ShortcutService.java @@ -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); + } } /** diff --git a/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java index 6d75df26b7820..ed53b7782e46d 100644 --- a/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java @@ -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. diff --git a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest2.java b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest2.java index 5011182173d77..1702ca4b2d53a 100644 --- a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest2.java +++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest2.java @@ -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)); + + }); + } } diff --git a/services/usage/java/com/android/server/usage/UsageStatsService.java b/services/usage/java/com/android/server/usage/UsageStatsService.java index ecfeff92ac9f9..eb3c665a5e574 100644 --- a/services/usage/java/com/android/server/usage/UsageStatsService.java +++ b/services/usage/java/com/android/server/usage/UsageStatsService.java @@ -1411,6 +1411,24 @@ public class UsageStatsService extends SystemService implements mHandler.obtainMessage(MSG_REPORT_EVENT, userId, 0, event).sendToTarget(); } + @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(); diff --git a/services/usage/java/com/android/server/usage/UsageStatsXmlV1.java b/services/usage/java/com/android/server/usage/UsageStatsXmlV1.java index c95ff2364f2c6..03cee9c26da9e 100644 --- a/services/usage/java/com/android/server/usage/UsageStatsXmlV1.java +++ b/services/usage/java/com/android/server/usage/UsageStatsXmlV1.java @@ -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); diff --git a/services/usage/java/com/android/server/usage/UserUsageStatsService.java b/services/usage/java/com/android/server/usage/UserUsageStatsService.java index 7d003f3bf61a9..59e4c8021bdc5 100644 --- a/services/usage/java/com/android/server/usage/UserUsageStatsService.java +++ b/services/usage/java/com/android/server/usage/UserUsageStatsService.java @@ -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"; }