Shortcut: Only "main" activities can have shortcuts.

- Don't publish shortcuts when their target activities are not main.
- Only scan manifest shortcuts for main activities.
- When an app is updated, remove shortcuts that no longer belong to
valid main activities.

- Also re-publish manifest shortcuts after 'clear data'
- Also listen to PACKAGE_CHANGED and disable/re-publish shortcuts
properly.

Bug 29355786
Bug 29582255
Bug 29601844

Change-Id: I6c701ce669cf30a227bc2af4aa01de467ef73e3a
diff --git a/core/java/android/content/pm/ShortcutInfo.java b/core/java/android/content/pm/ShortcutInfo.java
index a25ee3c..35370f0 100644
--- a/core/java/android/content/pm/ShortcutInfo.java
+++ b/core/java/android/content/pm/ShortcutInfo.java
@@ -709,7 +709,7 @@
         @NonNull
         @Deprecated
         public Builder setId(@NonNull String id) {
-            mId = Preconditions.checkStringNotEmpty(id, "id");
+            mId = Preconditions.checkStringNotEmpty(id, "id cannot be empty");
             return this;
         }
 
@@ -721,14 +721,17 @@
          */
         public Builder(Context context, String id) {
             mContext = context;
-            mId = Preconditions.checkStringNotEmpty(id, "id");
+            mId = Preconditions.checkStringNotEmpty(id, "id cannot be empty");
         }
 
         /**
          * Sets the target activity. A shortcut will be shown with this activity on the launcher.
          *
-         * <p>This is a mandatory field, unless it's passed to
-         * {@link ShortcutManager#updateShortcuts(List)}.
+         * <p>Only "main" activities -- i.e. ones with an intent filter for
+         * {@link Intent#ACTION_MAIN} and {@link Intent#CATEGORY_LAUNCHER} can be target activities.
+         *
+         * <p>By default, the first main activity defined in the application manifest will be
+         * the target.
          *
          * <p>The package name of the target activity must match the package name of the shortcut
          * publisher.
@@ -738,7 +741,7 @@
          */
         @NonNull
         public Builder setActivity(@NonNull ComponentName activity) {
-            mActivity = Preconditions.checkNotNull(activity, "activity");
+            mActivity = Preconditions.checkNotNull(activity, "activity cannot be null");
             return this;
         }
 
@@ -785,7 +788,7 @@
         @NonNull
         public Builder setShortLabel(@NonNull CharSequence shortLabel) {
             Preconditions.checkState(mTitleResId == 0, "shortLabelResId already set");
-            mTitle = Preconditions.checkStringNotEmpty(shortLabel, "shortLabel");
+            mTitle = Preconditions.checkStringNotEmpty(shortLabel, "shortLabel cannot be empty");
             return this;
         }
 
@@ -810,7 +813,7 @@
         @NonNull
         public Builder setLongLabel(@NonNull CharSequence longLabel) {
             Preconditions.checkState(mTextResId == 0, "longLabelResId already set");
-            mText = Preconditions.checkStringNotEmpty(longLabel, "longLabel");
+            mText = Preconditions.checkStringNotEmpty(longLabel, "longLabel cannot be empty");
             return this;
         }
 
@@ -854,7 +857,8 @@
             Preconditions.checkState(
                     mDisabledMessageResId == 0, "disabledMessageResId already set");
             mDisabledMessage =
-                    Preconditions.checkStringNotEmpty(disabledMessage, "disabledMessage");
+                    Preconditions.checkStringNotEmpty(disabledMessage,
+                            "disabledMessage cannot be empty");
             return this;
         }
 
@@ -876,8 +880,8 @@
          */
         @NonNull
         public Builder setIntent(@NonNull Intent intent) {
-            mIntent = Preconditions.checkNotNull(intent, "intent");
-            Preconditions.checkNotNull(mIntent.getAction(), "Intent action must be set");
+            mIntent = Preconditions.checkNotNull(intent, "intent cannot be null");
+            Preconditions.checkNotNull(mIntent.getAction(), "intent's action must be set");
             return this;
         }
 
@@ -944,6 +948,11 @@
         return mActivity;
     }
 
+    /** @hide */
+    public void setActivity(ComponentName activity) {
+        mActivity = activity;
+    }
+
     /**
      * Icon.
      *
diff --git a/services/core/java/com/android/server/pm/ShortcutPackage.java b/services/core/java/com/android/server/pm/ShortcutPackage.java
index d637586..67e4e93 100644
--- a/services/core/java/com/android/server/pm/ShortcutPackage.java
+++ b/services/core/java/com/android/server/pm/ShortcutPackage.java
@@ -54,8 +54,6 @@
  * User information used by {@link ShortcutService}.
  *
  * All methods should be guarded by {@code #mShortcutUser.mService.mLock}.
- *
- * TODO Max dynamic shortcuts cap should be per activity.
  */
 class ShortcutPackage extends ShortcutPackageItem {
     private static final String TAG = ShortcutService.TAG;
@@ -321,9 +319,27 @@
     /**
      * Remove a dynamic shortcut by ID.  It'll be removed from the dynamic set, but if the shortcut
      * is pinned, it'll remain as a pinned shortcut, and is still enabled.
+     *
+     * @return true if it's actually removed because it wasn't pinned, or false if it's still
+     * pinned.
      */
-    public void deleteDynamicWithId(@NonNull String shortcutId) {
-        deleteOrDisableWithId(shortcutId, /* disable =*/ false, /* overrideImmutable=*/ false);
+    public boolean deleteDynamicWithId(@NonNull String shortcutId) {
+        final ShortcutInfo removed = deleteOrDisableWithId(
+                shortcutId, /* disable =*/ false, /* overrideImmutable=*/ false);
+        return removed == null;
+    }
+
+    /**
+     * Disable a dynamic shortcut by ID.  It'll be removed from the dynamic set, but if the shortcut
+     * is pinned, it'll remain as a pinned shortcut, but will be disabled.
+     *
+     * @return true if it's actually removed because it wasn't pinned, or false if it's still
+     * pinned.
+     */
+    private boolean disableDynamicWithId(@NonNull String shortcutId) {
+        final ShortcutInfo disabled = deleteOrDisableWithId(
+                shortcutId, /* disable =*/ true, /* overrideImmutable=*/ false);
+        return disabled == null;
     }
 
     /**
@@ -599,14 +615,14 @@
      *
      * @return TRUE if any shortcuts have been changed.
      */
-    public boolean handlePackageAddedOrUpdated(boolean isNewApp) {
+    public boolean handlePackageAddedOrUpdated(boolean isNewApp, boolean forceRescan) {
         final PackageInfo pi = mShortcutUser.mService.getPackageInfo(
                 getPackageName(), getPackageUserId());
         if (pi == null) {
             return false; // Shouldn't happen.
         }
 
-        if (!isNewApp) {
+        if (!isNewApp && !forceRescan) {
             // Make sure the version code or last update time has changed.
             // Otherwise, nothing to do.
             if (getPackageInfo().getVersionCode() >= pi.versionCode
@@ -649,12 +665,26 @@
         boolean changed = false;
 
         // For existing shortcuts, update timestamps if they have any resources.
+        // Also check if shortcuts' activities are still main activities.  Otherwise, disable them.
         if (!isNewApp) {
             Resources publisherRes = null;
 
             for (int i = mShortcuts.size() - 1; i >= 0; i--) {
                 final ShortcutInfo si = mShortcuts.valueAt(i);
 
+                if (si.isDynamic()) {
+                    if (!s.injectIsMainActivity(si.getActivity(), getPackageUserId())) {
+                        Slog.w(TAG, String.format(
+                                "%s is no longer main activity. Disabling shorcut %s.",
+                                getPackageName(), si.getId()));
+                        if (disableDynamicWithId(si.getId())) {
+                            continue; // Actually removed.
+                        }
+                        // Still pinned, so fall-through and possibly update the resources.
+                    }
+                    changed = true;
+                }
+
                 if (si.hasAnyResources()) {
                     if (!si.isOriginallyFromManifest()) {
                         if (publisherRes == null) {
@@ -912,9 +942,8 @@
             final ComponentName newActivity = newShortcut.getActivity();
             if (newActivity == null) {
                 if (operation != ShortcutService.OPERATION_UPDATE) {
-                    // This method may be called before validating shortcuts, so this may happen,
-                    // and is a caller side error.
-                    throw new NullPointerException("Activity must be provided");
+                    service.wtf("Activity must not be null at this point");
+                    continue; // Just ignore this invalid case.
                 }
                 continue; // Activity can be null for update.
             }
diff --git a/services/core/java/com/android/server/pm/ShortcutParser.java b/services/core/java/com/android/server/pm/ShortcutParser.java
index c349b75..858e1cd 100644
--- a/services/core/java/com/android/server/pm/ShortcutParser.java
+++ b/services/core/java/com/android/server/pm/ShortcutParser.java
@@ -21,6 +21,7 @@
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
 import android.content.pm.PackageInfo;
+import android.content.pm.ResolveInfo;
 import android.content.pm.ShortcutInfo;
 import android.content.res.TypedArray;
 import android.content.res.XmlResourceParser;
@@ -58,18 +59,36 @@
     @Nullable
     public static List<ShortcutInfo> parseShortcuts(ShortcutService service,
             String packageName, @UserIdInt int userId) throws IOException, XmlPullParserException {
-        final PackageInfo pi = service.injectGetActivitiesWithMetadata(packageName, userId);
+        if (ShortcutService.DEBUG) {
+            Slog.d(TAG, String.format("Scanning package %s for manifest shortcuts on user %d",
+                    packageName, userId));
+        }
+        final List<ResolveInfo> activities = service.injectGetMainActivities(packageName, userId);
+        if (activities == null || activities.size() == 0) {
+            return null;
+        }
 
         List<ShortcutInfo> result = null;
 
         try {
-            if (pi != null && pi.activities != null) {
-                for (ActivityInfo activityInfo : pi.activities) {
-                    result = parseShortcutsOneFile(service, activityInfo, packageName, userId, result);
+            final int size = activities.size();
+            for (int i = 0; i < size; i++) {
+                final ActivityInfo activityInfoNoMetadata = activities.get(i).activityInfo;
+                if (activityInfoNoMetadata == null) {
+                    continue;
+                }
+
+                final ActivityInfo activityInfoWithMetadata =
+                        service.injectGetActivityInfoWithMetadata(
+                        activityInfoNoMetadata.getComponentName(), userId);
+                if (activityInfoWithMetadata != null) {
+                    result = parseShortcutsOneFile(
+                            service, activityInfoWithMetadata, packageName, userId, result);
                 }
             }
         } catch (RuntimeException e) {
-            // Resource ID mismatch may cause various runtime exceptions when parsing XMLs.
+            // Resource ID mismatch may cause various runtime exceptions when parsing XMLs,
+            // But we don't crash the device, so just swallow them.
             service.wtf(
                     "Exception caught while parsing shortcut XML for package=" + packageName, e);
             return null;
@@ -81,6 +100,11 @@
             ShortcutService service,
             ActivityInfo activityInfo, String packageName, @UserIdInt int userId,
             List<ShortcutInfo> result) throws IOException, XmlPullParserException {
+        if (ShortcutService.DEBUG) {
+            Slog.d(TAG, String.format(
+                    "Checking main activity %s", activityInfo.getComponentName()));
+        }
+
         XmlResourceParser parser = null;
         try {
             parser = service.injectXmlMetaData(activityInfo, METADATA_KEY);
@@ -223,7 +247,7 @@
                     continue;
                 }
 
-                Log.w(TAG, "Unknown tag " + tag + " at depth " + depth);
+                ShortcutService.warnForInvalidTag(depth, tag);
             }
         } finally {
             if (parser != null) {
diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java
index 9f40772..1db1ce7 100644
--- a/services/core/java/com/android/server/pm/ShortcutService.java
+++ b/services/core/java/com/android/server/pm/ShortcutService.java
@@ -60,6 +60,7 @@
 import android.os.RemoteException;
 import android.os.ResultReceiver;
 import android.os.SELinux;
+import android.os.ServiceManager;
 import android.os.ShellCommand;
 import android.os.SystemClock;
 import android.os.UserHandle;
@@ -76,6 +77,7 @@
 import android.util.SparseLongArray;
 import android.util.TypedValue;
 import android.util.Xml;
+import android.view.IWindowManager;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
@@ -129,7 +131,7 @@
  * - Default launcher check does take a few ms.  Worth caching.
  *
  * - Detect when already registered instances are passed to APIs again, which might break
- *   internal bitmap handling.
+ * internal bitmap handling.
  *
  * - Add more call stats.
  */
@@ -181,6 +183,8 @@
 
     private static final String ATTR_VALUE = "value";
 
+    private static final String LAUNCHER_INTENT_CATEGORY = Intent.CATEGORY_LAUNCHER;
+
     @VisibleForTesting
     interface ConfigConstants {
         /**
@@ -282,7 +286,8 @@
     private List<Integer> mDirtyUserIds = new ArrayList<>();
 
     /**
-     * A counter that increments every time the system locale changes.  We keep track of it to reset
+     * A counter that increments every time the system locale changes.  We keep track of it to
+     * reset
      * throttling counters on the first call from each package after the last locale change.
      *
      * We need this mechanism because we can't do much in the locale change callback, which is
@@ -294,8 +299,8 @@
 
     private static final int PACKAGE_MATCH_FLAGS =
             PackageManager.MATCH_DIRECT_BOOT_AWARE
-            | PackageManager.MATCH_DIRECT_BOOT_UNAWARE
-            | PackageManager.MATCH_UNINSTALLED_PACKAGES;
+                    | PackageManager.MATCH_DIRECT_BOOT_UNAWARE
+                    | PackageManager.MATCH_UNINSTALLED_PACKAGES;
 
     // Stats
     @VisibleForTesting
@@ -306,13 +311,15 @@
         int GET_APPLICATION_INFO = 3;
         int LAUNCHER_PERMISSION_CHECK = 4;
         int CLEANUP_DANGLING_BITMAPS = 5;
-        int GET_ACTIVITIES_WITH_METADATA = 6;
+        int GET_ACTIVITY_WITH_METADATA = 6;
         int GET_INSTALLED_PACKAGES = 7;
         int CHECK_PACKAGE_CHANGES = 8;
         int GET_APPLICATION_RESOURCES = 9;
         int RESOURCE_NAME_LOOKUP = 10;
+        int GET_LAUNCHER_ACTIVITY = 11;
+        int CHECK_LAUNCHER_ACTIVITY = 12;
 
-        int COUNT = RESOURCE_NAME_LOOKUP + 1;
+        int COUNT = CHECK_LAUNCHER_ACTIVITY + 1;
     }
 
     final Object mStatLock = new Object();
@@ -335,9 +342,10 @@
             OPERATION_SET,
             OPERATION_ADD,
             OPERATION_UPDATE
-            })
+    })
     @Retention(RetentionPolicy.SOURCE)
-    @interface ShortcutOperation {}
+    @interface ShortcutOperation {
+    }
 
     public ShortcutService(Context context) {
         this(context, BackgroundThread.get().getLooper());
@@ -373,18 +381,22 @@
     }
 
     final private IUidObserver mUidObserver = new IUidObserver.Stub() {
-        @Override public void onUidStateChanged(int uid, int procState) throws RemoteException {
+        @Override
+        public void onUidStateChanged(int uid, int procState) throws RemoteException {
             handleOnUidStateChanged(uid, procState);
         }
 
-        @Override public void onUidGone(int uid) throws RemoteException {
+        @Override
+        public void onUidGone(int uid) throws RemoteException {
             handleOnUidStateChanged(uid, ActivityManager.MAX_PROCESS_STATE);
         }
 
-        @Override public void onUidActive(int uid) throws RemoteException {
+        @Override
+        public void onUidActive(int uid) throws RemoteException {
         }
 
-        @Override public void onUidIdle(int uid) throws RemoteException {
+        @Override
+        public void onUidIdle(int uid) throws RemoteException {
         }
     };
 
@@ -555,11 +567,11 @@
 
         final int iconDimensionDp = Math.max(1, injectIsLowRamDevice()
                 ? (int) parser.getLong(
-                    ConfigConstants.KEY_MAX_ICON_DIMENSION_DP_LOWRAM,
-                    DEFAULT_MAX_ICON_DIMENSION_LOWRAM_DP)
+                ConfigConstants.KEY_MAX_ICON_DIMENSION_DP_LOWRAM,
+                DEFAULT_MAX_ICON_DIMENSION_LOWRAM_DP)
                 : (int) parser.getLong(
-                    ConfigConstants.KEY_MAX_ICON_DIMENSION_DP,
-                    DEFAULT_MAX_ICON_DIMENSION_DP));
+                ConfigConstants.KEY_MAX_ICON_DIMENSION_DP,
+                DEFAULT_MAX_ICON_DIMENSION_DP));
 
         mMaxIconDimension = injectDipToPixel(iconDimensionDp);
 
@@ -777,7 +789,7 @@
             }
         } catch (FileNotFoundException e) {
             // Use the default
-        } catch (IOException|XmlPullParserException e) {
+        } catch (IOException | XmlPullParserException e) {
             Slog.e(TAG, "Failed to read file " + file.getBaseFile(), e);
 
             mRawLastResetTime = 0;
@@ -800,7 +812,7 @@
             saveUserInternalLocked(userId, os, /* forBackup= */ false);
 
             file.finishWrite(os);
-        } catch (XmlPullParserException|IOException e) {
+        } catch (XmlPullParserException | IOException e) {
             Slog.e(TAG, "Failed to write to file " + file.getBaseFile(), e);
             file.failWrite(os);
         }
@@ -850,10 +862,10 @@
             return null;
         }
         try {
-            final ShortcutUser ret =  loadUserInternal(userId, in, /* forBackup= */ false);
+            final ShortcutUser ret = loadUserInternal(userId, in, /* forBackup= */ false);
             cleanupDanglingBitmapDirectoriesLocked(userId, ret);
             return ret;
-        } catch (IOException|XmlPullParserException e) {
+        } catch (IOException | XmlPullParserException e) {
             Slog.e(TAG, "Failed to read file " + file.getBaseFile(), e);
             return null;
         } finally {
@@ -1133,7 +1145,7 @@
         }
 
         final String baseName = String.valueOf(injectCurrentTimeMillis());
-        for (int suffix = 0;; suffix++) {
+        for (int suffix = 0; ; suffix++) {
             final String filename = (suffix == 0 ? baseName : baseName + "_" + suffix) + ".png";
             final File file = new File(packagePath, filename);
             if (!file.exists()) {
@@ -1205,7 +1217,7 @@
                     } finally {
                         IoUtils.closeQuietly(out);
                     }
-                } catch (IOException|RuntimeException e) {
+                } catch (IOException | RuntimeException e) {
                     // STOPSHIP Change wtf to e
                     Slog.wtf(ShortcutService.TAG, "Unable to write bitmap to file", e);
                     if (path != null && path.exists()) {
@@ -1284,7 +1296,7 @@
 
     private boolean isCallerSystem() {
         final int callingUid = injectBinderCallingUid();
-         return UserHandle.isSameApp(callingUid, Process.SYSTEM_UID);
+        return UserHandle.isSameApp(callingUid, Process.SYSTEM_UID);
     }
 
     private boolean isCallerShell() {
@@ -1349,7 +1361,7 @@
 
     /**
      * @throws IllegalArgumentException if {@code numShortcuts} is bigger than
-     * {@link #getMaxActivityShortcuts()}.
+     *                                  {@link #getMaxActivityShortcuts()}.
      */
     void enforceMaxActivityShortcuts(int numShortcuts) {
         if (numShortcuts > mMaxShortcuts) {
@@ -1402,7 +1414,7 @@
      * Clean up / validate an incoming shortcut.
      * - Make sure all mandatory fields are set.
      * - Make sure the intent's extras are persistable, and them to set
-     *  {@link ShortcutInfo#mIntentPersistableExtras}.  Also clear its extras.
+     * {@link ShortcutInfo#mIntentPersistableExtras}.  Also clear its extras.
      * - Clear flags.
      *
      * TODO Detailed unit tests
@@ -1412,11 +1424,15 @@
         if (shortcut.getActivity() != null) {
             Preconditions.checkState(
                     shortcut.getPackage().equals(shortcut.getActivity().getPackageName()),
-                    "Activity package name mismatch");
+                    "Cannot publish shortcut: activity " + shortcut.getActivity() + " does not"
+                    + " belong to package " + shortcut.getPackage());
         }
 
         if (!forUpdate) {
             shortcut.enforceMandatoryFields();
+            Preconditions.checkArgument(
+                    injectIsMainActivity(shortcut.getActivity(), shortcut.getUserId()),
+                    "Cannot publish shortcut: " + shortcut.getActivity() + " is not main activity");
         }
         if (shortcut.getIcon() != null) {
             ShortcutInfo.validateIcon(shortcut.getIcon());
@@ -1425,6 +1441,26 @@
         shortcut.replaceFlags(0);
     }
 
+    /**
+     * 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);
+            if (si.getActivity() == null) {
+                if (defaultActivity == null) {
+                    defaultActivity = injectGetDefaultMainActivity(
+                            si.getPackage(), si.getUserId());
+                    Preconditions.checkState(defaultActivity != null,
+                            "Launcher activity not found for package " + si.getPackage());
+                }
+                si.setActivity(defaultActivity);
+            }
+        }
+    }
+
     private void assignImplicitRanks(List<ShortcutInfo> shortcuts) {
         for (int i = shortcuts.size() - 1; i >= 0; i--) {
             shortcuts.get(i).setImplicitRank(i);
@@ -1446,6 +1482,8 @@
 
             ps.ensureImmutableShortcutsNotIncluded(newShortcuts);
 
+            fillInDefaultActivity(newShortcuts);
+
             ps.enforceShortcutCountsBeforeOperation(newShortcuts, OPERATION_SET);
 
             // Throttling.
@@ -1493,6 +1531,9 @@
 
             ps.ensureImmutableShortcutsNotIncluded(newShortcuts);
 
+            // For update, don't fill in the default activity.  Having null activity means
+            // "don't update the activity" here.
+
             ps.enforceShortcutCountsBeforeOperation(newShortcuts, OPERATION_UPDATE);
 
             // Throttling.
@@ -1573,6 +1614,8 @@
 
             ps.ensureImmutableShortcutsNotIncluded(newShortcuts);
 
+            fillInDefaultActivity(newShortcuts);
+
             ps.enforceShortcutCountsBeforeOperation(newShortcuts, OPERATION_ADD);
 
             // Initialize the implicit ranks for ShortcutPackage.adjustRanks().
@@ -1795,7 +1838,8 @@
     }
 
     /**
-     * Reset all throttling, for developer options and command line.  Only system/shell can call it.
+     * Reset all throttling, for developer options and command line.  Only system/shell can call
+     * it.
      */
     @Override
     public void resetThrottling() {
@@ -1917,10 +1961,12 @@
 
     // === House keeping ===
 
-    private void cleanUpPackageForAllLoadedUsers(String packageName, @UserIdInt int packageUserId) {
+    private void cleanUpPackageForAllLoadedUsers(String packageName, @UserIdInt int packageUserId,
+            boolean appStillExists) {
         synchronized (mLock) {
             forEachLoadedUserLocked(user ->
-                    cleanUpPackageLocked(packageName, user.getUserId(), packageUserId));
+                    cleanUpPackageLocked(packageName, user.getUserId(), packageUserId,
+                            appStillExists));
         }
     }
 
@@ -1932,7 +1978,8 @@
      * This is called when an app is uninstalled, or an app gets "clear data"ed.
      */
     @VisibleForTesting
-    void cleanUpPackageLocked(String packageName, int owningUserId, int packageUserId) {
+    void cleanUpPackageLocked(String packageName, int owningUserId, int packageUserId,
+            boolean appStillExists) {
         final boolean wasUserLoaded = isUserLoadedLocked(owningUserId);
 
         final ShortcutUser user = getUserShortcutsLocked(owningUserId);
@@ -1961,6 +2008,13 @@
             notifyListeners(packageName, owningUserId);
         }
 
+        // If the app still exists (i.e. data cleared), we need to re-publish manifest shortcuts.
+        if (appStillExists && (packageUserId == owningUserId)) {
+            // This will do the notification and save when needed, so do it after the above
+            // notifyListeners.
+            user.handlePackageAddedOrUpdated(packageName, /* forceRescan=*/ true);
+        }
+
         if (!wasUserLoaded) {
             // Note this will execute the scheduled save.
             unloadUserLocked(owningUserId);
@@ -2269,19 +2323,29 @@
         public void onPackageDataCleared(String packageName, int uid) {
             handlePackageDataCleared(packageName, getChangingUserId());
         }
+
+        @Override
+        public boolean onPackageChanged(String packageName, int uid, String[] components) {
+            handlePackageChanged(packageName, getChangingUserId());
+            return false; // We don't need to receive onSomePackagesChanged(), so just false.
+        }
     };
 
     /**
      * Called when a user is unlocked.
      * - Check all known packages still exist, and otherwise perform cleanup.
      * - If a package still exists, check the version code.  If it's been updated, may need to
-     *   update timestamps of its shortcuts.
+     * update timestamps of its shortcuts.
      */
     @VisibleForTesting
     void checkPackageChanges(@UserIdInt int ownerUserId) {
         if (DEBUG) {
             Slog.d(TAG, "checkPackageChanges() ownerUserId=" + ownerUserId);
         }
+        if (injectIsSafeModeEnabled()) {
+            Slog.i(TAG, "Safe mode, skipping checkPackageChanges()");
+            return;
+        }
 
         final long start = injectElapsedRealtime();
         try {
@@ -2302,14 +2366,15 @@
                 if (gonePackages.size() > 0) {
                     for (int i = gonePackages.size() - 1; i >= 0; i--) {
                         final PackageWithUser pu = gonePackages.get(i);
-                        cleanUpPackageLocked(pu.packageName, ownerUserId, pu.userId);
+                        cleanUpPackageLocked(pu.packageName, ownerUserId, pu.userId,
+                                /* appStillExists = */ false);
                     }
                 }
                 final long now = injectCurrentTimeMillis();
 
                 // Then for each installed app, publish manifest shortcuts when needed.
                 forUpdatedPackages(ownerUserId, user.getLastAppScanTime(), ai -> {
-                    user.handlePackageAddedOrUpdated(ai.packageName);
+                    user.handlePackageAddedOrUpdated(ai.packageName, /* forceRescan=*/ false);
                 });
 
                 // Write the time just before the scan, because there may be apps that have just
@@ -2330,7 +2395,7 @@
         synchronized (mLock) {
             final ShortcutUser user = getUserShortcutsLocked(userId);
             user.attemptToRestoreIfNeededAndSave(this, packageName, userId);
-            user.handlePackageAddedOrUpdated(packageName);
+            user.handlePackageAddedOrUpdated(packageName, /* forceRescan=*/ false);
         }
         verifyStates();
     }
@@ -2345,7 +2410,7 @@
             user.attemptToRestoreIfNeededAndSave(this, packageName, userId);
 
             if (isPackageInstalled(packageName, userId)) {
-                user.handlePackageAddedOrUpdated(packageName);
+                user.handlePackageAddedOrUpdated(packageName, /* forceRescan=*/ false);
             }
         }
         verifyStates();
@@ -2356,7 +2421,7 @@
             Slog.d(TAG, String.format("handlePackageRemoved: %s user=%d", packageName,
                     packageUserId));
         }
-        cleanUpPackageForAllLoadedUsers(packageName, packageUserId);
+        cleanUpPackageForAllLoadedUsers(packageName, packageUserId, /* appStillExists = */ false);
 
         verifyStates();
     }
@@ -2366,7 +2431,23 @@
             Slog.d(TAG, String.format("handlePackageDataCleared: %s user=%d", packageName,
                     packageUserId));
         }
-        cleanUpPackageForAllLoadedUsers(packageName, packageUserId);
+        cleanUpPackageForAllLoadedUsers(packageName, packageUserId, /* appStillExists = */ true);
+
+        verifyStates();
+    }
+
+    private void handlePackageChanged(String packageName, int packageUserId) {
+        if (DEBUG) {
+            Slog.d(TAG, String.format("handlePackageChanged: %s user=%d", packageName,
+                    packageUserId));
+        }
+
+        // Activities may be disabled or enabled.  Just rescan the package.
+        synchronized (mLock) {
+            final ShortcutUser user = getUserShortcutsLocked(packageUserId);
+
+            user.handlePackageAddedOrUpdated(packageName, /* forceRescan=*/ true);
+        }
 
         verifyStates();
     }
@@ -2405,7 +2486,7 @@
         final long token = injectClearCallingIdentity();
         try {
             return mIPackageManager.getPackageInfo(packageName, PACKAGE_MATCH_FLAGS
-                    | (getSignatures ? PackageManager.GET_SIGNATURES : 0)
+                            | (getSignatures ? PackageManager.GET_SIGNATURES : 0)
                     , userId);
         } catch (RemoteException e) {
             // Shouldn't happen.
@@ -2439,14 +2520,12 @@
     }
 
     @Nullable
-    @VisibleForTesting
-    PackageInfo injectGetActivitiesWithMetadata(String packageName, @UserIdInt int userId) {
+    ActivityInfo injectGetActivityInfoWithMetadata(ComponentName activity, @UserIdInt int userId) {
         final long start = injectElapsedRealtime();
         final long token = injectClearCallingIdentity();
         try {
-            return mIPackageManager.getPackageInfo(packageName,
-                    PACKAGE_MATCH_FLAGS | PackageManager.GET_ACTIVITIES
-                            | PackageManager.GET_META_DATA, userId);
+            return mIPackageManager.getActivityInfo(activity,
+                    PACKAGE_MATCH_FLAGS | PackageManager.GET_META_DATA, userId);
         } catch (RemoteException e) {
             // Shouldn't happen.
             Slog.wtf(TAG, "RemoteException", e);
@@ -2454,7 +2533,7 @@
         } finally {
             injectRestoreCallingIdentity(token);
 
-            logDurationStat(Stats.GET_ACTIVITIES_WITH_METADATA, start);
+            logDurationStat(Stats.GET_ACTIVITY_WITH_METADATA, start);
         }
     }
 
@@ -2531,6 +2610,86 @@
         }
     }
 
+    private Intent getMainActivityIntent() {
+        final Intent intent = new Intent(Intent.ACTION_MAIN);
+        intent.addCategory(LAUNCHER_INTENT_CATEGORY);
+        return intent;
+    }
+
+    @Nullable
+    ComponentName injectGetDefaultMainActivity(@NonNull String packageName, int userId) {
+        final long start = injectElapsedRealtime();
+        final long token = injectClearCallingIdentity();
+        try {
+            final Intent intent = getMainActivityIntent();
+            intent.setPackage(packageName);
+
+            final List<ResolveInfo> resolved =
+                    mContext.getPackageManager().queryIntentActivitiesAsUser(
+                            intent, PACKAGE_MATCH_FLAGS, userId);
+
+            return (resolved == null || resolved.size() == 0)
+                    ? null : resolved.get(0).activityInfo.getComponentName();
+        } finally {
+            injectRestoreCallingIdentity(token);
+
+            logDurationStat(Stats.GET_LAUNCHER_ACTIVITY, start);
+        }
+    }
+
+    boolean injectIsMainActivity(@NonNull ComponentName activity, int userId) {
+        final long start = injectElapsedRealtime();
+        final long token = injectClearCallingIdentity();
+        try {
+            final Intent intent = getMainActivityIntent();
+            intent.setPackage(activity.getPackageName());
+            intent.setComponent(activity);
+
+            final List<ResolveInfo> resolved =
+                    mContext.getPackageManager().queryIntentActivitiesAsUser(
+                            intent, PACKAGE_MATCH_FLAGS, userId);
+
+            return resolved != null && resolved.size() > 0;
+        } finally {
+            injectRestoreCallingIdentity(token);
+
+            logDurationStat(Stats.CHECK_LAUNCHER_ACTIVITY, start);
+        }
+    }
+
+    @NonNull
+    List<ResolveInfo> injectGetMainActivities(@NonNull String packageName, int userId) {
+        final long start = injectElapsedRealtime();
+        final long token = injectClearCallingIdentity();
+        try {
+            final Intent intent = getMainActivityIntent();
+            intent.setPackage(packageName);
+
+            final List<ResolveInfo> resolved =
+                    mContext.getPackageManager().queryIntentActivitiesAsUser(
+                            intent, PACKAGE_MATCH_FLAGS, userId);
+
+            return (resolved != null) ? resolved : new ArrayList<>(0);
+        } finally {
+            injectRestoreCallingIdentity(token);
+
+            logDurationStat(Stats.CHECK_LAUNCHER_ACTIVITY, start);
+        }
+    }
+
+    boolean injectIsSafeModeEnabled() {
+        final long token = injectClearCallingIdentity();
+        try {
+            return IWindowManager.Stub
+                    .asInterface(ServiceManager.getService(Context.WINDOW_SERVICE))
+                    .isSafeModeEnabled();
+        } catch (RemoteException e) {
+            return false; // Shouldn't happen though.
+        } finally {
+            injectRestoreCallingIdentity(token);
+        }
+    }
+
     // === Backup & restore ===
 
     boolean shouldBackupApp(String packageName, int userId) {
@@ -2560,7 +2719,7 @@
             final ByteArrayOutputStream os = new ByteArrayOutputStream(32 * 1024);
             try {
                 saveUserInternalLocked(userId, os, /* forBackup */ true);
-            } catch (XmlPullParserException|IOException e) {
+            } catch (XmlPullParserException | IOException e) {
                 // Shouldn't happen.
                 Slog.w(TAG, "Backup failed.", e);
                 return null;
@@ -2579,7 +2738,7 @@
         final ByteArrayInputStream is = new ByteArrayInputStream(payload);
         try {
             user = loadUserInternal(userId, is, /* fromBackup */ true);
-        } catch (XmlPullParserException|IOException e) {
+        } catch (XmlPullParserException | IOException e) {
             Slog.w(TAG, "Restoration failed.", e);
             return;
         }
@@ -2656,7 +2815,7 @@
             pw.println(mResetInterval);
             pw.print("    maxUpdatesPerInterval: ");
             pw.println(mMaxUpdatesPerInterval);
-            pw.print("    maxDynamicShortcuts: ");
+            pw.print("    maxShortcutsPerActivity: ");
             pw.println(mMaxShortcuts);
             pw.println();
 
@@ -2670,11 +2829,13 @@
                 dumpStatLS(pw, p, Stats.GET_PACKAGE_INFO_WITH_SIG, "getPackageInfo(SIG)");
                 dumpStatLS(pw, p, Stats.GET_APPLICATION_INFO, "getApplicationInfo");
                 dumpStatLS(pw, p, Stats.CLEANUP_DANGLING_BITMAPS, "cleanupDanglingBitmaps");
-                dumpStatLS(pw, p, Stats.GET_ACTIVITIES_WITH_METADATA, "getActivities+metadata");
+                dumpStatLS(pw, p, Stats.GET_ACTIVITY_WITH_METADATA, "getActivity+metadata");
                 dumpStatLS(pw, p, Stats.GET_INSTALLED_PACKAGES, "getInstalledPackages");
                 dumpStatLS(pw, p, Stats.CHECK_PACKAGE_CHANGES, "checkPackageChanges");
                 dumpStatLS(pw, p, Stats.GET_APPLICATION_RESOURCES, "getApplicationResources");
                 dumpStatLS(pw, p, Stats.RESOURCE_NAME_LOOKUP, "resourceNameLookup");
+                dumpStatLS(pw, p, Stats.GET_LAUNCHER_ACTIVITY, "getLauncherActivity");
+                dumpStatLS(pw, p, Stats.CHECK_LAUNCHER_ACTIVITY, "checkLauncherActivity");
             }
 
             for (int i = 0; i < mUsers.size(); i++) {
@@ -2725,7 +2886,9 @@
 
         enforceShell();
 
-        (new MyShellCommand()).exec(this, in, out, err, args, resultReceiver);
+        final int status = (new MyShellCommand()).exec(this, in, out, err, args, resultReceiver);
+
+        resultReceiver.send(status, null);
     }
 
     static class CommandException extends Exception {
@@ -2796,6 +2959,9 @@
                     case "clear-shortcuts":
                         handleClearShortcuts();
                         break;
+                    case "verify-states": // hidden command to verify various internal states.
+                        handleVerifyStates();
+                        break;
                     default:
                         return handleDefaultCommands(cmd);
                 }
@@ -2938,7 +3104,16 @@
 
             Slog.i(TAG, "cmd: handleClearShortcuts: " + mUserId + ", " + packageName);
 
-            ShortcutService.this.cleanUpPackageForAllLoadedUsers(packageName, mUserId);
+            ShortcutService.this.cleanUpPackageForAllLoadedUsers(packageName, mUserId,
+                    /* appStillExists = */ true);
+        }
+
+        private void handleVerifyStates() throws CommandException {
+            try {
+                verifyStatesForce(); // This will throw when there's an issue.
+            } catch (Throwable th) {
+                throw new CommandException(th.getMessage() + "\n" + Log.getStackTraceString(th));
+            }
         }
     }
 
@@ -2978,7 +3153,7 @@
     }
 
     final void wtf(String message) {
-        wtf( message, /* exception= */ null);
+        wtf(message, /* exception= */ null);
     }
 
     // Injection point.
@@ -3092,6 +3267,10 @@
         }
     }
 
+    private final void verifyStatesForce() {
+        verifyStatesInner();
+    }
+
     private void verifyStatesInner() {
         synchronized (this) {
             forEachLoadedUserLocked(u -> u.forAllPackageItems(ShortcutPackageItem::verifyStates));
diff --git a/services/core/java/com/android/server/pm/ShortcutUser.java b/services/core/java/com/android/server/pm/ShortcutUser.java
index f8ee325..7ea89c9 100644
--- a/services/core/java/com/android/server/pm/ShortcutUser.java
+++ b/services/core/java/com/android/server/pm/ShortcutUser.java
@@ -88,7 +88,7 @@
 
         @Override
         public String toString() {
-            return String.format("{Package: %d, %s}", userId, packageName);
+            return String.format("[Package: %d, %s]", userId, packageName);
         }
     }
 
@@ -99,8 +99,6 @@
 
     private final ArrayMap<String, ShortcutPackage> mPackages = new ArrayMap<>();
 
-    private final SparseArray<ShortcutPackage> mPackagesFromUid = new SparseArray<>();
-
     private final ArrayMap<PackageWithUser, ShortcutLauncher> mLaunchers = new ArrayMap<>();
 
     /** Default launcher that can access the launcher apps APIs. */
@@ -244,12 +242,12 @@
         }
     }
 
-    public void handlePackageAddedOrUpdated(@NonNull String packageName) {
+    public void handlePackageAddedOrUpdated(@NonNull String packageName, boolean forceRescan) {
         final boolean isNewApp = !mPackages.containsKey(packageName);
 
         final ShortcutPackage shortcutPackage = getPackageShortcuts(packageName);
 
-        if (!shortcutPackage.handlePackageAddedOrUpdated(isNewApp)) {
+        if (!shortcutPackage.handlePackageAddedOrUpdated(isNewApp, forceRescan)) {
             if (isNewApp) {
                 mPackages.remove(packageName);
             }
@@ -381,8 +379,10 @@
         pw.print(mUserId);
         pw.print("  Known locale seq#: ");
         pw.print(mKnownLocaleChangeSequenceNumber);
-        pw.print("  Last app scan: ");
+        pw.print("  Last app scan: [");
         pw.print(mLastAppScanTime);
+        pw.print("] ");
+        pw.print(ShortcutService.formatTime(mLastAppScanTime));
         pw.println();
 
         prefix += prefix + "  ";
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 bf56da3..71d1a3a 100644
--- a/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java
@@ -52,6 +52,7 @@
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManagerInternal;
+import android.content.pm.ResolveInfo;
 import android.content.pm.ShortcutInfo;
 import android.content.pm.ShortcutManager;
 import android.content.pm.ShortcutServiceInternal;
@@ -98,8 +99,11 @@
 import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
+import java.util.function.BiFunction;
 import java.util.function.BiPredicate;
 import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Predicate;
 
 public abstract class BaseShortcutManagerTest extends InstrumentationTestCase {
     protected static final String TAG = "ShortcutManagerTest";
@@ -114,6 +118,8 @@
 
     protected static final String[] EMPTY_STRINGS = new String[0]; // Just for readability.
 
+    protected static final String MAIN_ACTIVITY_CLASS = "MainActivity";
+
     // public for mockito
     public class BaseContext extends MockContext {
         @Override
@@ -311,8 +317,55 @@
         }
 
         @Override
-        PackageInfo injectGetActivitiesWithMetadata(String packageName, @UserIdInt int userId) {
-            return mContext.injectGetActivitiesWithMetadata(packageName, userId);
+        ActivityInfo injectGetActivityInfoWithMetadata(ComponentName activity,
+                @UserIdInt int userId) {
+            final PackageInfo pi = mContext.injectGetActivitiesWithMetadata(
+                    activity.getPackageName(), userId);
+            if (pi == null || pi.activities == null) {
+                return null;
+            }
+            for (ActivityInfo ai : pi.activities) {
+                if (!mEnabledActivityChecker.test(ai.getComponentName(), userId)) {
+                    continue;
+                }
+                if (activity.equals(ai.getComponentName())) {
+                    return ai;
+                }
+            }
+            return null;
+        }
+
+        @Override
+        boolean injectIsMainActivity(@NonNull ComponentName activity, int userId) {
+            if (!mEnabledActivityChecker.test(activity, userId)) {
+                return false;
+            }
+            return mMainActivityChecker.test(activity, userId);
+        }
+
+        @Override
+        List<ResolveInfo> injectGetMainActivities(@NonNull String packageName, int userId) {
+            final PackageInfo pi = mContext.injectGetActivitiesWithMetadata(
+                    packageName, userId);
+            if (pi == null || pi.activities == null) {
+                return null;
+            }
+            final ArrayList<ResolveInfo> ret = new ArrayList<>(pi.activities.length);
+            for (int i = 0; i < pi.activities.length; i++) {
+                if (!mEnabledActivityChecker.test(pi.activities[i].getComponentName(), userId)) {
+                    continue;
+                }
+                final ResolveInfo ri = new ResolveInfo();
+                ri.activityInfo = pi.activities[i];
+                ret.add(ri);
+            }
+
+            return ret;
+        }
+
+        @Override
+        ComponentName injectGetDefaultMainActivity(@NonNull String packageName, int userId) {
+            return mMainActivityFetcher.apply(packageName, userId);
         }
 
         @Override
@@ -335,6 +388,11 @@
         }
 
         @Override
+        boolean injectIsSafeModeEnabled() {
+            return mSafeMode;
+        }
+
+        @Override
         void wtf(String message, Exception e) {
             // During tests, WTF is fatal.
             fail(message + "  exception: " + e);
@@ -441,6 +499,8 @@
 
     protected File mInjectedFilePathRoot;
 
+    protected boolean mSafeMode;
+
     protected long mInjectedCurrentTimeMillis;
 
     protected boolean mInjectedIsLowRamDevice;
@@ -512,6 +572,15 @@
             LAUNCHER_1.equals(callingPackage) || LAUNCHER_2.equals(callingPackage)
             || LAUNCHER_3.equals(callingPackage) || LAUNCHER_4.equals(callingPackage);
 
+    protected BiPredicate<ComponentName, Integer> mMainActivityChecker =
+            (activity, userId) -> true;
+
+    protected BiFunction<String, Integer, ComponentName> mMainActivityFetcher =
+            (packageName, userId) -> new ComponentName(packageName, MAIN_ACTIVITY_CLASS);
+
+    protected BiPredicate<ComponentName, Integer> mEnabledActivityChecker
+            = (activity, userId) -> true; // all activities are enabled.
+
     protected static final long START_TIME = 1440000000101L;
 
     protected static final long INTERVAL = 10000;
@@ -1051,10 +1120,6 @@
                 + "/" + ShortcutService.FILENAME_USER_PACKAGES, message);
     }
 
-    protected void waitOnMainThread() throws Throwable {
-        runTestOnUiThread(() -> {});
-    }
-
     /**
      * Make a shortcut with an ID.
      */
@@ -1410,6 +1475,13 @@
         return i;
     }
 
+    protected Intent genPackageChangedIntent(String pakcageName, int userId) {
+        Intent i = new Intent(Intent.ACTION_PACKAGE_CHANGED);
+        i.setData(Uri.parse("package:" + pakcageName));
+        i.putExtra(Intent.EXTRA_USER_HANDLE, userId);
+        return i;
+    }
+
     protected Intent genPackageDataClear(String packageName, int userId) {
         Intent i = new Intent(Intent.ACTION_PACKAGE_DATA_CLEARED);
         i.setData(Uri.parse("package:" + packageName));
diff --git a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java
index cec4782..7d33a30 100644
--- a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java
+++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java
@@ -40,6 +40,7 @@
 import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertDynamicShortcutCountExceeded;
 import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertEmpty;
 import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertExpectException;
+import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertForLauncherCallback;
 import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertShortcutIds;
 import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertWith;
 import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.filterByActivity;
@@ -50,6 +51,7 @@
 import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.pfdToBitmap;
 import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.resetAll;
 import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.set;
+import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.waitOnMainThread;
 
 import static org.mockito.Matchers.any;
 import static org.mockito.Matchers.anyInt;
@@ -84,6 +86,7 @@
 import com.android.server.pm.ShortcutService.ConfigConstants;
 import com.android.server.pm.ShortcutService.FileOutputStreamWithPath;
 import com.android.server.pm.ShortcutUser.PackageWithUser;
+import com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.ShortcutListAsserter;
 
 import org.mockito.ArgumentCaptor;
 
@@ -91,6 +94,9 @@
 import java.io.IOException;
 import java.util.List;
 import java.util.Locale;
+import java.util.function.BiFunction;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
 
 /**
  * Tests for ShortcutService and ShortcutManager.
@@ -347,6 +353,127 @@
         });
     }
 
+    public void testPublishWithNoActivity() {
+        // If activity is not explicitly set, use the default one.
+
+        runWithCaller(CALLING_PACKAGE_2, USER_10, () -> {
+            // s1 and s3 has no activities.
+            final ShortcutInfo si1 = new ShortcutInfo.Builder(mClientContext, "si1")
+                    .setShortLabel("label1")
+                    .setIntent(new Intent("action1"))
+                    .build();
+            final ShortcutInfo si2 = new ShortcutInfo.Builder(mClientContext, "si2")
+                    .setShortLabel("label2")
+                    .setActivity(new ComponentName(getCallingPackage(), "abc"))
+                    .setIntent(new Intent("action2"))
+                    .build();
+            final ShortcutInfo si3 = new ShortcutInfo.Builder(mClientContext, "si3")
+                    .setShortLabel("label3")
+                    .setIntent(new Intent("action3"))
+                    .build();
+
+            // Set test 1
+            assertTrue(mManager.setDynamicShortcuts(list(si1)));
+
+            assertWith(getCallerShortcuts())
+                    .haveIds("si1")
+                    .forShortcutWithId("si1", si -> {
+                        assertEquals(new ComponentName(getCallingPackage(),
+                                MAIN_ACTIVITY_CLASS), si.getActivity());
+                    });
+
+            // Set test 2
+            assertTrue(mManager.setDynamicShortcuts(list(si2, si1)));
+
+            assertWith(getCallerShortcuts())
+                    .haveIds("si1", "si2")
+                    .forShortcutWithId("si1", si -> {
+                        assertEquals(new ComponentName(getCallingPackage(),
+                                MAIN_ACTIVITY_CLASS), si.getActivity());
+                    })
+                    .forShortcutWithId("si2", si -> {
+                        assertEquals(new ComponentName(getCallingPackage(),
+                                "abc"), si.getActivity());
+                    });
+
+
+            // Set test 3
+            assertTrue(mManager.setDynamicShortcuts(list(si3, si1)));
+
+            assertWith(getCallerShortcuts())
+                    .haveIds("si1", "si3")
+                    .forShortcutWithId("si1", si -> {
+                        assertEquals(new ComponentName(getCallingPackage(),
+                                MAIN_ACTIVITY_CLASS), si.getActivity());
+                    })
+                    .forShortcutWithId("si3", si -> {
+                        assertEquals(new ComponentName(getCallingPackage(),
+                                MAIN_ACTIVITY_CLASS), si.getActivity());
+                    });
+
+            mInjectedCurrentTimeMillis += INTERVAL; // reset throttling
+
+            // Add test 1
+            mManager.removeAllDynamicShortcuts();
+            assertTrue(mManager.addDynamicShortcuts(list(si1)));
+
+            assertWith(getCallerShortcuts())
+                    .haveIds("si1")
+                    .forShortcutWithId("si1", si -> {
+                        assertEquals(new ComponentName(getCallingPackage(),
+                                MAIN_ACTIVITY_CLASS), si.getActivity());
+                    });
+
+            // Add test 2
+            mManager.removeAllDynamicShortcuts();
+            assertTrue(mManager.addDynamicShortcuts(list(si2, si1)));
+
+            assertWith(getCallerShortcuts())
+                    .haveIds("si1", "si2")
+                    .forShortcutWithId("si1", si -> {
+                        assertEquals(new ComponentName(getCallingPackage(),
+                                MAIN_ACTIVITY_CLASS), si.getActivity());
+                    })
+                    .forShortcutWithId("si2", si -> {
+                        assertEquals(new ComponentName(getCallingPackage(),
+                                "abc"), si.getActivity());
+                    });
+
+
+            // Add test 3
+            mManager.removeAllDynamicShortcuts();
+            assertTrue(mManager.addDynamicShortcuts(list(si3, si1)));
+
+            assertWith(getCallerShortcuts())
+                    .haveIds("si1", "si3")
+                    .forShortcutWithId("si1", si -> {
+                        assertEquals(new ComponentName(getCallingPackage(),
+                                MAIN_ACTIVITY_CLASS), si.getActivity());
+                    })
+                    .forShortcutWithId("si3", si -> {
+                        assertEquals(new ComponentName(getCallingPackage(),
+                                MAIN_ACTIVITY_CLASS), si.getActivity());
+                    });
+        });
+    }
+
+    public void testPublishWithNoActivity_noMainActivityInPackage() {
+        runWithCaller(CALLING_PACKAGE_2, USER_10, () -> {
+            final ShortcutInfo si1 = new ShortcutInfo.Builder(mClientContext, "si1")
+                    .setShortLabel("label1")
+                    .setIntent(new Intent("action1"))
+                    .build();
+
+            // Returning null means there's no main activity in this package.
+            mMainActivityFetcher = (packageName, userId) -> null;
+
+            assertExpectException(
+                    RuntimeException.class, "Launcher activity not found for", () -> {
+                        assertTrue(mManager.setDynamicShortcuts(list(si1)));
+                    });
+        });
+    }
+
     public void testDeleteDynamicShortcuts() {
         final ShortcutInfo si1 = makeShortcut("shortcut1");
         final ShortcutInfo si2 = makeShortcut("shortcut2");
@@ -2336,151 +2463,87 @@
                         + ConfigConstants.KEY_MAX_SHORTCUTS + "=99999999"
         );
 
-        LauncherApps.Callback c0 = mock(LauncherApps.Callback.class);
+        setCaller(LAUNCHER_1, USER_0);
 
-        // Set listeners
-
-        runWithCaller(LAUNCHER_1, USER_0, () -> {
-            mLauncherApps.registerCallback(c0, new Handler(Looper.getMainLooper()));
-        });
-
-        runWithCaller(CALLING_PACKAGE_1, UserHandle.USER_SYSTEM, () -> {
-            assertTrue(mManager.setDynamicShortcuts(list(
-                    makeShortcut("s1"), makeShortcut("s2"), makeShortcut("s3"))));
-        });
-
-        waitOnMainThread();
-        ArgumentCaptor<List> shortcuts = ArgumentCaptor.forClass(List.class);
-        verify(c0).onShortcutsChanged(
-                eq(CALLING_PACKAGE_1),
-                shortcuts.capture(),
-                eq(HANDLE_USER_0)
-        );
-        assertWith(shortcuts.getValue())
+        assertForLauncherCallback(mLauncherApps, () -> {
+            runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+                assertTrue(mManager.setDynamicShortcuts(list(
+                        makeShortcut("s1"), makeShortcut("s2"), makeShortcut("s3"))));
+            });
+        }).assertCallbackCalledForPackageAndUser(CALLING_PACKAGE_1, HANDLE_USER_0)
                 .haveIds("s1", "s2", "s3")
                 .areAllWithKeyFieldsOnly()
                 .areAllDynamic();
 
         // From different package.
-        reset(c0);
-        runWithCaller(CALLING_PACKAGE_2, UserHandle.USER_SYSTEM, () -> {
-            assertTrue(mManager.setDynamicShortcuts(list(
-                    makeShortcut("s1"), makeShortcut("s2"), makeShortcut("s3"))));
-        });
-        waitOnMainThread();
-        shortcuts = ArgumentCaptor.forClass(List.class);
-        verify(c0).onShortcutsChanged(
-                eq(CALLING_PACKAGE_2),
-                shortcuts.capture(),
-                eq(HANDLE_USER_0)
-        );
-        assertWith(shortcuts.getValue())
+        assertForLauncherCallback(mLauncherApps, () -> {
+            runWithCaller(CALLING_PACKAGE_2, USER_0, () -> {
+                assertTrue(mManager.setDynamicShortcuts(list(
+                        makeShortcut("s1"), makeShortcut("s2"), makeShortcut("s3"))));
+            });
+        }).assertCallbackCalledForPackageAndUser(CALLING_PACKAGE_2, HANDLE_USER_0)
                 .haveIds("s1", "s2", "s3")
                 .areAllWithKeyFieldsOnly()
                 .areAllDynamic();
 
         // Different user, callback shouldn't be called.
-        reset(c0);
-        runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
-            assertTrue(mManager.setDynamicShortcuts(list(
-                    makeShortcut("s1"), makeShortcut("s2"), makeShortcut("s3"))));
-        });
-        waitOnMainThread();
-        verify(c0, times(0)).onShortcutsChanged(
-                anyString(),
-                any(List.class),
-                any(UserHandle.class)
-        );
+        assertForLauncherCallback(mLauncherApps, () -> {
+            runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
+                assertTrue(mManager.setDynamicShortcuts(list(
+                        makeShortcut("s1"), makeShortcut("s2"), makeShortcut("s3"))));
+            });
+        }).assertNoCallbackCalled();
+
 
         // Test for addDynamicShortcuts.
-        reset(c0);
-        runWithCaller(CALLING_PACKAGE_1, UserHandle.USER_SYSTEM, () -> {
-            dumpsysOnLogcat("before addDynamicShortcuts");
-            assertTrue(mManager.addDynamicShortcuts(list(makeShortcut("s4"))));
-        });
-
-        waitOnMainThread();
-        shortcuts = ArgumentCaptor.forClass(List.class);
-        verify(c0).onShortcutsChanged(
-                eq(CALLING_PACKAGE_1),
-                shortcuts.capture(),
-                eq(HANDLE_USER_0)
-        );
-        assertWith(shortcuts.getValue())
+        assertForLauncherCallback(mLauncherApps, () -> {
+            runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+                assertTrue(mManager.addDynamicShortcuts(list(makeShortcut("s4"))));
+            });
+        }).assertCallbackCalledForPackageAndUser(CALLING_PACKAGE_1, HANDLE_USER_0)
                 .haveIds("s1", "s2", "s3", "s4")
                 .areAllWithKeyFieldsOnly()
                 .areAllDynamic();
 
         // Test for remove
-        reset(c0);
-        runWithCaller(CALLING_PACKAGE_1, UserHandle.USER_SYSTEM, () -> {
-            mManager.removeDynamicShortcuts(list("s1"));
-        });
-
-        waitOnMainThread();
-        shortcuts = ArgumentCaptor.forClass(List.class);
-        verify(c0).onShortcutsChanged(
-                eq(CALLING_PACKAGE_1),
-                shortcuts.capture(),
-                eq(HANDLE_USER_0)
-        );
-        assertWith(shortcuts.getValue())
+        assertForLauncherCallback(mLauncherApps, () -> {
+            runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+                mManager.removeDynamicShortcuts(list("s1"));
+            });
+        }).assertCallbackCalledForPackageAndUser(CALLING_PACKAGE_1, HANDLE_USER_0)
                 .haveIds("s2", "s3", "s4")
                 .areAllWithKeyFieldsOnly()
                 .areAllDynamic();
 
         // Test for update
-        reset(c0);
-        runWithCaller(CALLING_PACKAGE_1, UserHandle.USER_SYSTEM, () -> {
-            assertTrue(mManager.updateShortcuts(list(
-                    makeShortcut("s1"), makeShortcut("s2"))));
-        });
-
-        waitOnMainThread();
-        shortcuts = ArgumentCaptor.forClass(List.class);
-        verify(c0).onShortcutsChanged(
-                eq(CALLING_PACKAGE_1),
-                shortcuts.capture(),
-                eq(HANDLE_USER_0)
-        );
-        assertWith(shortcuts.getValue())
+        assertForLauncherCallback(mLauncherApps, () -> {
+            runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+                assertTrue(mManager.updateShortcuts(list(
+                        makeShortcut("s1"), makeShortcut("s2"))));
+            });
+        }).assertCallbackCalledForPackageAndUser(CALLING_PACKAGE_1, HANDLE_USER_0)
+                // All remaining shortcuts will be passed regardless of what's been updated.
                 .haveIds("s2", "s3", "s4")
                 .areAllWithKeyFieldsOnly()
                 .areAllDynamic();
 
         // Test for deleteAll
-        reset(c0);
-        runWithCaller(CALLING_PACKAGE_1, UserHandle.USER_SYSTEM, () -> {
-            mManager.removeAllDynamicShortcuts();
-        });
-
-        waitOnMainThread();
-        shortcuts = ArgumentCaptor.forClass(List.class);
-        verify(c0).onShortcutsChanged(
-                eq(CALLING_PACKAGE_1),
-                shortcuts.capture(),
-                eq(HANDLE_USER_0)
-        );
-        assertWith(shortcuts.getValue())
+        assertForLauncherCallback(mLauncherApps, () -> {
+            runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+                mManager.removeAllDynamicShortcuts();
+            });
+        }).assertCallbackCalledForPackageAndUser(CALLING_PACKAGE_1, HANDLE_USER_0)
                 .isEmpty();
 
         // Update package1 with manifest shortcuts
-        reset(c0);
-        addManifestShortcutResource(
-                new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()),
-                R.xml.shortcut_2);
-        updatePackageVersion(CALLING_PACKAGE_1, 1);
-        mService.mPackageMonitor.onReceive(getTestContext(),
-                genPackageAddIntent(CALLING_PACKAGE_1, USER_0));
-
-        waitOnMainThread();
-        shortcuts = ArgumentCaptor.forClass(List.class);
-        verify(c0).onShortcutsChanged(
-                eq(CALLING_PACKAGE_1),
-                shortcuts.capture(),
-                eq(HANDLE_USER_0)
-        );
-        assertWith(shortcuts.getValue())
+        assertForLauncherCallback(mLauncherApps, () -> {
+            addManifestShortcutResource(
+                    new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()),
+                    R.xml.shortcut_2);
+            updatePackageVersion(CALLING_PACKAGE_1, 1);
+            mService.mPackageMonitor.onReceive(getTestContext(),
+                    genPackageAddIntent(CALLING_PACKAGE_1, USER_0));
+        }).assertCallbackCalledForPackageAndUser(CALLING_PACKAGE_1, HANDLE_USER_0)
                 .areAllManifest()
                 .areAllWithKeyFieldsOnly()
                 .haveIds("ms1", "ms2");
@@ -2518,58 +2581,42 @@
         mService.mPackageMonitor.onReceive(getTestContext(),
                 genPackageAddIntent(CALLING_PACKAGE_1, USER_0));
 
-        reset(c0); // Check the callback for the next API call.
-        runWithCaller(CALLING_PACKAGE_1, UserHandle.USER_SYSTEM, () -> {
-            mManager.removeDynamicShortcuts(list("s2"));
+        assertForLauncherCallback(mLauncherApps, () -> {
+            runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+                mManager.removeDynamicShortcuts(list("s2"));
 
-            assertWith(getCallerShortcuts())
-                    .haveIds("ms2", "s1", "s2")
+                assertWith(getCallerShortcuts())
+                        .haveIds("ms2", "s1", "s2")
 
-                    .selectByIds("ms2")
-                    .areAllNotManifest()
-                    .areAllPinned()
-                    .areAllImmutable()
-                    .areAllDisabled()
+                        .selectByIds("ms2")
+                        .areAllNotManifest()
+                        .areAllPinned()
+                        .areAllImmutable()
+                        .areAllDisabled()
 
-                    .revertToOriginalList()
-                    .selectByIds("s1")
-                    .areAllDynamic()
-                    .areAllNotPinned()
-                    .areAllEnabled()
+                        .revertToOriginalList()
+                        .selectByIds("s1")
+                        .areAllDynamic()
+                        .areAllNotPinned()
+                        .areAllEnabled()
 
-                    .revertToOriginalList()
-                    .selectByIds("s2")
-                    .areAllNotDynamic()
-                    .areAllPinned()
-                    .areAllEnabled()
-                    ;
-        });
-
-        waitOnMainThread();
-        shortcuts = ArgumentCaptor.forClass(List.class);
-        verify(c0).onShortcutsChanged(
-                eq(CALLING_PACKAGE_1),
-                shortcuts.capture(),
-                eq(HANDLE_USER_0)
-        );
-        assertWith(shortcuts.getValue())
+                        .revertToOriginalList()
+                        .selectByIds("s2")
+                        .areAllNotDynamic()
+                        .areAllPinned()
+                        .areAllEnabled()
+                ;
+            });
+        }).assertCallbackCalledForPackageAndUser(CALLING_PACKAGE_1, HANDLE_USER_0)
                 .haveIds("ms2", "s1", "s2")
                 .areAllWithKeyFieldsOnly();
 
         // Remove CALLING_PACKAGE_2
-        reset(c0);
-        uninstallPackage(USER_0, CALLING_PACKAGE_2);
-        mService.cleanUpPackageLocked(CALLING_PACKAGE_2, USER_0, USER_0);
-
-        // Should get a callback with an empty list.
-        waitOnMainThread();
-        shortcuts = ArgumentCaptor.forClass(List.class);
-        verify(c0).onShortcutsChanged(
-                eq(CALLING_PACKAGE_2),
-                shortcuts.capture(),
-                eq(HANDLE_USER_0)
-        );
-        assertWith(shortcuts.getValue())
+        assertForLauncherCallback(mLauncherApps, () -> {
+            uninstallPackage(USER_0, CALLING_PACKAGE_2);
+            mService.cleanUpPackageLocked(CALLING_PACKAGE_2, USER_0, USER_0,
+                    /* appStillExists = */ false);
+        }).assertCallbackCalledForPackageAndUser(CALLING_PACKAGE_2, HANDLE_USER_0)
                 .isEmpty();
     }
 
@@ -2970,7 +3017,7 @@
 
         // Nonexistent package.
         uninstallPackage(USER_0, "abc");
-        mService.cleanUpPackageLocked("abc", USER_0, USER_0);
+        mService.cleanUpPackageLocked("abc", USER_0, USER_0, /* appStillExists = */ false);
 
         // No changes.
         assertEquals(set(CALLING_PACKAGE_1, CALLING_PACKAGE_2),
@@ -3002,7 +3049,8 @@
 
         // Remove a package.
         uninstallPackage(USER_0, CALLING_PACKAGE_1);
-        mService.cleanUpPackageLocked(CALLING_PACKAGE_1, USER_0, USER_0);
+        mService.cleanUpPackageLocked(CALLING_PACKAGE_1, USER_0, USER_0,
+                /* appStillExists = */ false);
 
         assertEquals(set(CALLING_PACKAGE_2),
                 hashSet(user0.getAllPackagesForTest().keySet()));
@@ -3033,7 +3081,7 @@
 
         // Remove a launcher.
         uninstallPackage(USER_10, LAUNCHER_1);
-        mService.cleanUpPackageLocked(LAUNCHER_1, USER_10, USER_10);
+        mService.cleanUpPackageLocked(LAUNCHER_1, USER_10, USER_10, /* appStillExists = */ false);
 
         assertEquals(set(CALLING_PACKAGE_2),
                 hashSet(user0.getAllPackagesForTest().keySet()));
@@ -3061,7 +3109,8 @@
 
         // Remove a package.
         uninstallPackage(USER_10, CALLING_PACKAGE_2);
-        mService.cleanUpPackageLocked(CALLING_PACKAGE_2, USER_10, USER_10);
+        mService.cleanUpPackageLocked(CALLING_PACKAGE_2, USER_10, USER_10,
+                /* appStillExists = */ false);
 
         assertEquals(set(CALLING_PACKAGE_2),
                 hashSet(user0.getAllPackagesForTest().keySet()));
@@ -3089,7 +3138,8 @@
 
         // Remove the other launcher from user 10 too.
         uninstallPackage(USER_10, LAUNCHER_2);
-        mService.cleanUpPackageLocked(LAUNCHER_2, USER_10, USER_10);
+        mService.cleanUpPackageLocked(LAUNCHER_2, USER_10, USER_10,
+                /* appStillExists = */ false);
 
         assertEquals(set(CALLING_PACKAGE_2),
                 hashSet(user0.getAllPackagesForTest().keySet()));
@@ -3117,7 +3167,8 @@
 
         // More remove.
         uninstallPackage(USER_10, CALLING_PACKAGE_1);
-        mService.cleanUpPackageLocked(CALLING_PACKAGE_1, USER_10, USER_10);
+        mService.cleanUpPackageLocked(CALLING_PACKAGE_1, USER_10, USER_10,
+                /* appStillExists = */ false);
 
         assertEquals(set(CALLING_PACKAGE_2),
                 hashSet(user0.getAllPackagesForTest().keySet()));
@@ -3143,6 +3194,74 @@
         mService.saveDirtyInfo();
     }
 
+    public void testCleanupPackage_republishManifests() {
+        addManifestShortcutResource(
+                new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()),
+                R.xml.shortcut_2);
+        updatePackageVersion(CALLING_PACKAGE_1, 1);
+        mService.mPackageMonitor.onReceive(getTestContext(),
+                genPackageAddIntent(CALLING_PACKAGE_1, USER_0));
+
+        runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+            assertTrue(mManager.setDynamicShortcuts(list(
+                    makeShortcut("s1"), makeShortcut("s2"), makeShortcut("s3"))));
+        });
+        runWithCaller(LAUNCHER_1, USER_0, () -> {
+            mLauncherApps.pinShortcuts(CALLING_PACKAGE_1,
+                    list("s2", "s3", "ms1", "ms2"), HANDLE_USER_0);
+        });
+
+        // Remove ms2 from manifest.
+        addManifestShortcutResource(
+                new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()),
+                R.xml.shortcut_1);
+        updatePackageVersion(CALLING_PACKAGE_1, 1);
+        mService.mPackageMonitor.onReceive(getTestContext(),
+                genPackageAddIntent(CALLING_PACKAGE_1, USER_0));
+
+        runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+            assertTrue(mManager.setDynamicShortcuts(list(
+                    makeShortcut("s1"), makeShortcut("s2"))));
+
+            // Make sure the shortcuts are in the intended state.
+            assertWith(getCallerShortcuts())
+                    .haveIds("ms1", "ms2", "s1", "s2", "s3")
+
+                    .selectByIds("ms1")
+                    .areAllManifest()
+                    .areAllPinned()
+
+                    .revertToOriginalList()
+                    .selectByIds("ms2")
+                    .areAllNotManifest()
+                    .areAllPinned()
+
+                    .revertToOriginalList()
+                    .selectByIds("s1")
+                    .areAllDynamic()
+                    .areAllNotPinned()
+
+                    .revertToOriginalList()
+                    .selectByIds("s2")
+                    .areAllDynamic()
+                    .areAllPinned()
+
+                    .revertToOriginalList()
+                    .selectByIds("s3")
+                    .areAllNotDynamic()
+                    .areAllPinned();
+        });
+
+        // Clean up + re-publish manifests.
+        mService.cleanUpPackageLocked(CALLING_PACKAGE_1, USER_0, USER_0,
+                /* appStillExists = */ true);
+        runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+            assertWith(getCallerShortcuts())
+                    .haveIds("ms1")
+                    .areAllManifest();
+        });
+    }
+
     public void testHandleGonePackage_crossProfile() {
         // Create some shortcuts.
         runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
@@ -3454,6 +3573,20 @@
         assertTrue(mManager.addDynamicShortcuts(list(
                 makeShortcutWithIcon("s1", bmp32x32), makeShortcutWithIcon("s2", bmp32x32)
         )));
+        // Also add a manifest shortcut, which should be removed too.
+        addManifestShortcutResource(
+                new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()),
+                R.xml.shortcut_1);
+        updatePackageVersion(CALLING_PACKAGE_1, 1);
+        mService.mPackageMonitor.onReceive(getTestContext(),
+                genPackageAddIntent(CALLING_PACKAGE_1, USER_0));
+        runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+            assertWith(getCallerShortcuts())
+                    .haveIds("s1", "s2", "ms1")
+
+                    .selectManifest()
+                    .haveIds("ms1");
+        });
 
         setCaller(CALLING_PACKAGE_2, USER_0);
         assertTrue(mManager.addDynamicShortcuts(list(makeShortcutWithIcon("s1", bmp32x32))));
@@ -3629,8 +3762,47 @@
         assertTrue(bitmapDirectoryExists(CALLING_PACKAGE_3, USER_10));
     }
 
-    public void testHandlePackageUpdate() throws Throwable {
+    public void testHandlePackageClearData_manifestRepublished() {
 
+        // Add two manifests and two dynamics.
+        addManifestShortcutResource(
+                new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()),
+                R.xml.shortcut_2);
+        updatePackageVersion(CALLING_PACKAGE_1, 1);
+        mService.mPackageMonitor.onReceive(getTestContext(),
+                genPackageAddIntent(CALLING_PACKAGE_1, USER_10));
+
+        runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
+            assertTrue(mManager.addDynamicShortcuts(list(
+                    makeShortcut("s1"), makeShortcut("s2"))));
+        });
+        runWithCaller(LAUNCHER_1, USER_10, () -> {
+            mLauncherApps.pinShortcuts(CALLING_PACKAGE_1, list("ms2", "s2"), HANDLE_USER_10);
+        });
+
+        runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
+            assertWith(getCallerShortcuts())
+                    .haveIds("ms1", "ms2", "s1", "s2")
+                    .areAllEnabled()
+
+                    .selectPinned()
+                    .haveIds("ms2", "s2");
+        });
+
+        // Clear data
+        mService.mPackageMonitor.onReceive(getTestContext(),
+                genPackageDataClear(CALLING_PACKAGE_1, USER_10));
+
+        // Only manifest shortcuts will remain, and are no longer pinned.
+        runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
+            assertWith(getCallerShortcuts())
+                    .haveIds("ms1", "ms2")
+                    .areAllEnabled()
+                    .areAllNotPinned();
+        });
+    }
+
+    public void testHandlePackageUpdate() throws Throwable {
         // Set up shortcuts and launchers.
 
         final Icon res32x32 = Icon.createWithResource(getTestContext(), R.drawable.black_32x32);
@@ -3894,6 +4066,202 @@
         });
     }
 
+    public void testHandlePackageChanged() {
+        final ComponentName ACTIVITY1 = new ComponentName(CALLING_PACKAGE_1, "act1");
+        final ComponentName ACTIVITY2 = new ComponentName(CALLING_PACKAGE_1, "act2");
+
+        addManifestShortcutResource(ACTIVITY1, R.xml.shortcut_1);
+        addManifestShortcutResource(ACTIVITY2, R.xml.shortcut_1_alt);
+
+        updatePackageVersion(CALLING_PACKAGE_1, 1);
+        mService.mPackageMonitor.onReceive(getTestContext(),
+                genPackageAddIntent(CALLING_PACKAGE_1, USER_10));
+
+        runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
+            assertTrue(mManager.addDynamicShortcuts(list(
+                    makeShortcutWithActivity("s1", ACTIVITY1),
+                    makeShortcutWithActivity("s2", ACTIVITY2)
+            )));
+        });
+        runWithCaller(LAUNCHER_1, USER_10, () -> {
+            mLauncherApps.pinShortcuts(CALLING_PACKAGE_1, list("ms1-alt", "s2"), HANDLE_USER_10);
+        });
+
+        runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
+            assertWith(getCallerShortcuts())
+                    .haveIds("ms1", "ms1-alt", "s1", "s2")
+                    .areAllEnabled()
+
+                    .selectPinned()
+                    .haveIds("ms1-alt", "s2")
+
+                    .revertToOriginalList()
+                    .selectByIds("ms1", "s1")
+                    .areAllWithActivity(ACTIVITY1)
+
+                    .revertToOriginalList()
+                    .selectByIds("ms1-alt", "s2")
+                    .areAllWithActivity(ACTIVITY2)
+                    ;
+        });
+
+        // First, no changes.
+        mService.mPackageMonitor.onReceive(getTestContext(),
+                genPackageChangedIntent(CALLING_PACKAGE_1, USER_10));
+
+        runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
+            assertWith(getCallerShortcuts())
+                    .haveIds("ms1", "ms1-alt", "s1", "s2")
+                    .areAllEnabled()
+
+                    .selectPinned()
+                    .haveIds("ms1-alt", "s2")
+
+                    .revertToOriginalList()
+                    .selectByIds("ms1", "s1")
+                    .areAllWithActivity(ACTIVITY1)
+
+                    .revertToOriginalList()
+                    .selectByIds("ms1-alt", "s2")
+                    .areAllWithActivity(ACTIVITY2)
+            ;
+        });
+
+        // Disable activity 1
+        mEnabledActivityChecker = (activity, userId) -> !ACTIVITY1.equals(activity);
+        mService.mPackageMonitor.onReceive(getTestContext(),
+                genPackageChangedIntent(CALLING_PACKAGE_1, USER_10));
+
+        runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
+            assertWith(getCallerShortcuts())
+                    .haveIds("ms1-alt", "s2")
+                    .areAllEnabled()
+
+                    .selectPinned()
+                    .haveIds("ms1-alt", "s2")
+
+                    .revertToOriginalList()
+                    .selectByIds("ms1-alt", "s2")
+                    .areAllWithActivity(ACTIVITY2)
+            ;
+        });
+
+        // Re-enable activity 1.
+        // Manifest shortcuts will be re-published, but dynamic ones are not.
+        mEnabledActivityChecker = (activity, userId) -> true;
+        mService.mPackageMonitor.onReceive(getTestContext(),
+                genPackageChangedIntent(CALLING_PACKAGE_1, USER_10));
+
+        runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
+            assertWith(getCallerShortcuts())
+                    .haveIds("ms1", "ms1-alt", "s2")
+                    .areAllEnabled()
+
+                    .selectPinned()
+                    .haveIds("ms1-alt", "s2")
+
+                    .revertToOriginalList()
+                    .selectByIds("ms1")
+                    .areAllWithActivity(ACTIVITY1)
+
+                    .revertToOriginalList()
+                    .selectByIds("ms1-alt", "s2")
+                    .areAllWithActivity(ACTIVITY2)
+                    ;
+        });
+
+        // Disable activity 2
+        // Because "ms1-alt" and "s2" are both pinned, they will remain, but disabled.
+        mEnabledActivityChecker = (activity, userId) -> !ACTIVITY2.equals(activity);
+        mService.mPackageMonitor.onReceive(getTestContext(),
+                genPackageChangedIntent(CALLING_PACKAGE_1, USER_10));
+
+        runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
+            assertWith(getCallerShortcuts())
+                    .haveIds("ms1", "ms1-alt", "s2")
+
+                    .selectDynamic().isEmpty().revertToOriginalList() // no dynamics.
+
+                    .selectPinned()
+                    .haveIds("ms1-alt", "s2")
+                    .areAllDisabled()
+
+                    .revertToOriginalList()
+                    .selectByIds("ms1")
+                    .areAllWithActivity(ACTIVITY1)
+                    .areAllEnabled()
+            ;
+        });
+    }
+
+    public void testHandlePackageUpdate_activityNoLongerMain() throws Throwable {
+        runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+            assertTrue(mManager.setDynamicShortcuts(list(
+                    makeShortcutWithActivity("s1a",
+                            new ComponentName(getCallingPackage(), "act1")),
+                    makeShortcutWithActivity("s1b",
+                            new ComponentName(getCallingPackage(), "act1")),
+                    makeShortcutWithActivity("s2a",
+                            new ComponentName(getCallingPackage(), "act2")),
+                    makeShortcutWithActivity("s2b",
+                            new ComponentName(getCallingPackage(), "act2")),
+                    makeShortcutWithActivity("s3a",
+                            new ComponentName(getCallingPackage(), "act3")),
+                    makeShortcutWithActivity("s3b",
+                            new ComponentName(getCallingPackage(), "act3"))
+            )));
+            assertWith(getCallerShortcuts())
+                    .haveIds("s1a", "s1b", "s2a", "s2b", "s3a", "s3b")
+                    .areAllDynamic();
+        });
+        runWithCaller(LAUNCHER_1, USER_0, () -> {
+            mLauncherApps.pinShortcuts(CALLING_PACKAGE_1,
+                    list("s1b", "s2b", "s3b"),
+                    HANDLE_USER_0);
+        });
+        runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+            assertWith(getCallerShortcuts())
+                    .haveIds("s1a", "s1b", "s2a", "s2b", "s3a", "s3b")
+                    .areAllDynamic()
+
+                    .selectByIds("s1b", "s2b", "s3b")
+                    .areAllPinned();
+        });
+
+        // Update the app and act2 and act3 are no longer main.
+        mMainActivityChecker = (activity, userId) -> {
+            return activity.getClassName().equals("act1");
+        };
+
+        setCaller(LAUNCHER_1, USER_0);
+        assertForLauncherCallback(mLauncherApps, () -> {
+            updatePackageVersion(CALLING_PACKAGE_1, 1);
+            mService.mPackageMonitor.onReceive(getTestContext(),
+                    genPackageUpdateIntent(CALLING_PACKAGE_1, USER_0));
+        }).assertCallbackCalledForPackageAndUser(CALLING_PACKAGE_1, HANDLE_USER_0)
+                // Make sure the launcher gets callbacks.
+                .haveIds("s1a", "s1b", "s2b", "s3b")
+                .areAllWithKeyFieldsOnly();
+
+        runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+            // s2a and s3a are gone, but s2b and s3b will remain because they're pinned, and
+            // disabled.
+            assertWith(getCallerShortcuts())
+                    .haveIds("s1a", "s1b", "s2b", "s3b")
+
+                    .selectByIds("s1a", "s1b")
+                    .areAllDynamic()
+                    .areAllEnabled()
+
+                    .revertToOriginalList()
+                    .selectByIds("s2b", "s3b")
+                    .areAllNotDynamic()
+                    .areAllDisabled()
+                    .areAllPinned()
+                    ;
+        });
+    }
+
     protected void prepareForBackupTest() {
 
         prepareCrossProfileDataSet();
diff --git a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest2.java b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest2.java
index 2856866..f570ff2 100644
--- a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest2.java
+++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest2.java
@@ -72,18 +72,68 @@
                 "ID must be provided",
                 () -> new ShortcutInfo.Builder(getTestContext()).build());
 
-        assertExpectException(NullPointerException.class, "Intent action must be set",
-                () -> new ShortcutInfo.Builder(getTestContext()).setIntent(new Intent()));
-
-        assertExpectException(NullPointerException.class, "Activity must be provided", () -> {
-            ShortcutInfo si = new ShortcutInfo.Builder(getTestContext()).setId("id").build();
-            assertTrue(getManager().setDynamicShortcuts(list(si)));
-        });
+        assertExpectException(
+                RuntimeException.class,
+                "id cannot be empty",
+                () -> new ShortcutInfo.Builder(getTestContext(), null));
 
         assertExpectException(
+                RuntimeException.class,
+                "id cannot be empty",
+                () -> new ShortcutInfo.Builder(getTestContext(), ""));
+
+        assertExpectException(
+                RuntimeException.class,
+                "intent cannot be null",
+                () -> new ShortcutInfo.Builder(getTestContext(), "id").setIntent(null));
+
+        assertExpectException(
+                RuntimeException.class,
+                "action must be set",
+                () -> new ShortcutInfo.Builder(getTestContext(), "id").setIntent(new Intent()));
+
+        assertExpectException(
+                RuntimeException.class,
+                "activity cannot be null",
+                () -> new ShortcutInfo.Builder(getTestContext(), "id").setActivity(null));
+
+        assertExpectException(
+                RuntimeException.class,
+                "shortLabel cannot be empty",
+                () -> new ShortcutInfo.Builder(getTestContext(), "id").setShortLabel(null));
+
+        assertExpectException(
+                RuntimeException.class,
+                "shortLabel cannot be empty",
+                () -> new ShortcutInfo.Builder(getTestContext(), "id").setShortLabel(""));
+
+        assertExpectException(
+                RuntimeException.class,
+                "longLabel cannot be empty",
+                () -> new ShortcutInfo.Builder(getTestContext(), "id").setLongLabel(null));
+
+        assertExpectException(
+                RuntimeException.class,
+                "longLabel cannot be empty",
+                () -> new ShortcutInfo.Builder(getTestContext(), "id").setLongLabel(""));
+
+        assertExpectException(
+                RuntimeException.class,
+                "disabledMessage cannot be empty",
+                () -> new ShortcutInfo.Builder(getTestContext(), "id").setDisabledMessage(null));
+
+        assertExpectException(
+                RuntimeException.class,
+                "disabledMessage cannot be empty",
+                () -> new ShortcutInfo.Builder(getTestContext(), "id").setDisabledMessage(""));
+
+        assertExpectException(NullPointerException.class, "action must be set",
+                () -> new ShortcutInfo.Builder(getTestContext(), "id").setIntent(new Intent()));
+
+        // same for add.
+        assertExpectException(
                 IllegalArgumentException.class, "Short label must be provided", () -> {
-            ShortcutInfo si = new ShortcutInfo.Builder(getTestContext())
-                    .setId("id")
+            ShortcutInfo si = new ShortcutInfo.Builder(getTestContext(), "id")
                     .setActivity(new ComponentName(getTestContext().getPackageName(), "s"))
                     .build();
             assertTrue(getManager().setDynamicShortcuts(list(si)));
@@ -91,25 +141,24 @@
 
         assertExpectException(
                 IllegalArgumentException.class, "Short label must be provided", () -> {
-            ShortcutInfo si = new ShortcutInfo.Builder(getTestContext())
-                    .setId("id")
+            ShortcutInfo si = new ShortcutInfo.Builder(getTestContext(), "id")
                     .setActivity(new ComponentName(getTestContext().getPackageName(), "s"))
                     .build();
             assertTrue(getManager().addDynamicShortcuts(list(si)));
         });
 
+        // same for add.
         assertExpectException(NullPointerException.class, "Intent must be provided", () -> {
-            ShortcutInfo si = new ShortcutInfo.Builder(getTestContext())
-                    .setId("id")
+            ShortcutInfo si = new ShortcutInfo.Builder(getTestContext(), "id")
                     .setActivity(new ComponentName(getTestContext().getPackageName(), "s"))
                     .setShortLabel("x")
                     .build();
             assertTrue(getManager().setDynamicShortcuts(list(si)));
         });
 
+        // same for add.
         assertExpectException(NullPointerException.class, "Intent must be provided", () -> {
-            ShortcutInfo si = new ShortcutInfo.Builder(getTestContext())
-                    .setId("id")
+            ShortcutInfo si = new ShortcutInfo.Builder(getTestContext(), "id")
                     .setActivity(new ComponentName(getTestContext().getPackageName(), "s"))
                     .setShortLabel("x")
                     .build();
@@ -117,18 +166,17 @@
         });
 
         assertExpectException(
-                IllegalStateException.class, "package name mismatch", () -> {
-            ShortcutInfo si = new ShortcutInfo.Builder(getTestContext())
-                    .setId("id")
+                IllegalStateException.class, "does not belong to package", () -> {
+            ShortcutInfo si = new ShortcutInfo.Builder(getTestContext(), "id")
                     .setActivity(new ComponentName("xxx", "s"))
                     .build();
             assertTrue(getManager().setDynamicShortcuts(list(si)));
         });
 
+        // same for add.
         assertExpectException(
-                IllegalStateException.class, "package name mismatch", () -> {
-            ShortcutInfo si = new ShortcutInfo.Builder(getTestContext())
-                    .setId("id")
+                IllegalStateException.class, "does not belong to package", () -> {
+            ShortcutInfo si = new ShortcutInfo.Builder(getTestContext(), "id")
                     .setActivity(new ComponentName("xxx", "s"))
                     .build();
             assertTrue(getManager().addDynamicShortcuts(list(si)));
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 4aa7590..7d7285a 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
@@ -24,9 +24,11 @@
 import static junit.framework.Assert.fail;
 
 import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyInt;
 import static org.mockito.Matchers.anyList;
 import static org.mockito.Matchers.anyString;
 import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
@@ -35,11 +37,14 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.pm.LauncherApps;
+import android.content.pm.LauncherApps.Callback;
 import android.content.pm.ShortcutInfo;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
 import android.os.BaseBundle;
 import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
 import android.os.Parcel;
 import android.os.ParcelFileDescriptor;
 import android.os.PersistableBundle;
@@ -54,6 +59,7 @@
 import org.hamcrest.BaseMatcher;
 import org.hamcrest.Description;
 import org.hamcrest.Matcher;
+import org.mockito.ArgumentCaptor;
 import org.mockito.Mockito;
 
 import java.io.BufferedReader;
@@ -69,6 +75,7 @@
 import java.util.Set;
 import java.util.SortedSet;
 import java.util.TreeSet;
+import java.util.concurrent.CountDownLatch;
 import java.util.function.BooleanSupplier;
 import java.util.function.Consumer;
 import java.util.function.Function;
@@ -664,8 +671,8 @@
         }
 
         private ShortcutListAsserter(ShortcutListAsserter original, List<ShortcutInfo> list) {
-            mOriginal = original == null ? this : original;
-            mList = new ArrayList<>(list);
+            mOriginal = (original == null) ? this : original;
+            mList = (list == null) ? new ArrayList<>(0) : new ArrayList<>(list);
         }
 
         public ShortcutListAsserter revertToOriginalList() {
@@ -813,6 +820,11 @@
             return this;
         }
 
+        public ShortcutListAsserter areAllWithActivity(ComponentName activity) {
+            forAllShortcuts(s -> assertTrue("id=" + s.getId(), s.getActivity().equals(activity)));
+            return this;
+        }
+
         public ShortcutListAsserter forAllShortcuts(Consumer<ShortcutInfo> sa) {
             boolean found = false;
             for (int i = 0; i < mList.size(); i++) {
@@ -902,4 +914,69 @@
             }
         }
     }
+
+    public static void waitOnMainThread() throws InterruptedException {
+        final CountDownLatch latch = new CountDownLatch(1);
+
+        new Handler(Looper.getMainLooper()).post(() -> latch.countDown());
+
+        latch.await();
+    }
+
+    public static class LauncherCallbackAsserter {
+        private final LauncherApps.Callback mCallback = mock(LauncherApps.Callback.class);
+
+        private Callback getMockCallback() {
+            return mCallback;
+        }
+
+        public LauncherCallbackAsserter assertNoCallbackCalled() {
+            verify(mCallback, times(0)).onShortcutsChanged(
+                    anyString(),
+                    any(List.class),
+                    any(UserHandle.class));
+            return this;
+        }
+
+        public LauncherCallbackAsserter assertNoCallbackCalledForPackage(
+                String publisherPackageName) {
+            verify(mCallback, times(0)).onShortcutsChanged(
+                    eq(publisherPackageName),
+                    any(List.class),
+                    any(UserHandle.class));
+            return this;
+        }
+
+        public LauncherCallbackAsserter assertNoCallbackCalledForPackageAndUser(
+                String publisherPackageName, UserHandle publisherUserHandle) {
+            verify(mCallback, times(0)).onShortcutsChanged(
+                    eq(publisherPackageName),
+                    any(List.class),
+                    eq(publisherUserHandle));
+            return this;
+        }
+
+        public ShortcutListAsserter assertCallbackCalledForPackageAndUser(
+                String publisherPackageName, UserHandle publisherUserHandle) {
+            final ArgumentCaptor<List> shortcuts = ArgumentCaptor.forClass(List.class);
+            verify(mCallback, times(1)).onShortcutsChanged(
+                    eq(publisherPackageName),
+                    shortcuts.capture(),
+                    eq(publisherUserHandle));
+            return new ShortcutListAsserter(shortcuts.getValue());
+        }
+    }
+
+    public static LauncherCallbackAsserter assertForLauncherCallback(
+            LauncherApps launcherApps, Runnable body) throws InterruptedException {
+        final LauncherCallbackAsserter asserter = new LauncherCallbackAsserter();
+        launcherApps.registerCallback(asserter.getMockCallback(),
+                new Handler(Looper.getMainLooper()));
+
+        body.run();
+
+        waitOnMainThread();
+
+        return asserter;
+    }
 }