Merge "Adding support for apps to return PinItemRequest to launchers as a result of Intent.ACTION_CREATE_SHORTCUT"

This commit is contained in:
Sunny Goyal
2017-01-19 00:27:49 +00:00
committed by Android (Google) Code Review
14 changed files with 500 additions and 58 deletions

View File

@@ -9009,10 +9009,10 @@ package android.content {
field public static final java.lang.String EXTRA_RESTRICTIONS_LIST = "android.intent.extra.restrictions_list";
field public static final java.lang.String EXTRA_RESULT_RECEIVER = "android.intent.extra.RESULT_RECEIVER";
field public static final java.lang.String EXTRA_RETURN_RESULT = "android.intent.extra.RETURN_RESULT";
field public static final java.lang.String EXTRA_SHORTCUT_ICON = "android.intent.extra.shortcut.ICON";
field public static final java.lang.String EXTRA_SHORTCUT_ICON_RESOURCE = "android.intent.extra.shortcut.ICON_RESOURCE";
field public static final java.lang.String EXTRA_SHORTCUT_INTENT = "android.intent.extra.shortcut.INTENT";
field public static final java.lang.String EXTRA_SHORTCUT_NAME = "android.intent.extra.shortcut.NAME";
field public static final deprecated java.lang.String EXTRA_SHORTCUT_ICON = "android.intent.extra.shortcut.ICON";
field public static final deprecated java.lang.String EXTRA_SHORTCUT_ICON_RESOURCE = "android.intent.extra.shortcut.ICON_RESOURCE";
field public static final deprecated java.lang.String EXTRA_SHORTCUT_INTENT = "android.intent.extra.shortcut.INTENT";
field public static final deprecated java.lang.String EXTRA_SHORTCUT_NAME = "android.intent.extra.shortcut.NAME";
field public static final java.lang.String EXTRA_SHUTDOWN_USERSPACE_ONLY = "android.intent.extra.SHUTDOWN_USERSPACE_ONLY";
field public static final java.lang.String EXTRA_STREAM = "android.intent.extra.STREAM";
field public static final java.lang.String EXTRA_SUBJECT = "android.intent.extra.SUBJECT";
@@ -9797,6 +9797,8 @@ package android.content.pm {
method public android.content.pm.ApplicationInfo getApplicationInfo(java.lang.String, int, android.os.UserHandle);
method public android.content.pm.LauncherApps.PinItemRequest getPinItemRequest(android.content.Intent);
method public android.graphics.drawable.Drawable getShortcutBadgedIconDrawable(android.content.pm.ShortcutInfo, int);
method public android.content.IntentSender getShortcutConfigActivityIntent(android.content.pm.LauncherActivityInfo);
method public java.util.List<android.content.pm.LauncherActivityInfo> getShortcutConfigActivityList(java.lang.String, android.os.UserHandle);
method public android.graphics.drawable.Drawable getShortcutIconDrawable(android.content.pm.ShortcutInfo, int);
method public java.util.List<android.content.pm.ShortcutInfo> getShortcuts(android.content.pm.LauncherApps.ShortcutQuery, android.os.UserHandle);
method public boolean hasShortcutHostPermission();
@@ -10400,6 +10402,7 @@ package android.content.pm {
public class ShortcutManager {
method public boolean addDynamicShortcuts(java.util.List<android.content.pm.ShortcutInfo>);
method public android.content.Intent createShortcutResultIntent(android.content.pm.ShortcutInfo);
method public void disableShortcuts(java.util.List<java.lang.String>);
method public void disableShortcuts(java.util.List<java.lang.String>, java.lang.CharSequence);
method public void enableShortcuts(java.util.List<java.lang.String>);

View File

@@ -9402,10 +9402,10 @@ package android.content {
field public static final java.lang.String EXTRA_RESULT_NEEDED = "android.intent.extra.RESULT_NEEDED";
field public static final java.lang.String EXTRA_RESULT_RECEIVER = "android.intent.extra.RESULT_RECEIVER";
field public static final java.lang.String EXTRA_RETURN_RESULT = "android.intent.extra.RETURN_RESULT";
field public static final java.lang.String EXTRA_SHORTCUT_ICON = "android.intent.extra.shortcut.ICON";
field public static final java.lang.String EXTRA_SHORTCUT_ICON_RESOURCE = "android.intent.extra.shortcut.ICON_RESOURCE";
field public static final java.lang.String EXTRA_SHORTCUT_INTENT = "android.intent.extra.shortcut.INTENT";
field public static final java.lang.String EXTRA_SHORTCUT_NAME = "android.intent.extra.shortcut.NAME";
field public static final deprecated java.lang.String EXTRA_SHORTCUT_ICON = "android.intent.extra.shortcut.ICON";
field public static final deprecated java.lang.String EXTRA_SHORTCUT_ICON_RESOURCE = "android.intent.extra.shortcut.ICON_RESOURCE";
field public static final deprecated java.lang.String EXTRA_SHORTCUT_INTENT = "android.intent.extra.shortcut.INTENT";
field public static final deprecated java.lang.String EXTRA_SHORTCUT_NAME = "android.intent.extra.shortcut.NAME";
field public static final java.lang.String EXTRA_SHUTDOWN_USERSPACE_ONLY = "android.intent.extra.SHUTDOWN_USERSPACE_ONLY";
field public static final java.lang.String EXTRA_SPLIT_NAME = "android.intent.extra.SPLIT_NAME";
field public static final java.lang.String EXTRA_STREAM = "android.intent.extra.STREAM";
@@ -10225,6 +10225,8 @@ package android.content.pm {
method public android.content.pm.ApplicationInfo getApplicationInfo(java.lang.String, int, android.os.UserHandle);
method public android.content.pm.LauncherApps.PinItemRequest getPinItemRequest(android.content.Intent);
method public android.graphics.drawable.Drawable getShortcutBadgedIconDrawable(android.content.pm.ShortcutInfo, int);
method public android.content.IntentSender getShortcutConfigActivityIntent(android.content.pm.LauncherActivityInfo);
method public java.util.List<android.content.pm.LauncherActivityInfo> getShortcutConfigActivityList(java.lang.String, android.os.UserHandle);
method public android.graphics.drawable.Drawable getShortcutIconDrawable(android.content.pm.ShortcutInfo, int);
method public java.util.List<android.content.pm.ShortcutInfo> getShortcuts(android.content.pm.LauncherApps.ShortcutQuery, android.os.UserHandle);
method public boolean hasShortcutHostPermission();
@@ -10901,6 +10903,7 @@ package android.content.pm {
public class ShortcutManager {
method public boolean addDynamicShortcuts(java.util.List<android.content.pm.ShortcutInfo>);
method public android.content.Intent createShortcutResultIntent(android.content.pm.ShortcutInfo);
method public void disableShortcuts(java.util.List<java.lang.String>);
method public void disableShortcuts(java.util.List<java.lang.String>, java.lang.CharSequence);
method public void enableShortcuts(java.util.List<java.lang.String>);

View File

@@ -9034,10 +9034,10 @@ package android.content {
field public static final java.lang.String EXTRA_RESTRICTIONS_LIST = "android.intent.extra.restrictions_list";
field public static final java.lang.String EXTRA_RESULT_RECEIVER = "android.intent.extra.RESULT_RECEIVER";
field public static final java.lang.String EXTRA_RETURN_RESULT = "android.intent.extra.RETURN_RESULT";
field public static final java.lang.String EXTRA_SHORTCUT_ICON = "android.intent.extra.shortcut.ICON";
field public static final java.lang.String EXTRA_SHORTCUT_ICON_RESOURCE = "android.intent.extra.shortcut.ICON_RESOURCE";
field public static final java.lang.String EXTRA_SHORTCUT_INTENT = "android.intent.extra.shortcut.INTENT";
field public static final java.lang.String EXTRA_SHORTCUT_NAME = "android.intent.extra.shortcut.NAME";
field public static final deprecated java.lang.String EXTRA_SHORTCUT_ICON = "android.intent.extra.shortcut.ICON";
field public static final deprecated java.lang.String EXTRA_SHORTCUT_ICON_RESOURCE = "android.intent.extra.shortcut.ICON_RESOURCE";
field public static final deprecated java.lang.String EXTRA_SHORTCUT_INTENT = "android.intent.extra.shortcut.INTENT";
field public static final deprecated java.lang.String EXTRA_SHORTCUT_NAME = "android.intent.extra.shortcut.NAME";
field public static final java.lang.String EXTRA_SHUTDOWN_USERSPACE_ONLY = "android.intent.extra.SHUTDOWN_USERSPACE_ONLY";
field public static final java.lang.String EXTRA_STREAM = "android.intent.extra.STREAM";
field public static final java.lang.String EXTRA_SUBJECT = "android.intent.extra.SUBJECT";
@@ -9825,6 +9825,8 @@ package android.content.pm {
method public android.content.pm.ApplicationInfo getApplicationInfo(java.lang.String, int, android.os.UserHandle);
method public android.content.pm.LauncherApps.PinItemRequest getPinItemRequest(android.content.Intent);
method public android.graphics.drawable.Drawable getShortcutBadgedIconDrawable(android.content.pm.ShortcutInfo, int);
method public android.content.IntentSender getShortcutConfigActivityIntent(android.content.pm.LauncherActivityInfo);
method public java.util.List<android.content.pm.LauncherActivityInfo> getShortcutConfigActivityList(java.lang.String, android.os.UserHandle);
method public android.graphics.drawable.Drawable getShortcutIconDrawable(android.content.pm.ShortcutInfo, int);
method public java.util.List<android.content.pm.ShortcutInfo> getShortcuts(android.content.pm.LauncherApps.ShortcutQuery, android.os.UserHandle);
method public boolean hasShortcutHostPermission();
@@ -10432,6 +10434,7 @@ package android.content.pm {
public class ShortcutManager {
ctor public ShortcutManager(android.content.Context);
method public boolean addDynamicShortcuts(java.util.List<android.content.pm.ShortcutInfo>);
method public android.content.Intent createShortcutResultIntent(android.content.pm.ShortcutInfo);
method public void disableShortcuts(java.util.List<java.lang.String>);
method public void disableShortcuts(java.util.List<java.lang.String>, java.lang.CharSequence);
method public void enableShortcuts(java.util.List<java.lang.String>);

View File

@@ -715,11 +715,13 @@ public class Intent implements Parcelable, Cloneable {
/**
* Activity Action: Creates a shortcut.
* <p>Input: Nothing.</p>
* <p>Output: An Intent representing the shortcut. The intent must contain three
* <p>Output: An Intent representing the {@link android.content.pm.ShortcutInfo} result.</p>
* <p>For compatibility with older versions of android the intent may also contain three
* extras: SHORTCUT_INTENT (value: Intent), SHORTCUT_NAME (value: String),
* and SHORTCUT_ICON (value: Bitmap) or SHORTCUT_ICON_RESOURCE
* (value: ShortcutIconResource).</p>
*
* @see android.content.pm.ShortcutManager#createShortcutResultIntent
* @see #EXTRA_SHORTCUT_INTENT
* @see #EXTRA_SHORTCUT_NAME
* @see #EXTRA_SHORTCUT_ICON
@@ -733,26 +735,34 @@ public class Intent implements Parcelable, Cloneable {
* The name of the extra used to define the Intent of a shortcut.
*
* @see #ACTION_CREATE_SHORTCUT
* @deprecated Replaced with {@link android.content.pm.ShortcutManager#createShortcutResultIntent}
*/
@Deprecated
public static final String EXTRA_SHORTCUT_INTENT = "android.intent.extra.shortcut.INTENT";
/**
* The name of the extra used to define the name of a shortcut.
*
* @see #ACTION_CREATE_SHORTCUT
* @deprecated Replaced with {@link android.content.pm.ShortcutManager#createShortcutResultIntent}
*/
@Deprecated
public static final String EXTRA_SHORTCUT_NAME = "android.intent.extra.shortcut.NAME";
/**
* The name of the extra used to define the icon, as a Bitmap, of a shortcut.
*
* @see #ACTION_CREATE_SHORTCUT
* @deprecated Replaced with {@link android.content.pm.ShortcutManager#createShortcutResultIntent}
*/
@Deprecated
public static final String EXTRA_SHORTCUT_ICON = "android.intent.extra.shortcut.ICON";
/**
* The name of the extra used to define the icon, as a ShortcutIconResource, of a shortcut.
*
* @see #ACTION_CREATE_SHORTCUT
* @see android.content.Intent.ShortcutIconResource
* @deprecated Replaced with {@link android.content.pm.ShortcutManager#createShortcutResultIntent}
*/
@Deprecated
public static final String EXTRA_SHORTCUT_ICON_RESOURCE =
"android.intent.extra.shortcut.ICON_RESOURCE";

View File

@@ -18,6 +18,7 @@ package android.content.pm;
import android.content.ComponentName;
import android.content.Intent;
import android.content.IntentSender;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.IOnAppsChangedListener;
@@ -60,4 +61,8 @@ interface ILauncherApps {
int userId);
boolean hasShortcutHostPermission(String callingPackage);
ParceledListSlice getShortcutConfigActivities(String packageName, in UserHandle user);
IntentSender getShortcutConfigActivityIntent(String callingPackage, in ComponentName component,
in UserHandle user);
}

View File

@@ -15,6 +15,7 @@
*/
package android.content.pm;
import android.content.Intent;
import android.content.IntentSender;
import android.content.pm.ParceledListSlice;
import android.content.pm.ShortcutInfo;
@@ -45,6 +46,8 @@ interface IShortcutService {
boolean requestPinShortcut(String packageName, in ShortcutInfo shortcut,
in IntentSender resultIntent, int userId);
Intent createShortcutResultIntent(String packageName, in ShortcutInfo shortcut, int userId);
void disableShortcuts(String packageName, in List shortcutIds, CharSequence disabledMessage,
int disabledMessageResId, int userId);

View File

@@ -27,6 +27,7 @@ import android.content.ActivityNotFoundException;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentSender;
import android.content.pm.PackageManager.ApplicationInfoFlags;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.Resources;
@@ -384,25 +385,11 @@ public class LauncherApps {
* @return List of launchable activities. Can be an empty list but will not be null.
*/
public List<LauncherActivityInfo> getActivityList(String packageName, UserHandle user) {
ParceledListSlice<ResolveInfo> activities = null;
try {
activities = mService.getLauncherActivities(packageName, user);
return convertToActivityList(mService.getLauncherActivities(packageName, user), user);
} catch (RemoteException re) {
throw re.rethrowFromSystemServer();
}
if (activities == null) {
return Collections.EMPTY_LIST;
}
ArrayList<LauncherActivityInfo> lais = new ArrayList<LauncherActivityInfo>();
for (ResolveInfo ri : activities.getList()) {
LauncherActivityInfo lai = new LauncherActivityInfo(mContext, ri.activityInfo, user);
if (DEBUG) {
Log.v(TAG, "Returning activity for profile " + user + " : "
+ lai.getComponentName());
}
lais.add(lai);
}
return lais;
}
/**
@@ -464,6 +451,73 @@ public class LauncherApps {
}
}
/**
* Retrieves a list of config activities for creating {@link ShortcutInfo}.
*
* @param packageName The specific package to query. If null, it checks all installed packages
* in the profile.
* @param user The UserHandle of the profile.
* @return List of config activities. Can be an empty list but will not be null.
*
* @see Intent#ACTION_CREATE_SHORTCUT
* @see #getShortcutConfigActivityIntent(LauncherActivityInfo)
*/
public List<LauncherActivityInfo> getShortcutConfigActivityList(@Nullable String packageName,
@NonNull UserHandle user) {
try {
return convertToActivityList(mService.getShortcutConfigActivities(packageName, user),
user);
} catch (RemoteException re) {
throw re.rethrowFromSystemServer();
}
}
private List<LauncherActivityInfo> convertToActivityList(
@Nullable ParceledListSlice<ResolveInfo> activities, UserHandle user) {
if (activities == null) {
return Collections.EMPTY_LIST;
}
ArrayList<LauncherActivityInfo> lais = new ArrayList<>();
for (ResolveInfo ri : activities.getList()) {
LauncherActivityInfo lai = new LauncherActivityInfo(mContext, ri.activityInfo, user);
if (DEBUG) {
Log.v(TAG, "Returning activity for profile " + user + " : "
+ lai.getComponentName());
}
lais.add(lai);
}
return lais;
}
/**
* Returns an intent sender which can be used to start the configure activity for creating
* custom shortcuts. Use this method if the provider is in another profile as you are not
* allowed to start an activity in another profile.
*
* <p>The caller should receive {@link PinItemRequest} in onActivityResult on
* {@link android.app.Activity#RESULT_OK}.
*
* <p>Callers must be allowed to access the shortcut information, as defined in {@link
* #hasShortcutHostPermission()}.
*
* @param info a configuration activity returned by {@link #getShortcutConfigActivityList}
*
* @throws IllegalStateException when the user is locked or not running.
* @throws SecurityException if {@link #hasShortcutHostPermission()} is false.
*
* @see #getPinItemRequest(Intent)
* @see Intent#ACTION_CREATE_SHORTCUT
* @see android.app.Activity#startIntentSenderForResult
*/
public IntentSender getShortcutConfigActivityIntent(@NonNull LauncherActivityInfo info) {
try {
return mService.getShortcutConfigActivityIntent(
mContext.getPackageName(), info.getComponentName(), info.getUser());
} catch (RemoteException re) {
throw re.rethrowFromSystemServer();
}
}
/**
* Checks if the package is installed and enabled for a profile.
*

View File

@@ -880,6 +880,31 @@ public class ShortcutManager {
}
}
/**
* Returns an Intent which can be used by the default launcher to pin {@param shortcut}.
* This should be used by an Activity to set result in response to
* {@link Intent#ACTION_CREATE_SHORTCUT}.
*
* @param shortcut New shortcut to pin. If an app wants to pin an existing (either dynamic
* or manifest) shortcut, then it only needs to have an ID, and other fields don't have to
* be set, in which case, the target shortcut must be enabled.
* If it's a new shortcut, all the mandatory fields, such as a short label, must be
* set.
* @return The intent that should be set as the result for the calling activity or null.
*
* @see Intent#ACTION_CREATE_SHORTCUT
*
* @throws IllegalArgumentException if a shortcut with the same ID exists and is disabled.
*/
public Intent createShortcutResultIntent(@NonNull ShortcutInfo shortcut) {
try {
return mService.createShortcutResultIntent(mContext.getPackageName(), shortcut,
injectMyUserId());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Called internally when an app is considered to have come to the foreground
* even when technically it's not. This method resets the throttling for this package.

View File

@@ -21,9 +21,11 @@ import android.annotation.UserIdInt;
import android.app.ActivityManager;
import android.app.ActivityManagerInternal;
import android.app.AppGlobals;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentSender;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.ILauncherApps;
@@ -277,24 +279,11 @@ public class LauncherAppsService extends SystemService {
@Override
public ParceledListSlice<ResolveInfo> getLauncherActivities(String packageName, UserHandle user)
throws RemoteException {
ensureInUserProfiles(user, "Cannot retrieve activities for unrelated profile " + user);
if (!isUserEnabled(user)) {
return null;
}
final Intent mainIntent = new Intent(Intent.ACTION_MAIN, null);
mainIntent.addCategory(Intent.CATEGORY_LAUNCHER);
mainIntent.setPackage(packageName);
long ident = Binder.clearCallingIdentity();
try {
List<ResolveInfo> apps = mPm.queryIntentActivitiesAsUser(mainIntent,
PackageManager.MATCH_DIRECT_BOOT_AWARE
| PackageManager.MATCH_DIRECT_BOOT_UNAWARE,
user.getIdentifier());
return new ParceledListSlice<>(apps);
} finally {
Binder.restoreCallingIdentity(ident);
}
return queryActivitiesForUser(
new Intent(Intent.ACTION_MAIN)
.addCategory(Intent.CATEGORY_LAUNCHER)
.setPackage(packageName),
user);
}
@Override
@@ -317,6 +306,53 @@ public class LauncherAppsService extends SystemService {
}
}
@Override
public ParceledListSlice getShortcutConfigActivities(String packageName, UserHandle user)
throws RemoteException {
return queryActivitiesForUser(
new Intent(Intent.ACTION_CREATE_SHORTCUT).setPackage(packageName), user);
}
private ParceledListSlice<ResolveInfo> queryActivitiesForUser(Intent intent,
UserHandle user) {
ensureInUserProfiles(user, "Cannot retrieve activities for unrelated profile " + user);
if (!isUserEnabled(user)) {
return null;
}
long ident = injectClearCallingIdentity();
try {
List<ResolveInfo> apps = mPm.queryIntentActivitiesAsUser(intent,
PackageManager.MATCH_DIRECT_BOOT_AWARE
| PackageManager.MATCH_DIRECT_BOOT_UNAWARE,
user.getIdentifier());
return new ParceledListSlice<>(apps);
} finally {
injectRestoreCallingIdentity(ident);
}
}
@Override
public IntentSender getShortcutConfigActivityIntent(String callingPackage,
ComponentName component, UserHandle user) throws RemoteException {
ensureShortcutPermission(callingPackage, user);
Preconditions.checkNotNull(component);
Preconditions.checkArgument(isUserEnabled(user), "User not enabled");
// All right, create the sender.
Intent intent = new Intent(Intent.ACTION_CREATE_SHORTCUT).setComponent(component);
final long identity = Binder.clearCallingIdentity();
try {
return PendingIntent.getActivityAsUser(
mContext, 0, intent, PendingIntent.FLAG_ONE_SHOT
| PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_CANCEL_CURRENT,
null, user)
.getIntentSender();
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
public boolean isPackageEnabled(String packageName, UserHandle user)
throws RemoteException {

View File

@@ -15,6 +15,7 @@
*/
package com.android.server.pm;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.appwidget.AppWidgetProviderInfo;
import android.content.ComponentName;
@@ -24,6 +25,7 @@ import android.content.pm.IPinItemRequest;
import android.content.pm.LauncherApps;
import android.content.pm.LauncherApps.PinItemRequest;
import android.content.pm.ShortcutInfo;
import android.os.Binder;
import android.os.Bundle;
import android.os.UserHandle;
import android.util.Log;
@@ -50,18 +52,31 @@ class ShortcutRequestPinProcessor {
private static class PinItemRequestInner extends IPinItemRequest.Stub {
protected final ShortcutRequestPinProcessor mProcessor;
private final IntentSender mResultIntent;
private final int mLauncherUid;
@GuardedBy("this")
private boolean mAccepted;
private PinItemRequestInner(ShortcutRequestPinProcessor processor,
IntentSender resultIntent) {
IntentSender resultIntent, int launcherUid) {
mProcessor = processor;
mResultIntent = resultIntent;
mLauncherUid = launcherUid;
}
/**
* Returns true if the caller is same as the default launcher app when this request
* object was created.
*/
private boolean isCallerValid() {
return mProcessor.isCallerUid(mLauncherUid);
}
@Override
public boolean isValid() {
if (!isCallerValid()) {
return false;
}
// TODO When an app calls requestPinShortcut(), all pending requests should be
// invalidated.
synchronized (this) {
@@ -76,6 +91,9 @@ class ShortcutRequestPinProcessor {
public boolean accept(Bundle options) {
// Make sure the options are unparcellable by the FW. (e.g. not containing unknown
// classes.)
if (!isCallerValid()) {
throw new SecurityException("Calling uid mismatch");
}
Intent extras = null;
if (options != null) {
try {
@@ -126,8 +144,8 @@ class ShortcutRequestPinProcessor {
private PinShortcutRequestInner(ShortcutRequestPinProcessor processor,
ShortcutInfo shortcutOriginal, ShortcutInfo shortcutForLauncher,
IntentSender resultIntent,
String launcherPackage, int launcherUserId, boolean preExisting) {
super(processor, resultIntent);
String launcherPackage, int launcherUserId, int launcherUid, boolean preExisting) {
super(processor, resultIntent, launcherUid);
this.shortcutOriginal = shortcutOriginal;
this.shortcutForLauncher = shortcutForLauncher;
this.launcherPackage = launcherPackage;
@@ -157,6 +175,7 @@ class ShortcutRequestPinProcessor {
/**
* Handle {@link android.content.pm.ShortcutManager#requestPinShortcut)} and
* {@link android.appwidget.AppWidgetManager#requestPinAppWidget}.
* In this flow the PinItemRequest is delivered directly to the default launcher app.
* One of {@param inShortcut} and {@param inAppWidget} is always non-null and the other is
* always null.
*/
@@ -184,9 +203,13 @@ class ShortcutRequestPinProcessor {
// Next, validate the incoming shortcut, etc.
final PinItemRequest request;
if (inShortcut != null) {
request = requestPinShortcutLocked(inShortcut, resultIntent, confirmActivity);
request = requestPinShortcutLocked(inShortcut, resultIntent, confirmActivity,
true /* ignoreIfAlreadyPinned */);
} else {
request = new PinItemRequest(inAppWidget, new PinItemRequestInner(this, resultIntent));
int launcherUid = mService.injectGetPackageUid(
confirmActivity.first.getPackageName(), launcherUserId);
request = new PinItemRequest(inAppWidget,
new PinItemRequestInner(this, resultIntent, launcherUid));
}
if (request == null) {
@@ -196,11 +219,42 @@ class ShortcutRequestPinProcessor {
return startRequestConfirmActivity(confirmActivity.first, launcherUserId, request);
}
/**
* Handle {@link android.content.pm.ShortcutManager#createShortcutResultIntent(ShortcutInfo)}.
* In this flow the PinItemRequest is delivered to the caller app. Its the app's responsibility
* to send it to the Launcher app (via {@link android.app.Activity#setResult(int, Intent)}).
*/
public Intent createShortcutResultIntent(@NonNull ShortcutInfo inShortcut, int userId) {
// Find the default launcher activity
final int launcherUserId = mService.getParentOrSelfUserId(userId);
final ComponentName defaultLauncher = mService.getDefaultLauncher(launcherUserId);
if (defaultLauncher == null) {
Log.e(TAG, "Default launcher not found.");
return null;
}
// Make sure the launcher user is unlocked. (it's always the parent profile, so should
// really be unlocked here though.)
mService.throwIfUserLockedL(launcherUserId);
// Next, validate the incoming shortcut, etc.
PinItemRequest request = requestPinShortcutLocked(inShortcut, null,
Pair.create(defaultLauncher, launcherUserId), false /* ignoreIfAlreadyPinned */);
if (request == null) {
return null;
}
return new Intent().putExtra(LauncherApps.EXTRA_PIN_ITEM_REQUEST, request);
}
/**
* Handle {@link android.content.pm.ShortcutManager#requestPinShortcut)}.
*
* @param ignoreIfAlreadyPinned if true and the {@param inShortcut} is already pinned for
* {@param confirmActivity}, null is returned instead.
*/
private PinItemRequest requestPinShortcutLocked(ShortcutInfo inShortcut,
IntentSender resultIntent, Pair<ComponentName, Integer> confirmActivity) {
IntentSender resultIntent, Pair<ComponentName, Integer> confirmActivity,
boolean ignoreIfAlreadyPinned) {
final ShortcutPackage ps = mService.getPackageShortcutsForPublisherLocked(
inShortcut.getPackage(), inShortcut.getUserId());
@@ -221,9 +275,10 @@ class ShortcutRequestPinProcessor {
if (existsAlready) {
validateExistingShortcut(existing);
final boolean isAlreadyPinned = mService.getLauncherShortcutsLocked(
launcherPackage, existing.getUserId(), launcherUserId).hasPinned(existing);
// See if it's already pinned.
if (mService.getLauncherShortcutsLocked(
launcherPackage, existing.getUserId(), launcherUserId).hasPinned(existing)) {
if (ignoreIfAlreadyPinned && isAlreadyPinned) {
Log.i(TAG, "Launcher's already pinning shortcut " + existing.getId()
+ " for package " + existing.getPackage());
return null;
@@ -233,8 +288,10 @@ class ShortcutRequestPinProcessor {
// Note this will remove the intent and icons.
shortcutForLauncher = existing.clone(ShortcutInfo.CLONE_REMOVE_FOR_LAUNCHER);
// FLAG_PINNED is still set, if it's pinned by other launchers.
shortcutForLauncher.clearFlags(ShortcutInfo.FLAG_PINNED);
if (!isAlreadyPinned) {
// FLAG_PINNED is still set, if it's pinned by other launchers.
shortcutForLauncher.clearFlags(ShortcutInfo.FLAG_PINNED);
}
} else {
// If the shortcut has no default activity, try to set the main activity.
// But in the request-pin case, it's optional, so it's okay even if the caller
@@ -264,7 +321,9 @@ class ShortcutRequestPinProcessor {
// Create a request object.
final PinShortcutRequestInner inner =
new PinShortcutRequestInner(this, inShortcut, shortcutForLauncher, resultIntent,
launcherPackage, launcherUserId, existsAlready);
launcherPackage, launcherUserId,
mService.injectGetPackageUid(launcherPackage, launcherUserId),
existsAlready);
return new PinItemRequest(shortcutForLauncher, inner);
}
@@ -327,6 +386,10 @@ class ShortcutRequestPinProcessor {
mService.injectSendIntentSender(intent, extras);
}
public boolean isCallerUid(int uid) {
return uid == mService.injectBinderCallingUid();
}
/**
* The last step of the "request pin shortcut" flow. Called when the launcher accepted a
* request.

View File

@@ -1529,7 +1529,7 @@ public class ShortcutService extends IShortcutService.Stub {
if (UserHandle.getUserId(callingUid) != userId) {
throw new SecurityException("Invalid user-ID");
}
if (injectGetPackageUid(packageName, userId) == injectBinderCallingUid()) {
if (injectGetPackageUid(packageName, userId) == callingUid) {
return; // Caller is valid.
}
throw new SecurityException("Calling package name mismatch");
@@ -1854,6 +1854,25 @@ public class ShortcutService extends IShortcutService.Stub {
return requestPinItem(packageName, userId, shortcut, null, resultIntent);
}
@Override
public Intent createShortcutResultIntent(String packageName, ShortcutInfo shortcut, int userId)
throws RemoteException {
Preconditions.checkNotNull(shortcut);
Preconditions.checkArgument(shortcut.isEnabled(), "Shortcut must be enabled");
verifyCaller(packageName, userId);
final Intent ret;
synchronized (mLock) {
throwIfUserLockedL(userId);
// Send request to the launcher, if supported.
ret = mShortcutRequestPinProcessor.createShortcutResultIntent(shortcut, userId);
}
verifyStates();
return ret;
}
/**
* Handles {@link #requestPinShortcut} and {@link ShortcutServiceInternal#requestPinAppWidget}.
* After validating the caller, it passes the request to {@link #mShortcutRequestPinProcessor}.

View File

@@ -138,6 +138,12 @@
android:enabled="true" android:exported="true">
<meta-data android:name="android.app.shortcuts" android:resource="@xml/shortcut_1"/>
</activity-alias>
<activity-alias android:name="a.ShortcutConfigActivity"
android:targetActivity="com.android.server.pm.ShortcutTestActivity">
<intent-filter>
<action android:name="android.intent.action.CREATE_SHORTCUT" />
</intent-filter>
</activity-alias>
<activity-alias android:name="a.DisabledMain"
android:targetActivity="com.android.server.pm.ShortcutTestActivity"

View File

@@ -0,0 +1,183 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.pm;
import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils
.assertExpectException;
import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.list;
import android.content.ComponentName;
import android.content.Intent;
import android.content.pm.LauncherActivityInfo;
import android.content.pm.LauncherApps.PinItemRequest;
import android.content.pm.ShortcutInfo;
import android.content.pm.ShortcutManager;
import android.os.Process;
import android.test.suitebuilder.annotation.SmallTest;
import static org.mockito.Mockito.*;
/**
* Tests for {@link ShortcutManager#createShortcutResultIntent(ShortcutInfo)} and relevant APIs.
*
m FrameworksServicesTests &&
adb install \
-r -g ${ANDROID_PRODUCT_OUT}/data/app/FrameworksServicesTests/FrameworksServicesTests.apk &&
adb shell am instrument -e class com.android.server.pm.ShortcutManagerTest10 \
-w com.android.frameworks.servicestests/android.support.test.runner.AndroidJUnitRunner
*/
@SmallTest
public class ShortcutManagerTest10 extends BaseShortcutManagerTest {
private PinItemRequest mRequest;
private PinItemRequest verifyAndGetCreateShortcutResult(Intent resultIntent) {
PinItemRequest request = mLauncherApps.getPinItemRequest(resultIntent);
assertNotNull(request);
assertEquals(PinItemRequest.REQUEST_TYPE_SHORTCUT, request.getRequestType());
return request;
}
public void testCreateShortcutResult_noDefaultLauncher() {
runWithCaller(CALLING_PACKAGE_1, USER_P0, () -> {
ShortcutInfo s1 = makeShortcut("s1");
assertNull(mManager.createShortcutResultIntent(s1));
});
}
public void testCreateShortcutResult_validResult() {
setDefaultLauncher(USER_0, mMainActivityFetcher.apply(LAUNCHER_1, USER_0));
runWithCaller(CALLING_PACKAGE_1, USER_P0, () -> {
ShortcutInfo s1 = makeShortcut("s1");
Intent intent = mManager.createShortcutResultIntent(s1);
mRequest = verifyAndGetCreateShortcutResult(intent);
});
runWithCaller(LAUNCHER_1, USER_0, () -> {
assertTrue(mRequest.isValid());
assertTrue(mRequest.accept());
});
}
public void testCreateShortcutResult_alreadyPinned() {
setDefaultLauncher(USER_0, mMainActivityFetcher.apply(LAUNCHER_1, USER_0));
runWithCaller(CALLING_PACKAGE_1, USER_P0, () -> {
assertTrue(mManager.setDynamicShortcuts(list(makeShortcut("s1"))));
});
runWithCaller(LAUNCHER_1, USER_0, () -> {
mLauncherApps.pinShortcuts(CALLING_PACKAGE_1, list("s1"), HANDLE_USER_P0);
});
runWithCaller(CALLING_PACKAGE_1, USER_P0, () -> {
ShortcutInfo s1 = makeShortcut("s1");
Intent intent = mManager.createShortcutResultIntent(s1);
mRequest = verifyAndGetCreateShortcutResult(intent);
});
runWithCaller(LAUNCHER_1, USER_0, () -> {
assertTrue(mRequest.isValid());
assertTrue(mRequest.getShortcutInfo().isPinned());
assertTrue(mRequest.accept());
});
}
public void testCreateShortcutResult_alreadyPinnedByAnother() {
runWithCaller(CALLING_PACKAGE_1, USER_P0, () -> {
assertTrue(mManager.setDynamicShortcuts(list(makeShortcut("s1"))));
});
// Initially all launchers have the shortcut permission, until we call setDefaultLauncher().
runWithCaller(LAUNCHER_2, USER_0, () -> {
mLauncherApps.pinShortcuts(CALLING_PACKAGE_1, list("s1"), HANDLE_USER_P0);
});
setDefaultLauncher(USER_0, mMainActivityFetcher.apply(LAUNCHER_1, USER_0));
runWithCaller(CALLING_PACKAGE_1, USER_P0, () -> {
ShortcutInfo s1 = makeShortcut("s1");
Intent intent = mManager.createShortcutResultIntent(s1);
mRequest = verifyAndGetCreateShortcutResult(intent);
});
runWithCaller(LAUNCHER_1, USER_0, () -> {
assertTrue(mRequest.isValid());
assertFalse(mRequest.getShortcutInfo().isPinned());
assertTrue(mRequest.accept());
});
}
public void testCreateShortcutResult_defaultLauncherChanges() {
setDefaultLauncher(USER_0, mMainActivityFetcher.apply(LAUNCHER_1, USER_0));
runWithCaller(CALLING_PACKAGE_1, USER_P0, () -> {
ShortcutInfo s1 = makeShortcut("s1");
Intent intent = mManager.createShortcutResultIntent(s1);
mRequest = verifyAndGetCreateShortcutResult(intent);
});
setDefaultLauncher(USER_0, mMainActivityFetcher.apply(LAUNCHER_2, USER_0));
// Verify that other launcher can't use this request
runWithCaller(LAUNCHER_2, USER_0, () -> {
assertFalse(mRequest.isValid());
assertExpectException(SecurityException.class, "Calling uid mismatch",
mRequest::accept);
});
runWithCaller(LAUNCHER_1, USER_0, () -> {
// Set some random caller UID.
mInjectedCallingUid = 12345;
assertFalse(mRequest.isValid());
assertExpectException(SecurityException.class, "Calling uid mismatch",
mRequest::accept);
});
runWithCaller(LAUNCHER_1, USER_0, () -> {
assertTrue(mRequest.isValid());
assertTrue(mRequest.accept());
});
}
private LauncherActivityInfo setupMockActivityInfo() {
doReturn(getTestContext().getPackageName()).when(mServiceContext).getPackageName();
doReturn(getTestContext().getContentResolver()).when(mServiceContext).getContentResolver();
LauncherActivityInfo info = mock(LauncherActivityInfo.class);
when(info.getComponentName()).thenReturn(
new ComponentName(getTestContext(), "a.ShortcutConfigActivity"));
when(info.getUser()).thenReturn(Process.myUserHandle());
return info;
}
public void testStartConfigActivity_defaultLauncher() {
LauncherActivityInfo info = setupMockActivityInfo();
setDefaultLauncher(USER_0, mMainActivityFetcher.apply(LAUNCHER_1, USER_0));
runWithCaller(LAUNCHER_1, USER_0, () ->
assertNotNull(mLauncherApps.getShortcutConfigActivityIntent(info))
);
}
public void testStartConfigActivity_nonDefaultLauncher() {
LauncherActivityInfo info = setupMockActivityInfo();
setDefaultLauncher(USER_0, mMainActivityFetcher.apply(LAUNCHER_1, USER_0));
runWithCaller(LAUNCHER_2, USER_0, () ->
assertExpectException(SecurityException.class, null, () ->
mLauncherApps.getShortcutConfigActivityIntent(info))
);
}
}

View File

@@ -1421,6 +1421,35 @@ public class ShortcutManagerTest8 extends BaseShortcutManagerTest {
});
}
public void testRequestPinShortcut_wrongLauncherCannotAccept() {
setDefaultLauncher(USER_0, mMainActivityFetcher.apply(LAUNCHER_1, USER_0));
runWithCaller(CALLING_PACKAGE_1, USER_P0, () -> {
ShortcutInfo s1 = makeShortcut("s1");
assertTrue(mManager.requestPinShortcut(s1, null));
verify(mServiceContext, times(0)).sendIntentSender(any(IntentSender.class));
});
final ArgumentCaptor<Intent> intent = ArgumentCaptor.forClass(Intent.class);
verify(mServiceContext).startActivityAsUser(intent.capture(), eq(HANDLE_USER_0));
final PinItemRequest request = mLauncherApps.getPinItemRequest(intent.getValue());
// Verify that other launcher can't use this request
runWithCaller(LAUNCHER_1, USER_0, () -> {
// Set some random caller UID.
mInjectedCallingUid = 12345;
assertFalse(request.isValid());
assertExpectException(SecurityException.class, "Calling uid mismatch", request::accept);
});
// The default launcher can still use this request
runWithCaller(LAUNCHER_1, USER_0, () -> {
assertTrue(request.isValid());
assertTrue(request.accept());
});
}
// TODO More tests:
// Cancel previous pending request and release memory?