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
}
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
}
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
}
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;
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() {
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.
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;
}
}
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;
}
}
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
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;
}
}
+ /**
+ * 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,
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;
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;
private final IPackageManager mIPackageManager;
private final PackageManagerInternal mPackageManagerInternal;
private final UserManager mUserManager;
+ private final UsageStatsManagerInternal mUsageStatsManagerInternal;
@GuardedBy("mLock")
final SparseIntArray mUidState = new SparseIntArray();
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);
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);
+ }
}
/**
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;
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;
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.
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;
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));
+
+ });
+ }
}
}
@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;
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";
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) {
}
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);
if (event.mConfiguration != null) {
pw.printPair("config", Configuration.resourceQualifierString(event.mConfiguration));
}
+ if (event.mShortcutId != null) {
+ pw.printPair("shortcutId", event.mShortcutId);
+ }
pw.println();
}
pw.decreaseIndent();
return "SYSTEM_INTERACTION";
case UsageEvents.Event.USER_INTERACTION:
return "USER_INTERACTION";
+ case UsageEvents.Event.SHORTCUT_INVOCATION:
+ return "SHORTCUT_INVOCATION";
default:
return "UNKNOWN";
}