Merge "ShortcutManager: Implement usage report API" into nyc-mr1-dev

This commit is contained in:
Makoto Onuki
2016-06-03 15:21:44 +00:00
committed by Android (Google) Code Review
12 changed files with 204 additions and 16 deletions

View File

@@ -6494,11 +6494,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
}

View File

@@ -6775,11 +6775,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
}

View File

@@ -6500,11 +6500,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
}

View File

@@ -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;
}
}

View File

@@ -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

View 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,

View 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);
}
}
/**

View 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.

View 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));
});
}
}

View File

@@ -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();

View 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);

View 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";
}