From a6be88a10d6f6391b09f626ead051d0c698fb2d1 Mon Sep 17 00:00:00 2001 From: Sunny Goyal Date: Thu, 12 Jan 2017 16:27:58 -0800 Subject: [PATCH] Adding support for apps to return PinItemRequest to launchers as a result of Intent.ACTION_CREATE_SHORTCUT > Adding API to allow launchers to query shortcut config activities in managed profiles. > Adding API to allow the default Launcher to start the shortcut config activity in managed profiles. > Updating the ACTION_CREATE_SHORTCUT documentation to represend changes in the expected result. Test: Manual tests and all the unit tests adb shell am instrument -e class com.android.server.pm.ShortcutManagerTest1 -w com.android.frameworks.servicestests ... to test10 Change-Id: I785c4f2fba782b864cc401ac7905330ea4498289 --- api/current.txt | 11 +- api/system-current.txt | 11 +- api/test-current.txt | 11 +- core/java/android/content/Intent.java | 12 +- .../android/content/pm/ILauncherApps.aidl | 5 + .../android/content/pm/IShortcutService.aidl | 3 + .../java/android/content/pm/LauncherApps.java | 84 ++++++-- .../android/content/pm/ShortcutManager.java | 25 +++ .../server/pm/LauncherAppsService.java | 72 +++++-- .../pm/ShortcutRequestPinProcessor.java | 85 ++++++-- .../android/server/pm/ShortcutService.java | 21 +- .../tests/servicestests/AndroidManifest.xml | 6 + .../server/pm/ShortcutManagerTest10.java | 183 ++++++++++++++++++ .../server/pm/ShortcutManagerTest8.java | 29 +++ 14 files changed, 500 insertions(+), 58 deletions(-) create mode 100644 services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest10.java diff --git a/api/current.txt b/api/current.txt index 7eba6b00dc0e5..80ccdd09d9d5c 100644 --- a/api/current.txt +++ b/api/current.txt @@ -8981,10 +8981,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"; @@ -9758,6 +9758,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 getShortcutConfigActivityList(java.lang.String, android.os.UserHandle); method public android.graphics.drawable.Drawable getShortcutIconDrawable(android.content.pm.ShortcutInfo, int); method public java.util.List getShortcuts(android.content.pm.LauncherApps.ShortcutQuery, android.os.UserHandle); method public boolean hasShortcutHostPermission(); @@ -10356,6 +10358,7 @@ package android.content.pm { public class ShortcutManager { method public boolean addDynamicShortcuts(java.util.List); + method public android.content.Intent createShortcutResultIntent(android.content.pm.ShortcutInfo); method public void disableShortcuts(java.util.List); method public void disableShortcuts(java.util.List, java.lang.CharSequence); method public void enableShortcuts(java.util.List); diff --git a/api/system-current.txt b/api/system-current.txt index ba34b5ae7494b..7d87835c2bbe2 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -9363,10 +9363,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"; @@ -10175,6 +10175,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 getShortcutConfigActivityList(java.lang.String, android.os.UserHandle); method public android.graphics.drawable.Drawable getShortcutIconDrawable(android.content.pm.ShortcutInfo, int); method public java.util.List getShortcuts(android.content.pm.LauncherApps.ShortcutQuery, android.os.UserHandle); method public boolean hasShortcutHostPermission(); @@ -10846,6 +10848,7 @@ package android.content.pm { public class ShortcutManager { method public boolean addDynamicShortcuts(java.util.List); + method public android.content.Intent createShortcutResultIntent(android.content.pm.ShortcutInfo); method public void disableShortcuts(java.util.List); method public void disableShortcuts(java.util.List, java.lang.CharSequence); method public void enableShortcuts(java.util.List); diff --git a/api/test-current.txt b/api/test-current.txt index 0ce0e8cd4e1e5..6d22e0afceb61 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -9006,10 +9006,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"; @@ -9786,6 +9786,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 getShortcutConfigActivityList(java.lang.String, android.os.UserHandle); method public android.graphics.drawable.Drawable getShortcutIconDrawable(android.content.pm.ShortcutInfo, int); method public java.util.List getShortcuts(android.content.pm.LauncherApps.ShortcutQuery, android.os.UserHandle); method public boolean hasShortcutHostPermission(); @@ -10387,6 +10389,7 @@ package android.content.pm { public class ShortcutManager { ctor public ShortcutManager(android.content.Context); method public boolean addDynamicShortcuts(java.util.List); + method public android.content.Intent createShortcutResultIntent(android.content.pm.ShortcutInfo); method public void disableShortcuts(java.util.List); method public void disableShortcuts(java.util.List, java.lang.CharSequence); method public void enableShortcuts(java.util.List); diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index b05ceaa8b7232..8558ce7a9b9ee 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -715,11 +715,13 @@ public class Intent implements Parcelable, Cloneable { /** * Activity Action: Creates a shortcut. *

Input: Nothing.

- *

Output: An Intent representing the shortcut. The intent must contain three + *

Output: An Intent representing the {@link android.content.pm.ShortcutInfo} result.

+ *

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

* + * @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"; diff --git a/core/java/android/content/pm/ILauncherApps.aidl b/core/java/android/content/pm/ILauncherApps.aidl index 430c7e706b644..51524164538a7 100644 --- a/core/java/android/content/pm/ILauncherApps.aidl +++ b/core/java/android/content/pm/ILauncherApps.aidl @@ -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); } diff --git a/core/java/android/content/pm/IShortcutService.aidl b/core/java/android/content/pm/IShortcutService.aidl index 91df8e8b84c30..c90134a048305 100644 --- a/core/java/android/content/pm/IShortcutService.aidl +++ b/core/java/android/content/pm/IShortcutService.aidl @@ -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); diff --git a/core/java/android/content/pm/LauncherApps.java b/core/java/android/content/pm/LauncherApps.java index 4cdd6535e0a8a..cf873b0278d95 100644 --- a/core/java/android/content/pm/LauncherApps.java +++ b/core/java/android/content/pm/LauncherApps.java @@ -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 getActivityList(String packageName, UserHandle user) { - ParceledListSlice 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 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; } /** @@ -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 getShortcutConfigActivityList(@Nullable String packageName, + @NonNull UserHandle user) { + try { + return convertToActivityList(mService.getShortcutConfigActivities(packageName, user), + user); + } catch (RemoteException re) { + throw re.rethrowFromSystemServer(); + } + } + + private List convertToActivityList( + @Nullable ParceledListSlice activities, UserHandle user) { + if (activities == null) { + return Collections.EMPTY_LIST; + } + ArrayList 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. + * + *

The caller should receive {@link PinItemRequest} in onActivityResult on + * {@link android.app.Activity#RESULT_OK}. + * + *

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. * diff --git a/core/java/android/content/pm/ShortcutManager.java b/core/java/android/content/pm/ShortcutManager.java index 385340000d839..805054fad3896 100644 --- a/core/java/android/content/pm/ShortcutManager.java +++ b/core/java/android/content/pm/ShortcutManager.java @@ -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. diff --git a/services/core/java/com/android/server/pm/LauncherAppsService.java b/services/core/java/com/android/server/pm/LauncherAppsService.java index 48e000d82ec75..2ddf6dbde8aad 100644 --- a/services/core/java/com/android/server/pm/LauncherAppsService.java +++ b/services/core/java/com/android/server/pm/LauncherAppsService.java @@ -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 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 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 queryActivitiesForUser(Intent intent, + UserHandle user) { + ensureInUserProfiles(user, "Cannot retrieve activities for unrelated profile " + user); + if (!isUserEnabled(user)) { + return null; + } + + long ident = injectClearCallingIdentity(); + try { + List 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 { diff --git a/services/core/java/com/android/server/pm/ShortcutRequestPinProcessor.java b/services/core/java/com/android/server/pm/ShortcutRequestPinProcessor.java index c8ddf0a711bf6..a156356e825e7 100644 --- a/services/core/java/com/android/server/pm/ShortcutRequestPinProcessor.java +++ b/services/core/java/com/android/server/pm/ShortcutRequestPinProcessor.java @@ -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 confirmActivity) { + IntentSender resultIntent, Pair 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. diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java index a890526c0f914..ae709feb4bd3e 100644 --- a/services/core/java/com/android/server/pm/ShortcutService.java +++ b/services/core/java/com/android/server/pm/ShortcutService.java @@ -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}. diff --git a/services/tests/servicestests/AndroidManifest.xml b/services/tests/servicestests/AndroidManifest.xml index 1c92e452d03cb..b5f6723749115 100644 --- a/services/tests/servicestests/AndroidManifest.xml +++ b/services/tests/servicestests/AndroidManifest.xml @@ -137,6 +137,12 @@ android:enabled="true" android:exported="true"> + + + + + { + 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)) + ); + } +} diff --git a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest8.java b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest8.java index bcd72fcd6181f..df275d20b3c0d 100644 --- a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest8.java +++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest8.java @@ -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 = 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?