ShortcutManager: direct pin shortcut support.

Test: Manual test and all the unit tests:
adb shell am instrument -e class com.android.server.pm.ShortcutManagerTest1 -w com.android.frameworks.servicestests
... to test8

Bug 32908854

Change-Id: I11b81656959cccfb4efa83f08380b915e6eb84a6
diff --git a/Android.mk b/Android.mk
index 5d902c2..09fc269 100644
--- a/Android.mk
+++ b/Android.mk
@@ -144,6 +144,7 @@
 	core/java/android/content/ISyncStatusObserver.aidl \
 	core/java/android/content/pm/ILauncherApps.aidl \
 	core/java/android/content/pm/IOnAppsChangedListener.aidl \
+	core/java/android/content/pm/IOnPermissionsChangeListener.aidl \
 	core/java/android/content/pm/IOtaDexopt.aidl \
 	core/java/android/content/pm/IPackageDataObserver.aidl \
 	core/java/android/content/pm/IPackageDeleteObserver.aidl \
@@ -156,7 +157,7 @@
 	core/java/android/content/pm/IPackageManager.aidl \
 	core/java/android/content/pm/IPackageMoveObserver.aidl \
 	core/java/android/content/pm/IPackageStatsObserver.aidl \
-	core/java/android/content/pm/IOnPermissionsChangeListener.aidl \
+	core/java/android/content/pm/IPinItemRequest.aidl \
 	core/java/android/content/pm/IShortcutService.aidl \
 	core/java/android/content/pm/permission/IRuntimePermissionPresenter.aidl \
 	core/java/android/database/IContentObserver.aidl \
diff --git a/api/current.txt b/api/current.txt
index 304ccf8..52b12f6 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -9648,6 +9648,7 @@
 
   public class LauncherApps {
     method public java.util.List<android.content.pm.LauncherActivityInfo> getActivityList(java.lang.String, 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.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);
@@ -9663,6 +9664,8 @@
     method public void startShortcut(java.lang.String, java.lang.String, android.graphics.Rect, android.os.Bundle, android.os.UserHandle);
     method public void startShortcut(android.content.pm.ShortcutInfo, android.graphics.Rect, android.os.Bundle);
     method public void unregisterCallback(android.content.pm.LauncherApps.Callback);
+    field public static final java.lang.String ACTION_CONFIRM_PIN_ITEM = "android.content.pm.action.CONFIRM_PIN_ITEM";
+    field public static final java.lang.String EXTRA_PIN_ITEM_REQUEST = "android.content.pm.extra.PIN_ITEM_REQUEST";
   }
 
   public static abstract class LauncherApps.Callback {
@@ -9677,6 +9680,21 @@
     method public void onShortcutsChanged(java.lang.String, java.util.List<android.content.pm.ShortcutInfo>, android.os.UserHandle);
   }
 
+  public static final class LauncherApps.PinItemRequest implements android.os.Parcelable {
+    method public boolean accept(android.os.Bundle);
+    method public boolean accept();
+    method public int describeContents();
+    method public int getRequestType();
+    method public android.content.pm.ShortcutInfo getShortcutInfo();
+    method public boolean isValid();
+    method public void writeToParcel(android.os.Parcel, int);
+    field public static final android.os.Parcelable.Creator<android.content.pm.LauncherApps.PinItemRequest> CREATOR;
+    field public static final int REQUEST_TYPE_SHORTCUT = 1; // 0x1
+  }
+
+  public static abstract class LauncherApps.PinItemRequest.RequestType implements java.lang.annotation.Annotation {
+  }
+
   public static class LauncherApps.ShortcutQuery {
     ctor public LauncherApps.ShortcutQuery();
     method public android.content.pm.LauncherApps.ShortcutQuery setActivity(android.content.ComponentName);
@@ -10241,9 +10259,11 @@
     method public int getMaxShortcutCountPerActivity();
     method public java.util.List<android.content.pm.ShortcutInfo> getPinnedShortcuts();
     method public boolean isRateLimitingActive();
+    method public boolean isRequestPinShortcutSupported();
     method public void removeAllDynamicShortcuts();
     method public void removeDynamicShortcuts(java.util.List<java.lang.String>);
     method public void reportShortcutUsed(java.lang.String);
+    method public boolean requestPinShortcut(android.content.pm.ShortcutInfo, android.content.IntentSender);
     method public boolean setDynamicShortcuts(java.util.List<android.content.pm.ShortcutInfo>);
     method public boolean updateShortcuts(java.util.List<android.content.pm.ShortcutInfo>);
   }
diff --git a/api/system-current.txt b/api/system-current.txt
index d54e6a9..9430f79 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -10048,6 +10048,7 @@
 
   public class LauncherApps {
     method public java.util.List<android.content.pm.LauncherActivityInfo> getActivityList(java.lang.String, 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.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);
@@ -10063,6 +10064,8 @@
     method public void startShortcut(java.lang.String, java.lang.String, android.graphics.Rect, android.os.Bundle, android.os.UserHandle);
     method public void startShortcut(android.content.pm.ShortcutInfo, android.graphics.Rect, android.os.Bundle);
     method public void unregisterCallback(android.content.pm.LauncherApps.Callback);
+    field public static final java.lang.String ACTION_CONFIRM_PIN_ITEM = "android.content.pm.action.CONFIRM_PIN_ITEM";
+    field public static final java.lang.String EXTRA_PIN_ITEM_REQUEST = "android.content.pm.extra.PIN_ITEM_REQUEST";
   }
 
   public static abstract class LauncherApps.Callback {
@@ -10077,6 +10080,21 @@
     method public void onShortcutsChanged(java.lang.String, java.util.List<android.content.pm.ShortcutInfo>, android.os.UserHandle);
   }
 
+  public static final class LauncherApps.PinItemRequest implements android.os.Parcelable {
+    method public boolean accept(android.os.Bundle);
+    method public boolean accept();
+    method public int describeContents();
+    method public int getRequestType();
+    method public android.content.pm.ShortcutInfo getShortcutInfo();
+    method public boolean isValid();
+    method public void writeToParcel(android.os.Parcel, int);
+    field public static final android.os.Parcelable.Creator<android.content.pm.LauncherApps.PinItemRequest> CREATOR;
+    field public static final int REQUEST_TYPE_SHORTCUT = 1; // 0x1
+  }
+
+  public static abstract class LauncherApps.PinItemRequest.RequestType implements java.lang.annotation.Annotation {
+  }
+
   public static class LauncherApps.ShortcutQuery {
     ctor public LauncherApps.ShortcutQuery();
     method public android.content.pm.LauncherApps.ShortcutQuery setActivity(android.content.ComponentName);
@@ -10712,9 +10730,11 @@
     method public int getMaxShortcutCountPerActivity();
     method public java.util.List<android.content.pm.ShortcutInfo> getPinnedShortcuts();
     method public boolean isRateLimitingActive();
+    method public boolean isRequestPinShortcutSupported();
     method public void removeAllDynamicShortcuts();
     method public void removeDynamicShortcuts(java.util.List<java.lang.String>);
     method public void reportShortcutUsed(java.lang.String);
+    method public boolean requestPinShortcut(android.content.pm.ShortcutInfo, android.content.IntentSender);
     method public boolean setDynamicShortcuts(java.util.List<android.content.pm.ShortcutInfo>);
     method public boolean updateShortcuts(java.util.List<android.content.pm.ShortcutInfo>);
   }
diff --git a/api/test-current.txt b/api/test-current.txt
index f269479..dcac07e 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -9676,6 +9676,7 @@
   public class LauncherApps {
     ctor public LauncherApps(android.content.Context);
     method public java.util.List<android.content.pm.LauncherActivityInfo> getActivityList(java.lang.String, 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.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);
@@ -9691,6 +9692,8 @@
     method public void startShortcut(java.lang.String, java.lang.String, android.graphics.Rect, android.os.Bundle, android.os.UserHandle);
     method public void startShortcut(android.content.pm.ShortcutInfo, android.graphics.Rect, android.os.Bundle);
     method public void unregisterCallback(android.content.pm.LauncherApps.Callback);
+    field public static final java.lang.String ACTION_CONFIRM_PIN_ITEM = "android.content.pm.action.CONFIRM_PIN_ITEM";
+    field public static final java.lang.String EXTRA_PIN_ITEM_REQUEST = "android.content.pm.extra.PIN_ITEM_REQUEST";
   }
 
   public static abstract class LauncherApps.Callback {
@@ -9705,6 +9708,21 @@
     method public void onShortcutsChanged(java.lang.String, java.util.List<android.content.pm.ShortcutInfo>, android.os.UserHandle);
   }
 
+  public static final class LauncherApps.PinItemRequest implements android.os.Parcelable {
+    method public boolean accept(android.os.Bundle);
+    method public boolean accept();
+    method public int describeContents();
+    method public int getRequestType();
+    method public android.content.pm.ShortcutInfo getShortcutInfo();
+    method public boolean isValid();
+    method public void writeToParcel(android.os.Parcel, int);
+    field public static final android.os.Parcelable.Creator<android.content.pm.LauncherApps.PinItemRequest> CREATOR;
+    field public static final int REQUEST_TYPE_SHORTCUT = 1; // 0x1
+  }
+
+  public static abstract class LauncherApps.PinItemRequest.RequestType implements java.lang.annotation.Annotation {
+  }
+
   public static class LauncherApps.ShortcutQuery {
     ctor public LauncherApps.ShortcutQuery();
     method public android.content.pm.LauncherApps.ShortcutQuery setActivity(android.content.ComponentName);
@@ -10272,9 +10290,11 @@
     method public int getMaxShortcutCountPerActivity();
     method public java.util.List<android.content.pm.ShortcutInfo> getPinnedShortcuts();
     method public boolean isRateLimitingActive();
+    method public boolean isRequestPinShortcutSupported();
     method public void removeAllDynamicShortcuts();
     method public void removeDynamicShortcuts(java.util.List<java.lang.String>);
     method public void reportShortcutUsed(java.lang.String);
+    method public boolean requestPinShortcut(android.content.pm.ShortcutInfo, android.content.IntentSender);
     method public boolean setDynamicShortcuts(java.util.List<android.content.pm.ShortcutInfo>);
     method public boolean updateShortcuts(java.util.List<android.content.pm.ShortcutInfo>);
   }
diff --git a/core/java/android/content/pm/IPinItemRequest.aidl b/core/java/android/content/pm/IPinItemRequest.aidl
new file mode 100644
index 0000000..efe2835
--- /dev/null
+++ b/core/java/android/content/pm/IPinItemRequest.aidl
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2016 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 android.content.pm;
+
+import android.os.Bundle;
+
+/**
+ * {@hide}
+ */
+interface IPinItemRequest {
+    boolean isValid();
+    boolean accept(in Bundle options);
+}
diff --git a/core/java/android/content/pm/IShortcutService.aidl b/core/java/android/content/pm/IShortcutService.aidl
index 1bf2ab0..91df8e8 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.IntentSender;
 import android.content.pm.ParceledListSlice;
 import android.content.pm.ShortcutInfo;
 
@@ -41,6 +42,9 @@
 
     boolean updateShortcuts(String packageName, in ParceledListSlice shortcuts, int userId);
 
+    boolean requestPinShortcut(String packageName, in ShortcutInfo shortcut,
+            in IntentSender resultIntent, int userId);
+
     void disableShortcuts(String packageName, in List shortcutIds, CharSequence disabledMessage,
             int disabledMessageResId, int userId);
 
@@ -63,4 +67,6 @@
     byte[] getBackupPayload(int user);
 
     void applyRestore(in byte[] payload, int user);
+
+    boolean isRequestPinShortcutSupported(int user);
 }
\ No newline at end of file
diff --git a/core/java/android/content/pm/LauncherApps.java b/core/java/android/content/pm/LauncherApps.java
index 7fc8044..5f4bc00 100644
--- a/core/java/android/content/pm/LauncherApps.java
+++ b/core/java/android/content/pm/LauncherApps.java
@@ -19,6 +19,8 @@
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.SdkConstant;
+import android.annotation.SdkConstant.SdkConstantType;
 import android.annotation.TestApi;
 import android.content.ActivityNotFoundException;
 import android.content.ComponentName;
@@ -32,11 +34,14 @@
 import android.graphics.Rect;
 import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
+import android.os.Parcel;
 import android.os.ParcelFileDescriptor;
+import android.os.Parcelable;
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.os.UserHandle;
@@ -69,6 +74,36 @@
     static final String TAG = "LauncherApps";
     static final boolean DEBUG = false;
 
+    /**
+     * Activity Action: For the default launcher to show the confirmation dialog to create
+     * a pinned shortcut.
+     *
+     * <p>See the {@link ShortcutManager} javadoc for details.
+     *
+     * <p>
+     * Use {@link #getPinItemRequest(Intent)} to get a {@link PinItemRequest} object,
+     * and call {@link PinItemRequest#accept(Bundle)}
+     * if the user accepts.  If the user doesn't accept, no further action is required.
+     *
+     * @see #EXTRA_PIN_ITEM_REQUEST
+     */
+    @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+    public static final String ACTION_CONFIRM_PIN_ITEM =
+            "android.content.pm.action.CONFIRM_PIN_ITEM";
+
+    /**
+     * An extra for {@link #ACTION_CONFIRM_PIN_ITEM} containing a
+     * {@link ShortcutInfo} of the shortcut the publisher app asked to pin.
+     *
+     * <p>A helper function {@link #getPinItemRequest(Intent)} can be used
+     * instead of using this constant directly.
+     *
+     * @see #ACTION_CONFIRM_PIN_ITEM
+     */
+    public static final String EXTRA_PIN_ITEM_REQUEST =
+            "android.content.pm.extra.PIN_ITEM_REQUEST";
+
+
     private Context mContext;
     private ILauncherApps mService;
     private PackageManager mPm;
@@ -655,23 +690,41 @@
                 }
             }
         } else if (shortcut.hasIconResource()) {
-            try {
-                final int resId = shortcut.getIconResourceId();
-                if (resId == 0) {
-                    return null; // Shouldn't happen but just in case.
+            return loadDrawableResourceFromPackage(shortcut.getPackage(),
+                    shortcut.getIconResourceId(), shortcut.getUserHandle(), density);
+        } else if (shortcut.getIcon() != null) {
+            // This happens if a shortcut is pending-approval.
+            final Icon icon = shortcut.getIcon();
+            switch (icon.getType()) {
+                case Icon.TYPE_RESOURCE: {
+                    return loadDrawableResourceFromPackage(shortcut.getPackage(),
+                            icon.getResId(), shortcut.getUserHandle(), density);
                 }
-                final ApplicationInfo ai = getApplicationInfo(shortcut.getPackage(),
-                        /* flags =*/ 0, shortcut.getUserHandle());
-                final Resources res = mContext.getPackageManager().getResourcesForApplication(ai);
-                return res.getDrawableForDensity(resId, density);
-            } catch (NameNotFoundException | Resources.NotFoundException e) {
-                return null;
+                case Icon.TYPE_BITMAP: {
+                    return icon.loadDrawable(mContext);
+                }
+                default:
+                    return null; // Shouldn't happen though.
             }
         } else {
             return null; // Has no icon.
         }
     }
 
+    private Drawable loadDrawableResourceFromPackage(String packageName, int resId,
+            UserHandle user, int density) {
+        try {
+            if (resId == 0) {
+                return null; // Shouldn't happen but just in case.
+            }
+            final ApplicationInfo ai = getApplicationInfo(packageName, /* flags =*/ 0, user);
+            final Resources res = mContext.getPackageManager().getResourcesForApplication(ai);
+            return res.getDrawableForDensity(resId, density);
+        } catch (NameNotFoundException | Resources.NotFoundException e) {
+            return null;
+        }
+    }
+
     /**
      * Returns the shortcut icon with badging appropriate for the profile.
      *
@@ -1064,4 +1117,121 @@
             obtainMessage(MSG_SHORTCUT_CHANGED, info).sendToTarget();
         }
     }
+
+    /**
+     * A helper method to extract a {@link PinItemRequest} set to
+     * the {@link #EXTRA_PIN_ITEM_REQUEST} extra.
+     */
+    public PinItemRequest getPinItemRequest(Intent intent) {
+        return intent.getParcelableExtra(EXTRA_PIN_ITEM_REQUEST);
+    }
+
+    /**
+     * Represents a "pin shortcut" request made by an app, which is sent with
+     * an {@link #ACTION_CONFIRM_PIN_ITEM} intent to the default launcher app.
+     *
+     * @see #EXTRA_PIN_ITEM_REQUEST
+     * @see #getPinItemRequest(Intent)
+     */
+    public static final class PinItemRequest implements Parcelable {
+
+        /** This is a request to pin shortcut. */
+        public static final int REQUEST_TYPE_SHORTCUT = 1;
+
+        @IntDef(value = {REQUEST_TYPE_SHORTCUT})
+        @Retention(RetentionPolicy.SOURCE)
+        public @interface RequestType {}
+
+        private final int mRequestType;
+        private final ShortcutInfo mShortcutInfo;
+        private final IPinItemRequest mInner;
+
+        /**
+         * @hide
+         */
+        public PinItemRequest(@RequestType int requestType, ShortcutInfo shortcutInfo,
+                IPinItemRequest inner) {
+            mRequestType = requestType;
+            mShortcutInfo = shortcutInfo;
+            mInner = inner;
+        }
+
+        /**
+         * Represents the type of a request.  For now {@link #REQUEST_TYPE_SHORTCUT} is the only
+         * valid type.
+         */
+        @RequestType
+        public int getRequestType() {
+            return mRequestType;
+        }
+
+        /**
+         * {@link ShortcutInfo} sent by the requesting app.  Always non-null for a
+         * {@link #REQUEST_TYPE_SHORTCUT} request.
+         */
+        @Nullable
+        public ShortcutInfo getShortcutInfo() {
+            return mShortcutInfo;
+        }
+
+        /**
+         * Return {@code TRUE} if a request is valid -- i.e. {@link #accept(Bundle)} has not been
+         * called, and it has not been canceled.
+         */
+        public boolean isValid() {
+            try {
+                return mInner.isValid();
+            } catch (RemoteException e) {
+                return false;
+            }
+        }
+
+        /**
+         * Called by the receiving launcher app when the user accepts the request.
+         */
+        public boolean accept(@Nullable Bundle options) {
+            try {
+                return mInner.accept(options);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+
+        /**
+         * Same as as {@link #accept(Bundle)} with no options.
+         */
+        public boolean accept() {
+            return accept(/* options= */ null);
+        }
+
+        private PinItemRequest(Parcel source) {
+            final ClassLoader cl = getClass().getClassLoader();
+
+            mRequestType = source.readInt();
+            mShortcutInfo = source.readParcelable(cl);
+            mInner = IPinItemRequest.Stub.asInterface(source.readStrongBinder());
+        }
+
+        @Override
+        public void writeToParcel(Parcel dest, int flags) {
+            dest.writeInt(mRequestType);
+            dest.writeParcelable(mShortcutInfo, flags);
+            dest.writeStrongBinder(mInner.asBinder());
+        }
+
+        public static final Creator<PinItemRequest> CREATOR =
+                new Creator<PinItemRequest>() {
+                    public PinItemRequest createFromParcel(Parcel source) {
+                        return new PinItemRequest(source);
+                    }
+                    public PinItemRequest[] newArray(int size) {
+                        return new PinItemRequest[size];
+                    }
+                };
+
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+    }
 }
diff --git a/core/java/android/content/pm/ShortcutManager.java b/core/java/android/content/pm/ShortcutManager.java
index f7c4d59..c8f00b8 100644
--- a/core/java/android/content/pm/ShortcutManager.java
+++ b/core/java/android/content/pm/ShortcutManager.java
@@ -16,21 +16,31 @@
 package android.content.pm;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SdkConstant;
+import android.annotation.SdkConstant.SdkConstantType;
 import android.annotation.TestApi;
 import android.annotation.UserIdInt;
 import android.app.Activity;
 import android.app.usage.UsageStatsManager;
 import android.content.Context;
 import android.content.Intent;
+import android.content.IntentSender;
+import android.os.Binder;
+import android.os.Parcel;
+import android.os.Parcelable;
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.os.UserHandle;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.Preconditions;
 
 import java.util.List;
 
 /**
+ * <p><strong>TODO Update the overview to how to use the O new features.</strong></p>
+ *
  * The ShortcutManager manages an app's <em>shortcuts</em>. Shortcuts provide users
  * with quick access to activities other than an app's main activity in the currently-active
  * launcher.  For example,
@@ -618,7 +628,7 @@
      *
      * @throws IllegalStateException when the user is locked.
      */
-    public boolean updateShortcuts(List<ShortcutInfo> shortcutInfoList) {
+    public boolean updateShortcuts(@NonNull List<ShortcutInfo> shortcutInfoList) {
         try {
             return mService.updateShortcuts(mContext.getPackageName(),
                     new ParceledListSlice(shortcutInfoList), injectMyUserId());
@@ -815,6 +825,61 @@
     }
 
     /**
+     * Return {@code TRUE} if the default launcher supports
+     * {@link #requestPinShortcut(ShortcutInfo, IntentSender)}.
+     */
+    public boolean isRequestPinShortcutSupported() {
+        try {
+            return mService.isRequestPinShortcutSupported(injectMyUserId());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Request to create a pinned shortcut.  The default launcher will receive this request and
+     * ask the user for approval.  If the user approves it, the shortcut will be created and
+     * {@code resultIntent} will be sent.  Otherwise, no responses will be sent to the caller.
+     *
+     * <p>When a request is denied by the user, the caller app will not get any response.
+     *
+     * <p>Only apps with a foreground activity or a foreground service can call it.  Otherwise
+     * it'll throw {@link IllegalStateException}.
+     *
+     * <p>When an app calls this API when a previous request is still waiting for a response,
+     * the previous request will be canceled.
+     *
+     * @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.
+     * @param resultIntent If not null, this intent will be sent when the shortcut is pinned.
+     *    Use {@link android.app.PendingIntent#getIntentSender()} to create a {@link IntentSender}.
+     *
+     * @return {@code TRUE} if the launcher supports this feature.  Note the API will return without
+     *    waiting for the user to respond, so getting {@code TRUE} from this API does *not* mean
+     *    the shortcut is pinned.  {@code FALSE} if the launcher doesn't support this feature.
+     *
+     * @see #isRequestPinShortcutSupported()
+     * @see IntentSender
+     * @see android.app.PendingIntent#getIntentSender()
+     *
+     * @throws IllegalArgumentException if a shortcut with the same ID exists and is disabled.
+     * @throws IllegalStateException The caller doesn't have a foreground activity or a foreground
+     * service.
+     */
+    public boolean requestPinShortcut(@NonNull ShortcutInfo shortcut,
+            @Nullable IntentSender resultIntent) {
+        try {
+            return mService.requestPinShortcut(mContext.getPackageName(), shortcut,
+                    resultIntent, 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.
      * For example, when the user sends an "inline reply" on a notification, the system UI will
diff --git a/services/core/java/com/android/server/pm/ShortcutLauncher.java b/services/core/java/com/android/server/pm/ShortcutLauncher.java
index 2af1bcb..3060840 100644
--- a/services/core/java/com/android/server/pm/ShortcutLauncher.java
+++ b/services/core/java/com/android/server/pm/ShortcutLauncher.java
@@ -16,6 +16,7 @@
 package com.android.server.pm;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.annotation.UserIdInt;
 import android.content.pm.PackageInfo;
 import android.content.pm.ShortcutInfo;
@@ -103,6 +104,9 @@
         // Nothing to do.
     }
 
+    /**
+     * Pin the given shortcuts, replacing the current pinned ones.
+     */
     public void pinShortcuts(@UserIdInt int packageUserId,
             @NonNull String packageName, @NonNull List<String> ids) {
         final ShortcutPackage packageShortcuts =
@@ -143,11 +147,39 @@
     /**
      * Return the pinned shortcut IDs for the publisher package.
      */
+    @Nullable
     public ArraySet<String> getPinnedShortcutIds(@NonNull String packageName,
             @UserIdInt int packageUserId) {
         return mPinnedShortcuts.get(PackageWithUser.of(packageUserId, packageName));
     }
 
+    /**
+     * Return true if the given shortcut is pinned by this launcher.
+     */
+    public boolean hasPinned(ShortcutInfo shortcut) {
+        final ArraySet<String> pinned =
+                getPinnedShortcutIds(shortcut.getPackage(), shortcut.getUserId());
+        return (pinned != null) && pinned.contains(shortcut.getId());
+    }
+
+    /**
+     * Additionally pin a shortcut. c.f. {@link #pinShortcuts(int, String, List)}
+     */
+    public void addPinnedShortcut(@NonNull String packageName, @UserIdInt int packageUserId,
+            String id) {
+        final ArraySet<String> pinnedSet = getPinnedShortcutIds(packageName, packageUserId);
+        final ArrayList<String> pinnedList;
+        if (pinnedSet != null) {
+            pinnedList = new ArrayList<>(pinnedSet.size() + 1);
+            pinnedList.addAll(pinnedSet);
+        } else {
+            pinnedList = new ArrayList<>(1);
+        }
+        pinnedList.add(id);
+
+        pinShortcuts(packageUserId, packageName, pinnedList);
+    }
+
     boolean cleanUpPackage(String packageName, @UserIdInt int packageUserId) {
         return mPinnedShortcuts.remove(PackageWithUser.of(packageUserId, packageName)) != null;
     }
diff --git a/services/core/java/com/android/server/pm/ShortcutPackage.java b/services/core/java/com/android/server/pm/ShortcutPackage.java
index 2eb0778..b745062 100644
--- a/services/core/java/com/android/server/pm/ShortcutPackage.java
+++ b/services/core/java/com/android/server/pm/ShortcutPackage.java
@@ -179,7 +179,7 @@
         }
     }
 
-    private void ensureNotImmutable(@NonNull String id) {
+    public void ensureNotImmutable(@NonNull String id) {
         ensureNotImmutable(mShortcuts.get(id));
     }
 
@@ -706,6 +706,7 @@
             for (int i = mShortcuts.size() - 1; i >= 0; i--) {
                 final ShortcutInfo si = mShortcuts.valueAt(i);
 
+                // Disable dynamic shortcuts whose target activity is gone.
                 if (si.isDynamic()) {
                     if (!s.injectIsMainActivity(si.getActivity(), getPackageUserId())) {
                         Slog.w(TAG, String.format(
diff --git a/services/core/java/com/android/server/pm/ShortcutRequestPinProcessor.java b/services/core/java/com/android/server/pm/ShortcutRequestPinProcessor.java
new file mode 100644
index 0000000..7928257
--- /dev/null
+++ b/services/core/java/com/android/server/pm/ShortcutRequestPinProcessor.java
@@ -0,0 +1,342 @@
+/*
+ * Copyright (C) 2016 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 android.annotation.Nullable;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.IntentSender;
+import android.content.pm.IPinItemRequest;
+import android.content.pm.LauncherApps;
+import android.content.pm.LauncherApps.PinItemRequest;
+import android.content.pm.ShortcutInfo;
+import android.os.Bundle;
+import android.os.UserHandle;
+import android.util.Log;
+import android.util.Pair;
+import android.util.Slog;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.Preconditions;
+
+/**
+ * Handles {@link android.content.pm.ShortcutManager#requestPinShortcut} related tasks.
+ */
+class ShortcutRequestPinProcessor {
+    private static final String TAG = ShortcutService.TAG;
+    private static final boolean DEBUG = ShortcutService.DEBUG;
+
+    private final ShortcutService mService;
+    private final Object mLock;
+
+    /**
+     * Internal for {@link android.content.pm.LauncherApps.PinItemRequest} which receives callbacks.
+     */
+    private static class PinShortcutRequestInner extends IPinItemRequest.Stub {
+        private final ShortcutRequestPinProcessor mProcessor;
+        public final ShortcutInfo shortcut;
+        private final IntentSender mResultIntent;
+
+        public final String launcherPackage;
+        public final int launcherUserId;
+        public final boolean preExisting;
+
+        @GuardedBy("this")
+        private boolean mAccepted;
+
+        private PinShortcutRequestInner(ShortcutRequestPinProcessor processor,
+                ShortcutInfo shortcut, IntentSender resultIntent,
+                String launcherPackage, int launcherUserId, boolean preExisting) {
+            mProcessor = processor;
+            this.shortcut = shortcut;
+            mResultIntent = resultIntent;
+            this.launcherPackage = launcherPackage;
+            this.launcherUserId = launcherUserId;
+            this.preExisting = preExisting;
+        }
+
+        @Override
+        public boolean isValid() {
+            // TODO When an app calls requestPinShortcut(), all pending requests should be
+            // invalidated.
+            synchronized (this) {
+                return !mAccepted;
+            }
+        }
+
+        /**
+         * Called when the launcher calls {@link PinItemRequest#accept}.
+         */
+        @Override
+        public boolean accept(Bundle options) {
+            // Make sure the options are unparcellable by the FW. (e.g. not containing unknown
+            // classes.)
+            if (options != null) {
+                try {
+                    options.size();
+                } catch (RuntimeException e) {
+                    throw new IllegalArgumentException("options cannot be unparceled", e);
+                }
+            }
+            synchronized (this) {
+                if (mAccepted) {
+                    throw new IllegalStateException("accept() called already");
+                }
+                mAccepted = true;
+            }
+            if (DEBUG) {
+                Slog.d(TAG, "Launcher accepted shortcut. ID=" + shortcut.getId()
+                        + " package=" + shortcut.getPackage()
+                        + " options=" + options);
+            }
+
+            // Pin it and send the result intent.
+            if (mProcessor.directPinShortcut(this)) {
+                mProcessor.sendResultIntent(mResultIntent);
+                return true;
+            } else {
+                return false;
+            }
+        }
+    }
+
+    public ShortcutRequestPinProcessor(ShortcutService service, Object lock) {
+        mService = service;
+        mLock = lock;
+    }
+
+    public boolean isRequestPinnedShortcutSupported(int callingUserId) {
+        return getRequestPinShortcutConfirmationActivity(callingUserId) != null;
+    }
+
+    /**
+     * Handle {@link android.content.pm.ShortcutManager#requestPinShortcut)}.
+     */
+    public boolean requestPinShortcutLocked(ShortcutInfo inShortcut, IntentSender resultIntent) {
+
+        // First, make sure the launcher supports it.
+
+        // Find the confirmation activity in the default launcher.
+        final Pair<ComponentName, Integer> confirmActivity =
+                getRequestPinShortcutConfirmationActivity(inShortcut.getUserId());
+
+        // If the launcher doesn't support it, just return a rejected result and finish.
+        if (confirmActivity == null) {
+            Log.w(TAG, "Launcher doesn't support requestPinnedShortcut(). Shortcut not created.");
+            return false;
+        }
+
+        final ComponentName launcherComponent = confirmActivity.first;
+        final String launcherPackage = confirmActivity.first.getPackageName();
+        final int launcherUserId = confirmActivity.second;
+
+        // 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.
+
+        final ShortcutPackage ps = mService.getPackageShortcutsForPublisherLocked(
+                inShortcut.getPackage(), inShortcut.getUserId());
+
+        final ShortcutInfo existing = ps.findShortcutById(inShortcut.getId());
+        final boolean existsAlready = existing != null;
+
+        if (DEBUG) {
+            Slog.d(TAG, "requestPinnedShortcut package=" + inShortcut.getPackage()
+                    + " existsAlready=" + existsAlready
+                    + " shortcut=" + inShortcut.toInsecureString());
+        }
+
+        // This is the shortcut that'll be sent to the launcher.
+        final ShortcutInfo shortcutToSend;
+
+        if (existsAlready) {
+            validateExistingShortcut(existing);
+
+            // See if it's already pinned.
+            if (mService.getLauncherShortcutsLocked(
+                    launcherPackage, existing.getUserId(), launcherUserId).hasPinned(existing)) {
+                Log.i(TAG, "Launcher's already pinning shortcut " + existing.getId()
+                        + " for package " + existing.getPackage());
+                sendResultIntent(resultIntent);
+                return true;
+            }
+
+            // Pass a clone, not the original.
+            // Note this will remove the intent and icons.
+            shortcutToSend = existing.clone(ShortcutInfo.CLONE_REMOVE_FOR_LAUNCHER);
+            shortcutToSend.clearFlags(ShortcutInfo.FLAG_PINNED);
+        } else {
+            // It doesn't exist, so it must have all mandatory fields.
+            mService.validateShortcutForPinRequest(inShortcut);
+
+            // Initialize the ShortcutInfo for pending approval.
+            inShortcut.resolveResourceStrings(mService.injectGetResourcesForApplicationAsUser(
+                    inShortcut.getPackage(), inShortcut.getUserId()));
+            if (DEBUG) {
+                Slog.d(TAG, "resolved shortcut=" + inShortcut.toInsecureString());
+            }
+            // TODO Remove the intent here -- don't pass shortcut intents to the launcher.
+            shortcutToSend = inShortcut;
+        }
+
+        // Create a request object.
+        final PinShortcutRequestInner inner =
+                new PinShortcutRequestInner(this, shortcutToSend, resultIntent,
+                        launcherPackage, launcherUserId, existsAlready);
+
+        final PinItemRequest outer = new PinItemRequest(PinItemRequest.REQUEST_TYPE_SHORTCUT,
+                shortcutToSend, inner);
+
+        return startRequestConfirmActivity(launcherComponent, launcherUserId, outer);
+    }
+
+    private void validateExistingShortcut(ShortcutInfo shortcutInfo) {
+        // Make sure it's enabled.
+        // (Because we can't always force enable it automatically as it may be a stale
+        // manifest shortcut.)
+        Preconditions.checkState(shortcutInfo.isEnabled(),
+                "Shortcut ID=" + shortcutInfo + " already exists but disabled.");
+
+    }
+
+    private boolean startRequestConfirmActivity(ComponentName activity, int launcherUserId,
+            PinItemRequest request) {
+        // Start the activity.
+        final Intent confirmIntent = new Intent(LauncherApps.ACTION_CONFIRM_PIN_ITEM);
+        confirmIntent.setComponent(activity);
+        confirmIntent.putExtra(LauncherApps.EXTRA_PIN_ITEM_REQUEST, request);
+        confirmIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+
+        final long token = mService.injectClearCallingIdentity();
+        try {
+            mService.mContext.startActivityAsUser(
+                    confirmIntent, UserHandle.of(launcherUserId));
+        } catch (RuntimeException e) { // ActivityNotFoundException, etc.
+            Log.e(TAG, "Unable to start activity " + activity, e);
+            return false;
+        } finally {
+            mService.injectRestoreCallingIdentity(token);
+        }
+        return true;
+    }
+
+    /**
+     * Find the activity that handles {@link LauncherApps#ACTION_CONFIRM_PIN_ITEM} in the
+     * default launcher.
+     */
+    @Nullable
+    @VisibleForTesting
+    Pair<ComponentName, Integer> getRequestPinShortcutConfirmationActivity(
+            int callingUserId) {
+        // Find the default launcher.
+        final int launcherUserId = mService.getParentOrSelfUserId(callingUserId);
+        final ComponentName defaultLauncher = mService.getDefaultLauncher(launcherUserId);
+
+        if (defaultLauncher == null) {
+            Log.e(TAG, "Default launcher not found.");
+            return null;
+        }
+        final ComponentName activity = mService.injectGetPinConfirmationActivity(
+                defaultLauncher.getPackageName(), launcherUserId);
+        return (activity == null) ? null : Pair.create(activity, launcherUserId);
+    }
+
+    public void sendResultIntent(@Nullable IntentSender intent) {
+        if (DEBUG) {
+            Slog.d(TAG, "Sending result intent.");
+        }
+        mService.injectSendIntentSender(intent);
+    }
+
+    /**
+     * The last step of the "request pin shortcut" flow.  Called when the launcher accepted a
+     * request.
+     */
+    public boolean directPinShortcut(PinShortcutRequestInner request) {
+
+        final ShortcutInfo original = request.shortcut;
+        final int appUserId = original.getUserId();
+        final String appPackageName = original.getPackage();
+        final int launcherUserId = request.launcherUserId;
+        final String launcherPackage = request.launcherPackage;
+        final String shortcutId = original.getId();
+
+        synchronized (mLock) {
+            if (!(mService.isUserUnlockedL(appUserId)
+                    && mService.isUserUnlockedL(request.launcherUserId))) {
+                Log.w(TAG, "User is locked now.");
+                return false;
+            }
+
+            final ShortcutPackage ps = mService.getPackageShortcutsForPublisherLocked(
+                    appPackageName, appUserId);
+            final ShortcutInfo current = ps.findShortcutById(shortcutId);
+
+            // The shortcut might have been changed, so we need to do the same validation again.
+            try {
+                if (current == null) {
+                    // It doesn't exist, so it must have all necessary fields.
+                    mService.validateShortcutForPinRequest(request.shortcut);
+                } else {
+                    validateExistingShortcut(current);
+                }
+            } catch (RuntimeException e) {
+                Log.w(TAG, "Unable to pin shortcut: " + e.getMessage());
+                return false;
+            }
+
+            // If the shortcut doesn't exist, need to create it.
+            // First, create it as a dynamic shortcut.
+            if (current == null) {
+                if (DEBUG) {
+                    Slog.d(TAG, "Temporarily adding " + shortcutId + " as dynamic");
+                }
+                // Add as a dynamic shortcut.
+                if (original.getActivity() == null) {
+                    original.setActivity(mService.getDummyMainActivity(appPackageName));
+                }
+                ps.addOrUpdateDynamicShortcut(original);
+            }
+
+            // Pin the shortcut.
+            if (DEBUG) {
+                Slog.d(TAG, "Pinning " + shortcutId);
+            }
+
+            final ShortcutLauncher launcher = mService.getLauncherShortcutsLocked(
+                    launcherPackage, appUserId, launcherUserId);
+            launcher.attemptToRestoreIfNeededAndSave();
+            launcher.addPinnedShortcut(appPackageName, appUserId, shortcutId);
+
+            if (current == null) {
+                if (DEBUG) {
+                    Slog.d(TAG, "Removing " + shortcutId + " as dynamic");
+                }
+                ps.deleteDynamicWithId(shortcutId);
+            }
+
+            ps.adjustRanks(); // Shouldn't be needed, but just in case.
+        }
+
+        mService.verifyStates();
+        mService.packageShortcutsChanged(appPackageName, appUserId);
+
+        return true;
+    }
+}
diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java
index c5c1c0c..424830b 100644
--- a/services/core/java/com/android/server/pm/ShortcutService.java
+++ b/services/core/java/com/android/server/pm/ShortcutService.java
@@ -29,6 +29,8 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.content.IntentSender;
+import android.content.IntentSender.SendIntentException;
 import android.content.pm.ActivityInfo;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.IPackageManager;
@@ -42,8 +44,10 @@
 import android.content.pm.ParceledListSlice;
 import android.content.pm.ResolveInfo;
 import android.content.pm.ShortcutInfo;
+import android.content.pm.ShortcutManager;
 import android.content.pm.ShortcutServiceInternal;
 import android.content.pm.ShortcutServiceInternal.ShortcutChangeListener;
+import android.content.pm.UserInfo;
 import android.content.res.Resources;
 import android.content.res.XmlResourceParser;
 import android.graphics.Bitmap;
@@ -190,6 +194,8 @@
     private static final String KEY_LOW_RAM = "lowRam";
     private static final String KEY_ICON_SIZE = "iconSize";
 
+    private static final String DUMMY_MAIN_ACTIVITY = "android.__dummy__";
+
     @VisibleForTesting
     interface ConfigConstants {
         /**
@@ -298,6 +304,8 @@
     private final UsageStatsManagerInternal mUsageStatsManagerInternal;
     private final ActivityManagerInternal mActivityManagerInternal;
 
+    private final ShortcutRequestPinProcessor mShortcutRequestPinProcessor;
+
     @GuardedBy("mLock")
     final SparseIntArray mUidState = new SparseIntArray();
 
@@ -336,8 +344,9 @@
         int IS_ACTIVITY_ENABLED = 13;
         int PACKAGE_UPDATE_CHECK = 14;
         int ASYNC_PRELOAD_USER_DELAY = 15;
+        int GET_DEFAULT_LAUNCHER = 16;
 
-        int COUNT = ASYNC_PRELOAD_USER_DELAY + 1;
+        int COUNT = GET_DEFAULT_LAUNCHER + 1;
     }
 
     private static final String[] STAT_LABELS = {
@@ -356,7 +365,8 @@
             "checkLauncherActivity",
             "isActivityEnabled",
             "packageUpdateCheck",
-            "asyncPreloadUserDelay"
+            "asyncPreloadUserDelay",
+            "getDefaultLauncher()"
     };
 
     final Object mStatLock = new Object();
@@ -417,6 +427,8 @@
         mActivityManagerInternal = Preconditions.checkNotNull(
                 LocalServices.getService(ActivityManagerInternal.class));
 
+        mShortcutRequestPinProcessor = new ShortcutRequestPinProcessor(this, mLock);
+
         if (onlyForPackageManagerApis) {
             return; // Don't do anything further.  For unit tests only.
         }
@@ -1591,12 +1603,11 @@
      * - Make sure the intent's extras are persistable, and them to set
      * {@link ShortcutInfo#mIntentPersistableExtrases}.  Also clear its extras.
      * - Clear flags.
-     *
-     * TODO Detailed unit tests
      */
-    private void fixUpIncomingShortcutInfo(@NonNull ShortcutInfo shortcut, boolean forUpdate) {
+    private void fixUpIncomingShortcutInfo(@NonNull ShortcutInfo shortcut, boolean forUpdate,
+            boolean forPinRequest) {
         Preconditions.checkNotNull(shortcut, "Null shortcut detected");
-        if (shortcut.getActivity() != null) {
+        if (!forPinRequest && shortcut.getActivity() != null) {
             Preconditions.checkState(
                     shortcut.getPackage().equals(shortcut.getActivity().getPackageName()),
                     "Cannot publish shortcut: activity " + shortcut.getActivity() + " does not"
@@ -1608,10 +1619,13 @@
         }
 
         if (!forUpdate) {
-            shortcut.enforceMandatoryFields(/* forPinned= */ false);
-            Preconditions.checkArgument(
-                    injectIsMainActivity(shortcut.getActivity(), shortcut.getUserId()),
-                    "Cannot publish shortcut: " + shortcut.getActivity() + " is not main activity");
+            shortcut.enforceMandatoryFields(/* forPinned= */ forPinRequest);
+            if (!forPinRequest) {
+                Preconditions.checkArgument(
+                        injectIsMainActivity(shortcut.getActivity(), shortcut.getUserId()),
+                        "Cannot publish shortcut: " + shortcut.getActivity()
+                                + " is not main activity");
+            }
         }
         if (shortcut.getIcon() != null) {
             ShortcutInfo.validateIcon(shortcut.getIcon());
@@ -1620,11 +1634,18 @@
         shortcut.replaceFlags(0);
     }
 
+    private void fixUpIncomingShortcutInfo(@NonNull ShortcutInfo shortcut, boolean forUpdate) {
+        fixUpIncomingShortcutInfo(shortcut, forUpdate, /*forPinRequest=*/ false);
+    }
+
+    public void validateShortcutForPinRequest(@NonNull ShortcutInfo shortcut) {
+        fixUpIncomingShortcutInfo(shortcut, /* forUpdate= */ false, /*forPinRequest=*/ true);
+    }
+
     /**
      * When a shortcut has no target activity, set the default one from the package.
      */
     private void fillInDefaultActivity(List<ShortcutInfo> shortcuts) {
-
         ComponentName defaultActivity = null;
         for (int i = shortcuts.size() - 1; i >= 0; i--) {
             final ShortcutInfo si = shortcuts.get(i);
@@ -1834,6 +1855,30 @@
     }
 
     @Override
+    public boolean requestPinShortcut(String packageName, ShortcutInfo shortcut,
+            IntentSender resultIntent, int userId) {
+        verifyCaller(packageName, userId);
+        Preconditions.checkNotNull(shortcut);
+        Preconditions.checkArgument(shortcut.isEnabled(), "Shortcut must be enabled");
+
+        final boolean ret;
+        synchronized (mLock) {
+            throwIfUserLockedL(userId);
+
+            // TODO Make sure the caller is in the foreground.
+
+            // TODO Cancel all pending request from the same app.
+
+            // Send request to the launcher, if supported.
+            ret = mShortcutRequestPinProcessor.requestPinShortcutLocked(shortcut, resultIntent);
+        }
+
+        verifyStates();
+
+        return ret;
+    }
+
+    @Override
     public void disableShortcuts(String packageName, List shortcutIds,
             CharSequence disabledMessage, int disabledMessageResId, @UserIdInt int userId) {
         verifyCaller(packageName, userId);
@@ -2049,6 +2094,16 @@
         }
     }
 
+    @Override
+    public boolean isRequestPinShortcutSupported(int callingUserId) {
+        final long token = injectClearCallingIdentity();
+        try {
+            return mShortcutRequestPinProcessor.isRequestPinnedShortcutSupported(callingUserId);
+        } finally {
+            injectRestoreCallingIdentity(token);
+        }
+    }
+
     /**
      * Reset all throttling, for developer options and command line.  Only system/shell can call
      * it.
@@ -2113,77 +2168,22 @@
     // This method is extracted so we can directly call this method from unit tests,
     // even when hasShortcutPermission() is overridden.
     @VisibleForTesting
-    boolean hasShortcutHostPermissionInner(@NonNull String callingPackage, int userId) {
+    boolean hasShortcutHostPermissionInner(@NonNull String packageName, int userId) {
         synchronized (mLock) {
             throwIfUserLockedL(userId);
 
             final ShortcutUser user = getUserShortcutsLocked(userId);
 
-            // Always trust the in-memory cache.
+            // Always trust the cached component.
             final ComponentName cached = user.getCachedLauncher();
             if (cached != null) {
-                if (cached.getPackageName().equals(callingPackage)) {
+                if (cached.getPackageName().equals(packageName)) {
                     return true;
                 }
             }
             // If the cached one doesn't match, then go ahead
 
-            final List<ResolveInfo> allHomeCandidates = new ArrayList<>();
-
-            // Default launcher from package manager.
-            final long startGetHomeActivitiesAsUser = injectElapsedRealtime();
-            final ComponentName defaultLauncher = mPackageManagerInternal
-                    .getHomeActivitiesAsUser(allHomeCandidates, userId);
-            logDurationStat(Stats.GET_DEFAULT_HOME, startGetHomeActivitiesAsUser);
-
-            ComponentName detected;
-            if (defaultLauncher != null) {
-                detected = defaultLauncher;
-                if (DEBUG) {
-                    Slog.v(TAG, "Default launcher from PM: " + detected);
-                }
-            } else {
-                detected = user.getLastKnownLauncher();
-
-                if (detected != null) {
-                    if (injectIsActivityEnabledAndExported(detected, userId)) {
-                        if (DEBUG) {
-                            Slog.v(TAG, "Cached launcher: " + detected);
-                        }
-                    } else {
-                        Slog.w(TAG, "Cached launcher " + detected + " no longer exists");
-                        detected = null;
-                        user.clearLauncher();
-                    }
-                }
-            }
-
-            if (detected == null) {
-                // If we reach here, that means it's the first check since the user was created,
-                // and there's already multiple launchers and there's no default set.
-                // Find the system one with the highest priority.
-                // (We need to check the priority too because of FallbackHome in Settings.)
-                // If there's no system launcher yet, then no one can access shortcuts, until
-                // the user explicitly
-                final int size = allHomeCandidates.size();
-
-                int lastPriority = Integer.MIN_VALUE;
-                for (int i = 0; i < size; i++) {
-                    final ResolveInfo ri = allHomeCandidates.get(i);
-                    if (!ri.activityInfo.applicationInfo.isSystemApp()) {
-                        continue;
-                    }
-                    if (DEBUG) {
-                        Slog.d(TAG, String.format("hasShortcutPermissionInner: pkg=%s prio=%d",
-                                ri.activityInfo.getComponentName(), ri.priority));
-                    }
-                    if (ri.priority < lastPriority) {
-                        continue;
-                    }
-                    detected = ri.activityInfo.getComponentName();
-                    lastPriority = ri.priority;
-                }
-            }
+            final ComponentName detected = getDefaultLauncher(userId);
 
             // Update the cache.
             user.setLauncher(detected);
@@ -2191,7 +2191,7 @@
                 if (DEBUG) {
                     Slog.v(TAG, "Detected launcher: " + detected);
                 }
-                return detected.getPackageName().equals(callingPackage);
+                return detected.getPackageName().equals(packageName);
             } else {
                 // Default launcher not found.
                 return false;
@@ -2199,6 +2199,80 @@
         }
     }
 
+    @Nullable
+    ComponentName getDefaultLauncher(@UserIdInt int userId) {
+        final long start = injectElapsedRealtime();
+        final long token = injectClearCallingIdentity();
+        try {
+            synchronized (mLock) {
+                throwIfUserLockedL(userId);
+
+                final ShortcutUser user = getUserShortcutsLocked(userId);
+
+                final List<ResolveInfo> allHomeCandidates = new ArrayList<>();
+
+                // Default launcher from package manager.
+                final long startGetHomeActivitiesAsUser = injectElapsedRealtime();
+                final ComponentName defaultLauncher = mPackageManagerInternal
+                        .getHomeActivitiesAsUser(allHomeCandidates, userId);
+                logDurationStat(Stats.GET_DEFAULT_HOME, startGetHomeActivitiesAsUser);
+
+                ComponentName detected = null;
+                if (defaultLauncher != null) {
+                    detected = defaultLauncher;
+                    if (DEBUG) {
+                        Slog.v(TAG, "Default launcher from PM: " + detected);
+                    }
+                } else {
+                    detected = user.getLastKnownLauncher();
+
+                    if (detected != null) {
+                        if (injectIsActivityEnabledAndExported(detected, userId)) {
+                            if (DEBUG) {
+                                Slog.v(TAG, "Cached launcher: " + detected);
+                            }
+                        } else {
+                            Slog.w(TAG, "Cached launcher " + detected + " no longer exists");
+                            detected = null;
+                            user.clearLauncher();
+                        }
+                    }
+                }
+
+                if (detected == null) {
+                    // If we reach here, that means it's the first check since the user was created,
+                    // and there's already multiple launchers and there's no default set.
+                    // Find the system one with the highest priority.
+                    // (We need to check the priority too because of FallbackHome in Settings.)
+                    // If there's no system launcher yet, then no one can access shortcuts, until
+                    // the user explicitly
+                    final int size = allHomeCandidates.size();
+
+                    int lastPriority = Integer.MIN_VALUE;
+                    for (int i = 0; i < size; i++) {
+                        final ResolveInfo ri = allHomeCandidates.get(i);
+                        if (!ri.activityInfo.applicationInfo.isSystemApp()) {
+                            continue;
+                        }
+                        if (DEBUG) {
+                            Slog.d(TAG, String.format("hasShortcutPermissionInner: pkg=%s prio=%d",
+                                    ri.activityInfo.getComponentName(), ri.priority));
+                        }
+                        if (ri.priority < lastPriority) {
+                            continue;
+                        }
+                        detected = ri.activityInfo.getComponentName();
+                        lastPriority = ri.priority;
+                    }
+                }
+                return detected;
+            }
+        } finally {
+            injectRestoreCallingIdentity(token);
+            logDurationStat(Stats.GET_DEFAULT_LAUNCHER, start);
+        }
+    }
+
     // === House keeping ===
 
     private void cleanUpPackageForAllLoadedUsers(String packageName, @UserIdInt int packageUserId,
@@ -3034,10 +3108,21 @@
         if (activity != null) {
             baseIntent.setComponent(activity);
         }
+        return queryActivities(baseIntent, userId, /* exportedOnly =*/ true);
+    }
 
-        final List<ResolveInfo> resolved =
-                mContext.getPackageManager().queryIntentActivitiesAsUser(
-                        baseIntent, PACKAGE_MATCH_FLAGS, userId);
+    @NonNull
+    List<ResolveInfo> queryActivities(@NonNull Intent intent, int userId,
+            boolean exportedOnly) {
+        final List<ResolveInfo> resolved;
+        final long token = injectClearCallingIdentity();
+        try {
+            resolved =
+                    mContext.getPackageManager().queryIntentActivitiesAsUser(
+                            intent, PACKAGE_MATCH_FLAGS, userId);
+        } finally {
+            injectRestoreCallingIdentity(token);
+        }
         if (resolved == null || resolved.size() == 0) {
             return EMPTY_RESOLVE_INFO;
         }
@@ -3045,7 +3130,9 @@
         if (!isInstalled(resolved.get(0).activityInfo)) {
             return EMPTY_RESOLVE_INFO;
         }
-        resolved.removeIf(ACTIVITY_NOT_EXPORTED);
+        if (exportedOnly) {
+            resolved.removeIf(ACTIVITY_NOT_EXPORTED);
+        }
         return resolved;
     }
 
@@ -3056,14 +3143,11 @@
     @Nullable
     ComponentName injectGetDefaultMainActivity(@NonNull String packageName, int userId) {
         final long start = injectElapsedRealtime();
-        final long token = injectClearCallingIdentity();
         try {
             final List<ResolveInfo> resolved =
                     queryActivities(getMainActivityIntent(), packageName, null, userId);
             return resolved.size() == 0 ? null : resolved.get(0).activityInfo.getComponentName();
         } finally {
-            injectRestoreCallingIdentity(token);
-
             logDurationStat(Stats.GET_LAUNCHER_ACTIVITY, start);
         }
     }
@@ -3073,31 +3157,36 @@
      */
     boolean injectIsMainActivity(@NonNull ComponentName activity, int userId) {
         final long start = injectElapsedRealtime();
-        final long token = injectClearCallingIdentity();
         try {
-            final List<ResolveInfo> resolved =
-                    queryActivities(getMainActivityIntent(), activity.getPackageName(),
-                            activity, userId);
+            if (DUMMY_MAIN_ACTIVITY.equals(activity.getClassName())) {
+                return true;
+            }
+            final List<ResolveInfo> resolved = queryActivities(
+                    getMainActivityIntent(), activity.getPackageName(), activity, userId);
             return resolved.size() > 0;
         } finally {
-            injectRestoreCallingIdentity(token);
-
             logDurationStat(Stats.CHECK_LAUNCHER_ACTIVITY, start);
         }
     }
 
     /**
+     * Create a dummy "main activity" component name which is used to create a dynamic shortcut
+     * with no main activity temporarily.
+     */
+    @NonNull
+    ComponentName getDummyMainActivity(@NonNull String packageName) {
+        return new ComponentName(packageName, DUMMY_MAIN_ACTIVITY);
+    }
+
+    /**
      * Return all the enabled, exported and main activities from a package.
      */
     @NonNull
     List<ResolveInfo> injectGetMainActivities(@NonNull String packageName, int userId) {
         final long start = injectElapsedRealtime();
-        final long token = injectClearCallingIdentity();
         try {
             return queryActivities(getMainActivityIntent(), packageName, null, userId);
         } finally {
-            injectRestoreCallingIdentity(token);
-
             logDurationStat(Stats.CHECK_LAUNCHER_ACTIVITY, start);
         }
     }
@@ -3109,17 +3198,33 @@
     boolean injectIsActivityEnabledAndExported(
             @NonNull ComponentName activity, @UserIdInt int userId) {
         final long start = injectElapsedRealtime();
-        final long token = injectClearCallingIdentity();
         try {
             return queryActivities(new Intent(), activity.getPackageName(), activity, userId)
                     .size() > 0;
         } finally {
-            injectRestoreCallingIdentity(token);
-
             logDurationStat(Stats.IS_ACTIVITY_ENABLED, start);
         }
     }
 
+    /**
+     * Get the {@link LauncherApps#ACTION_CONFIRM_PIN_ITEM} activity in a given package.
+     */
+    @Nullable
+    ComponentName injectGetPinConfirmationActivity(@NonNull String launcherPackageName,
+            int launcherUserId) {
+        Preconditions.checkNotNull(launcherPackageName);
+
+        final Intent confirmIntent = new Intent(LauncherApps.ACTION_CONFIRM_PIN_ITEM);
+        confirmIntent.setPackage(launcherPackageName);
+
+        final List<ResolveInfo> candidates = queryActivities(
+                confirmIntent, launcherUserId, /* exportedOnly =*/ false);
+        for (ResolveInfo ri : candidates) {
+            return ri.activityInfo.getComponentName();
+        }
+        return null;
+    }
+
     boolean injectIsSafeModeEnabled() {
         final long token = injectClearCallingIdentity();
         try {
@@ -3133,6 +3238,32 @@
         }
     }
 
+    /**
+     * If {@code userId} is of a managed profile, return the parent user ID.  Otherwise return
+     * itself.
+     */
+    int getParentOrSelfUserId(int userId) {
+        final long token = injectClearCallingIdentity();
+        try {
+            final UserInfo parent = mUserManager.getProfileParent(userId);
+            return (parent != null) ? parent.id : userId;
+        } finally {
+            injectRestoreCallingIdentity(token);
+        }
+    }
+
+    void injectSendIntentSender(IntentSender intentSender) {
+        if (intentSender == null) {
+            return;
+        }
+        try {
+            intentSender.sendIntent(mContext, /* code= */ 0, /* intent= */ null,
+                    /* onFinished=*/ null, /* handler= */ null);
+        } catch (SendIntentException e) {
+            Slog.w(TAG, "sendIntent failed().", e);
+        }
+    }
+
     // === Backup & restore ===
 
     boolean shouldBackupApp(String packageName, int userId) {
@@ -3749,6 +3880,11 @@
         }
     }
 
+    @VisibleForTesting
+    ShortcutRequestPinProcessor getShortcutRequestPinProcessorForTest() {
+        return mShortcutRequestPinProcessor;
+    }
+
     /**
      * Control whether {@link #verifyStates} should be performed.  We always perform it during unit
      * tests.
diff --git a/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java
index 99af9e8..cb27af1 100644
--- a/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java
@@ -46,6 +46,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.content.IntentSender;
 import android.content.pm.ActivityInfo;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.ILauncherApps;
@@ -74,6 +75,7 @@
 import android.os.UserManager;
 import android.test.InstrumentationTestCase;
 import android.test.mock.MockContext;
+import android.util.ArrayMap;
 import android.util.Log;
 import android.util.Pair;
 
@@ -123,6 +125,7 @@
     protected static final String[] EMPTY_STRINGS = new String[0]; // Just for readability.
 
     protected static final String MAIN_ACTIVITY_CLASS = "MainActivity";
+    protected static final String PIN_CONFIRM_ACTIVITY_CLASS = "PinConfirmActivity";
 
     // public for mockito
     public class BaseContext extends MockContext {
@@ -161,6 +164,11 @@
         public void unregisterReceiver(BroadcastReceiver receiver) {
             // ignore.
         }
+
+        @Override
+        public void startActivityAsUser(Intent intent, UserHandle user) {
+            // ignore, use spy to intercept it.
+        }
     }
 
     /** Context used in the client side */
@@ -201,6 +209,10 @@
         public XmlResourceParser injectXmlMetaData(ActivityInfo activityInfo, String key) {
             return BaseShortcutManagerTest.this.injectXmlMetaData(activityInfo, key);
         }
+
+        public void sendIntentSender(IntentSender intent) {
+            // Placeholder for spying.
+        }
     }
 
     /** ShortcutService with injection override methods. */
@@ -304,6 +316,15 @@
         }
 
         @Override
+        ComponentName getDefaultLauncher(@UserIdInt int userId) {
+            final ComponentName activity = mDefaultLauncher.get(userId);
+            if (activity != null) {
+                return activity;
+            }
+            return super.getDefaultLauncher(userId);
+        }
+
+        @Override
         PackageInfo injectPackageInfoWithUninstalled(String packageName, @UserIdInt int userId,
                 boolean getSignatures) {
             return getInjectedPackageInfo(packageName, userId, getSignatures);
@@ -375,6 +396,12 @@
         }
 
         @Override
+        ComponentName injectGetPinConfirmationActivity(@NonNull String launcherPackageName,
+                int launcherUserId) {
+            return mPinConfirmActivityFetcher.apply(launcherPackageName, launcherUserId);
+        }
+
+        @Override
         boolean injectIsActivityEnabledAndExported(ComponentName activity, @UserIdInt int userId) {
             assertNotNull(activity);
             return mEnabledActivityChecker.test(activity, userId);
@@ -413,6 +440,11 @@
         }
 
         @Override
+        void injectSendIntentSender(IntentSender intent) {
+            mContext.sendIntentSender(intent);
+        }
+
+        @Override
         void wtf(String message, Throwable th) {
             // During tests, WTF is fatal.
             fail(message + "  exception: " + th + "\n" + Log.getStackTraceString(th));
@@ -583,7 +615,7 @@
 
     protected static final UserInfo USER_INFO_0 = withProfileGroupId(
             new UserInfo(USER_0, "user0",
-                    UserInfo.FLAG_ADMIN | UserInfo.FLAG_PRIMARY | UserInfo.FLAG_INITIALIZED), 10);
+                    UserInfo.FLAG_ADMIN | UserInfo.FLAG_PRIMARY | UserInfo.FLAG_INITIALIZED), 0);
 
     protected static final UserInfo USER_INFO_10 =
             new UserInfo(USER_10, "user10", UserInfo.FLAG_INITIALIZED);
@@ -593,19 +625,24 @@
 
     protected static final UserInfo USER_INFO_P0 = withProfileGroupId(
             new UserInfo(USER_P0, "userP0",
-                    UserInfo.FLAG_MANAGED_PROFILE), 10);
+                    UserInfo.FLAG_MANAGED_PROFILE), 0);
 
     protected BiPredicate<String, Integer> mDefaultLauncherChecker =
             (callingPackage, userId) ->
             LAUNCHER_1.equals(callingPackage) || LAUNCHER_2.equals(callingPackage)
             || LAUNCHER_3.equals(callingPackage) || LAUNCHER_4.equals(callingPackage);
 
+    private final Map<Integer, ComponentName> mDefaultLauncher = new ArrayMap<>();
+
     protected BiPredicate<ComponentName, Integer> mMainActivityChecker =
             (activity, userId) -> true;
 
     protected BiFunction<String, Integer, ComponentName> mMainActivityFetcher =
             (packageName, userId) -> new ComponentName(packageName, MAIN_ACTIVITY_CLASS);
 
+    protected BiFunction<String, Integer, ComponentName> mPinConfirmActivityFetcher =
+            (packageName, userId) -> new ComponentName(packageName, PIN_CONFIRM_ACTIVITY_CLASS);
+
     protected BiPredicate<ComponentName, Integer> mEnabledActivityChecker
             = (activity, userId) -> true; // all activities are enabled.
 
@@ -722,6 +759,19 @@
                     return b(mRunningUsers.get(userId)) && b(mUnlockedUsers.get(userId));
                 }));
 
+        when(mMockUserManager.getProfileParent(anyInt()))
+                .thenAnswer(new AnswerWithSystemCheck<>(inv -> {
+                    final int userId = (Integer) inv.getArguments()[0];
+                    final UserInfo ui = mUserInfos.get(userId);
+                    assertNotNull(ui);
+                    if (ui.profileGroupId == UserInfo.NO_PROFILE_GROUP_ID) {
+                        return null;
+                    }
+                    final UserInfo parent = mUserInfos.get(ui.profileGroupId);
+                    assertNotNull(parent);
+                    return parent;
+                }));
+
         when(mMockActivityManagerInternal.getUidProcessState(anyInt())).thenReturn(
                 ActivityManager.PROCESS_STATE_CACHED_EMPTY);
 
@@ -1098,10 +1148,31 @@
         return mInjectedClientPackage;
     }
 
+    /**
+     * This controls {@link ShortcutService#hasShortcutHostPermission(String, int)}, but
+     * not {@link ShortcutService#getDefaultLauncher(int)}.  To control the later, use
+     * {@link #setDefaultLauncher(int, ComponentName)}.
+     */
     protected void setDefaultLauncherChecker(BiPredicate<String, Integer> p) {
         mDefaultLauncherChecker = p;
     }
 
+    /**
+     * Set the default launcher.  This will update {@link #mDefaultLauncherChecker} set by
+     * {@link #setDefaultLauncherChecker} too.
+     */
+    protected void setDefaultLauncher(int userId, ComponentName launcherActivity) {
+        mDefaultLauncher.put(userId, launcherActivity);
+
+        final BiPredicate<String, Integer> oldChecker = mDefaultLauncherChecker;
+        mDefaultLauncherChecker = (checkPackageName, checkUserId) -> {
+            if ((checkUserId == userId) && (launcherActivity !=  null)) {
+                return launcherActivity.getPackageName().equals(checkPackageName);
+            }
+            return oldChecker.test(checkPackageName, checkUserId);
+        };
+    }
+
     protected void runWithCaller(String packageName, int userId, Runnable r) {
         final String previousPackage = mInjectedClientPackage;
         final int previousUserId = UserHandle.getUserId(mInjectedCallingUid);
diff --git a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest6.java b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest6.java
index ba4dbc1..3684ca0 100644
--- a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest6.java
+++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest6.java
@@ -31,7 +31,8 @@
 import java.util.List;
 
 /**
- * Tests for {@link ShortcutService#hasShortcutHostPermissionInner}.
+ * Tests for {@link ShortcutService#hasShortcutHostPermissionInner}, which includes
+ * {@link ShortcutService#getDefaultLauncher}.
  */
 @SmallTest
 public class ShortcutManagerTest6 extends BaseShortcutManagerTest {
diff --git a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest8.java b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest8.java
new file mode 100644
index 0000000..de344c2
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest8.java
@@ -0,0 +1,291 @@
+/*
+ * Copyright (C) 2016 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.assertWith;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.annotation.Nullable;
+import android.app.PendingIntent;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.IntentSender;
+import android.content.pm.LauncherApps;
+import android.content.pm.LauncherApps.PinItemRequest;
+import android.content.pm.ShortcutInfo;
+import android.content.pm.ShortcutManager;
+import android.os.UserHandle;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.util.Pair;
+
+import org.mockito.ArgumentCaptor;
+
+/**
+ * Tests for {@link ShortcutManager#requestPinShortcut} 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.ShortcutManagerTest8 \
+ -w com.android.frameworks.servicestests/android.support.test.runner.AndroidJUnitRunner
+ */
+@SmallTest
+public class ShortcutManagerTest8 extends BaseShortcutManagerTest {
+    private ShortcutRequestPinProcessor mProcessor;
+
+    @Override
+    protected void initService() {
+        super.initService();
+        mProcessor = mService.getShortcutRequestPinProcessorForTest();
+    }
+
+    public void testGetParentOrSelfUserId() {
+        assertEquals(USER_0, mService.getParentOrSelfUserId(USER_0));
+        assertEquals(USER_10, mService.getParentOrSelfUserId(USER_10));
+        assertEquals(USER_11, mService.getParentOrSelfUserId(USER_11));
+        assertEquals(USER_0, mService.getParentOrSelfUserId(USER_P0));
+    }
+
+    public void testIsRequestPinShortcutSupported() {
+        setDefaultLauncher(USER_0, mMainActivityFetcher.apply(LAUNCHER_1, USER_0));
+        setDefaultLauncher(USER_10, mMainActivityFetcher.apply(LAUNCHER_2, USER_10));
+
+        Pair<ComponentName, Integer> actual;
+        // User 0
+        actual = mProcessor.getRequestPinShortcutConfirmationActivity(USER_0);
+
+        assertEquals(LAUNCHER_1, actual.first.getPackageName());
+        assertEquals(PIN_CONFIRM_ACTIVITY_CLASS, actual.first.getClassName());
+        assertEquals(USER_0, (int) actual.second);
+
+        // User 10
+        actual = mProcessor.getRequestPinShortcutConfirmationActivity(USER_10);
+
+        assertEquals(LAUNCHER_2, actual.first.getPackageName());
+        assertEquals(PIN_CONFIRM_ACTIVITY_CLASS, actual.first.getClassName());
+        assertEquals(USER_10, (int) actual.second);
+
+        // User P0 -> managed profile, return user-0's launcher.
+        actual = mProcessor.getRequestPinShortcutConfirmationActivity(USER_P0);
+
+        assertEquals(LAUNCHER_1, actual.first.getPackageName());
+        assertEquals(PIN_CONFIRM_ACTIVITY_CLASS, actual.first.getClassName());
+        assertEquals(USER_0, (int) actual.second);
+
+        // Check from the public API.
+        runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+            assertTrue(mManager.isRequestPinShortcutSupported());
+        });
+        runWithCaller(CALLING_PACKAGE_2, USER_0, () -> {
+            assertTrue(mManager.isRequestPinShortcutSupported());
+        });
+        runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
+            assertTrue(mManager.isRequestPinShortcutSupported());
+        });
+        runWithCaller(CALLING_PACKAGE_1, USER_P0, () -> {
+            assertTrue(mManager.isRequestPinShortcutSupported());
+        });
+
+        // Now, USER_0's launcher no longer has a confirm activity.
+        mPinConfirmActivityFetcher = (packageName, userId) ->
+                !LAUNCHER_2.equals(packageName)
+                        ? null : new ComponentName(packageName, PIN_CONFIRM_ACTIVITY_CLASS);
+
+        // User 10 -- still has confirm activity.
+        actual = mProcessor.getRequestPinShortcutConfirmationActivity(USER_10);
+
+        assertEquals(LAUNCHER_2, actual.first.getPackageName());
+        assertEquals(PIN_CONFIRM_ACTIVITY_CLASS, actual.first.getClassName());
+        assertEquals(USER_10, (int) actual.second);
+
+        // But user-0 and user p0 no longer has a confirmation activity.
+        assertNull(mProcessor.getRequestPinShortcutConfirmationActivity(USER_0));
+        assertNull(mProcessor.getRequestPinShortcutConfirmationActivity(USER_P0));
+
+        // Check from the public API.
+        runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+            assertFalse(mManager.isRequestPinShortcutSupported());
+        });
+        runWithCaller(CALLING_PACKAGE_2, USER_0, () -> {
+            assertFalse(mManager.isRequestPinShortcutSupported());
+        });
+        runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
+            assertTrue(mManager.isRequestPinShortcutSupported());
+        });
+        runWithCaller(CALLING_PACKAGE_1, USER_P0, () -> {
+            assertFalse(mManager.isRequestPinShortcutSupported());
+        });
+    }
+
+    public void testRequestPinShortcut_notSupported() {
+        // User-0's launcher has no confirmation activity.
+        setDefaultLauncher(USER_0, mMainActivityFetcher.apply(LAUNCHER_1, USER_0));
+
+        mPinConfirmActivityFetcher = (packageName, userId) ->
+                !LAUNCHER_2.equals(packageName)
+                        ? null : new ComponentName(packageName, PIN_CONFIRM_ACTIVITY_CLASS);
+
+        runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+            ShortcutInfo s1 = makeShortcut("s1");
+
+            assertFalse(mManager.requestPinShortcut(s1,
+                    /*PendingIntent=*/ null));
+
+            verify(mServiceContext, times(0))
+                    .startActivityAsUser(any(Intent.class), any(UserHandle.class));
+            verify(mServiceContext, times(0))
+                    .sendIntentSender(any(IntentSender.class));
+        });
+
+        runWithCaller(CALLING_PACKAGE_2, USER_0, () -> {
+            ShortcutInfo s1 = makeShortcut("s1");
+
+            assertFalse(mManager.requestPinShortcut(s1,
+                    /*PendingIntent=*/ null));
+
+            verify(mServiceContext, times(0))
+                    .startActivityAsUser(any(Intent.class), any(UserHandle.class));
+            verify(mServiceContext, times(0))
+                    .sendIntentSender(any(IntentSender.class));
+        });
+
+        runWithCaller(CALLING_PACKAGE_1, USER_P0, () -> {
+            ShortcutInfo s1 = makeShortcut("s1");
+
+            assertFalse(mManager.requestPinShortcut(s1,
+                    /*PendingIntent=*/ null));
+
+            verify(mServiceContext, times(0))
+                    .startActivityAsUser(any(Intent.class), any(UserHandle.class));
+            verify(mServiceContext, times(0))
+                    .sendIntentSender(any(IntentSender.class));
+        });
+    }
+
+    private void assertPinItemRequestIntent(Intent actualIntent, String expectedPackage) {
+        assertEquals(LauncherApps.ACTION_CONFIRM_PIN_ITEM, actualIntent.getAction());
+        assertEquals(expectedPackage, actualIntent.getComponent().getPackageName());
+        assertEquals(PIN_CONFIRM_ACTIVITY_CLASS,
+                actualIntent.getComponent().getClassName());
+        assertEquals(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK,
+                actualIntent.getFlags());
+    }
+
+    private void assertPinItemRequest(PinItemRequest actualRequest) {
+        assertNotNull(actualRequest);
+
+        assertEquals(PinItemRequest.REQUEST_TYPE_SHORTCUT, actualRequest.getRequestType());
+    }
+
+    /**
+     * Basic flow:
+     * - Launcher supports the feature.
+     * - Shortcut doesn't pre-exist.
+     */
+    private void checkRequestPinShortcut(@Nullable PendingIntent resultIntent) {
+        setDefaultLauncher(USER_0, mMainActivityFetcher.apply(LAUNCHER_1, USER_0));
+        setDefaultLauncher(USER_10, mMainActivityFetcher.apply(LAUNCHER_2, USER_10));
+
+        runWithCaller(CALLING_PACKAGE_1, USER_P0, () -> {
+            ShortcutInfo s1 = makeShortcut("s1");
+
+            assertTrue(mManager.requestPinShortcut(s1,
+                    resultIntent == null ? null : resultIntent.getIntentSender()));
+
+            verify(mServiceContext, times(0))
+                    .sendIntentSender(any(IntentSender.class));
+
+            // Shortcut shouldn't be registered yet.
+            assertWith(getCallerShortcuts())
+                    .isEmpty();
+        });
+
+        runWithCaller(LAUNCHER_1, USER_0, () -> {
+            // Check the intent passed to startActivityAsUser().
+            final ArgumentCaptor<Intent> intent = ArgumentCaptor.forClass(Intent.class);
+
+            verify(mServiceContext).startActivityAsUser(intent.capture(), eq(HANDLE_USER_0));
+
+            assertPinItemRequestIntent(intent.getValue(), mInjectedClientPackage);
+
+            // Check the request object.
+            final PinItemRequest request = mLauncherApps.getPinItemRequest(intent.getValue());
+
+            assertPinItemRequest(request);
+
+            assertWith(request.getShortcutInfo())
+                    .haveIds("s1")
+                    .areAllOrphan();
+
+            // Can't test icons; need to test on CTS.
+
+            // Accept the request.
+            request.accept();
+        });
+
+        // Check from the launcher side, including callback
+
+        // This method is always called, even with PI == null.
+        if (resultIntent == null) {
+            verify(mServiceContext, times(1)).sendIntentSender(eq(null));
+        } else {
+            verify(mServiceContext, times(1)).sendIntentSender(any(IntentSender.class));
+        }
+
+        runWithCaller(CALLING_PACKAGE_1, USER_P0, () -> {
+            assertWith(getCallerShortcuts())
+                    .haveIds("s1")
+                    .areAllNotDynamic()
+                    .areAllEnabled()
+                    .areAllPinned();
+        });
+    }
+
+    public void testRequestPinShortcut() {
+        checkRequestPinShortcut(/* resultIntent=*/ null);
+    }
+
+    public void testRequestPinShortcut_withCallback() {
+        final PendingIntent resultIntent =
+                PendingIntent.getActivity(getTestContext(), 0, new Intent(), 0);
+
+        checkRequestPinShortcut(resultIntent);
+    }
+
+    // TODO More tests:
+    // Shortcut exists as a dynamic shortcut.
+    // Shortcut exists as a manifest shortcut.
+    // Shortcut exists as a dynamic, already pinned by this launcher
+    // Shortcut exists as a manifest, already pinned by this launcher
+    // Shortcut exists as floating, already pinned by this launcher
+
+    // Shortcut exists as a dynamic, already pinned by another launcher
+    // Shortcut exists as a manifest, already pinned by another launcher
+    // Shortcut exists as floating, already pinned by another launcher
+
+    // Shortcut exists but disabled (both mutable and immutable)
+
+    // Shortcut exists but removed before accept().
+    // Shortcut exists but disabled before accept().
+    // Shortcut exists but pinned before accept().
+    // Shortcut exists but unpinned before accept().
+
+    // Cancel previous pending request and release memory?
+}
diff --git a/services/tests/shortcutmanagerutils/src/com/android/server/pm/shortcutmanagertest/ShortcutManagerTestUtils.java b/services/tests/shortcutmanagerutils/src/com/android/server/pm/shortcutmanagertest/ShortcutManagerTestUtils.java
index 6e74deb..8ecea71 100644
--- a/services/tests/shortcutmanagerutils/src/com/android/server/pm/shortcutmanagertest/ShortcutManagerTestUtils.java
+++ b/services/tests/shortcutmanagerutils/src/com/android/server/pm/shortcutmanagertest/ShortcutManagerTestUtils.java
@@ -729,6 +729,10 @@
         return new ShortcutListAsserter(list);
     }
 
+    public static ShortcutListAsserter assertWith(ShortcutInfo... list) {
+        return assertWith(list(list));
+    }
+
     /**
      * New style assertion that allows chained calls.
      */
@@ -886,6 +890,30 @@
             return this;
         }
 
+        public ShortcutListAsserter areAllFloating() {
+            forAllShortcuts(s -> assertTrue("id=" + s.getId(),
+                    s.isPinned() && !s.isDeclaredInManifest() && !s.isDynamic()));
+            return this;
+        }
+
+        public ShortcutListAsserter areAllNotFloating() {
+            forAllShortcuts(s -> assertTrue("id=" + s.getId(),
+                    !(s.isPinned() && !s.isDeclaredInManifest() && !s.isDynamic())));
+            return this;
+        }
+
+        public ShortcutListAsserter areAllOrphan() {
+            forAllShortcuts(s -> assertTrue("id=" + s.getId(),
+                    !s.isPinned() && !s.isDeclaredInManifest() && !s.isDynamic()));
+            return this;
+        }
+
+        public ShortcutListAsserter areAllNotOrphan() {
+            forAllShortcuts(s -> assertTrue("id=" + s.getId(),
+                    s.isPinned() || s.isDeclaredInManifest() || s.isDynamic()));
+            return this;
+        }
+
         public ShortcutListAsserter areAllWithKeyFieldsOnly() {
             forAllShortcuts(s -> assertTrue("id=" + s.getId(), s.hasKeyFieldsOnly()));
             return this;