Merge "When app is updated, save the new version code, and update shortcuts with resource based icons." into nyc-dev
diff --git a/services/core/java/com/android/server/pm/ShortcutPackage.java b/services/core/java/com/android/server/pm/ShortcutPackage.java
index 8e96a58..d7f8cc6 100644
--- a/services/core/java/com/android/server/pm/ShortcutPackage.java
+++ b/services/core/java/com/android/server/pm/ShortcutPackage.java
@@ -109,6 +109,27 @@
         return getPackageUserId();
     }
 
+    /**
+     * Called when a shortcut is about to be published.  At this point we know the publisher package
+     * exists (as opposed to Launcher trying to fetch shortcuts from a non-existent package), so
+     * we do some initialization for the package.
+     */
+    private void onShortcutPublish(ShortcutService s) {
+        // Make sure we have the version code for the app.  We need the version code in
+        // handlePackageUpdated().
+        if (getPackageInfo().getVersionCode() < 0) {
+            final int versionCode = s.getApplicationVersionCode(getPackageName(), getOwnerUserId());
+            if (ShortcutService.DEBUG) {
+                Slog.d(TAG, String.format("Package %s version = %d", getPackageName(),
+                        versionCode));
+            }
+            if (versionCode >= 0) {
+                getPackageInfo().setVersionCode(versionCode);
+                s.scheduleSaveUser(getOwnerUserId());
+            }
+        }
+    }
+
     @Override
     protected void onRestoreBlocked(ShortcutService s) {
         // Can't restore due to version/signature mismatch.  Remove all shortcuts.
@@ -153,6 +174,9 @@
      */
     public void addDynamicShortcut(@NonNull ShortcutService s,
             @NonNull ShortcutInfo newShortcut) {
+
+        onShortcutPublish(s);
+
         newShortcut.addFlags(ShortcutInfo.FLAG_DYNAMIC);
 
         final ShortcutInfo oldShortcut = mShortcuts.get(newShortcut.getId());
@@ -387,6 +411,40 @@
         mApiCallCount = 0;
     }
 
+    /**
+     * Called when the package is updated.  If there are shortcuts with resource icons, update
+     * their timestamps.
+     */
+    public void handlePackageUpdated(ShortcutService s, int newVersionCode) {
+        if (getPackageInfo().getVersionCode() >= newVersionCode) {
+            // Version hasn't changed; nothing to do.
+            return;
+        }
+        if (ShortcutService.DEBUG) {
+            Slog.d(TAG, String.format("Package %s updated, version %d -> %d", getPackageName(),
+                    getPackageInfo().getVersionCode(), newVersionCode));
+        }
+
+        getPackageInfo().setVersionCode(newVersionCode);
+
+        boolean changed = false;
+        for (int i = mShortcuts.size() - 1; i >= 0; i--) {
+            final ShortcutInfo si = mShortcuts.valueAt(i);
+
+            if (si.hasIconResource()) {
+                changed = true;
+                si.setTimestamp(s.injectCurrentTimeMillis());
+            }
+        }
+        if (changed) {
+            // This will send a notification to the launcher, and also save .
+            s.packageShortcutsChanged(getPackageName(), getPackageUserId());
+        } else {
+            // Still save the version code.
+            s.scheduleSaveUser(getPackageUserId());
+        }
+    }
+
     public void dump(@NonNull ShortcutService s, @NonNull PrintWriter pw, @NonNull String prefix) {
         pw.println();
 
@@ -413,17 +471,20 @@
         getPackageInfo().dump(s, pw, prefix + "  ");
         pw.println();
 
-        pw.println("      Shortcuts:");
+        pw.print(prefix);
+        pw.println("  Shortcuts:");
         long totalBitmapSize = 0;
         final ArrayMap<String, ShortcutInfo> shortcuts = mShortcuts;
         final int size = shortcuts.size();
         for (int i = 0; i < size; i++) {
             final ShortcutInfo si = shortcuts.valueAt(i);
-            pw.print("        ");
+            pw.print(prefix);
+            pw.print("    ");
             pw.println(si.toInsecureString());
             if (si.getBitmapPath() != null) {
                 final long len = new File(si.getBitmapPath()).length();
-                pw.print("          ");
+                pw.print(prefix);
+                pw.print("      ");
                 pw.print("bitmap size=");
                 pw.println(len);
 
diff --git a/services/core/java/com/android/server/pm/ShortcutPackageInfo.java b/services/core/java/com/android/server/pm/ShortcutPackageInfo.java
index 2c45890..74969f0 100644
--- a/services/core/java/com/android/server/pm/ShortcutPackageInfo.java
+++ b/services/core/java/com/android/server/pm/ShortcutPackageInfo.java
@@ -45,12 +45,14 @@
     private static final String TAG_SIGNATURE = "signature";
     private static final String ATTR_SIGNATURE_HASH = "hash";
 
+    private static final int VERSION_UNKNOWN = -1;
+
     /**
      * When true, this package information was restored from the previous device, and the app hasn't
      * been installed yet.
      */
     private boolean mIsShadow;
-    private int mVersionCode;
+    private int mVersionCode = VERSION_UNKNOWN;
     private ArrayList<byte[]> mSigHashes;
 
     private ShortcutPackageInfo(int versionCode, ArrayList<byte[]> sigHashes, boolean isShadow) {
@@ -60,7 +62,7 @@
     }
 
     public static ShortcutPackageInfo newEmpty() {
-        return new ShortcutPackageInfo(0, new ArrayList<>(0), /* isShadow */ false);
+        return new ShortcutPackageInfo(VERSION_UNKNOWN, new ArrayList<>(0), /* isShadow */ false);
     }
 
     public boolean isShadow() {
@@ -75,6 +77,10 @@
         return mVersionCode;
     }
 
+    public void setVersionCode(int versionCode) {
+        mVersionCode = versionCode;
+    }
+
     public boolean hasSignatures() {
         return mSigHashes.size() > 0;
     }
diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java
index 0ac5c1f..5764161 100644
--- a/services/core/java/com/android/server/pm/ShortcutService.java
+++ b/services/core/java/com/android/server/pm/ShortcutService.java
@@ -356,7 +356,7 @@
             // Preload
             getUserShortcutsLocked(userId);
 
-            cleanupGonePackages(userId);
+            checkPackageChanges(userId);
         }
     }
 
@@ -1158,7 +1158,11 @@
      * - Sends a notification to LauncherApps
      * - Write to file
      */
-    private void userPackageChanged(@NonNull String packageName, @UserIdInt int userId) {
+    void packageShortcutsChanged(@NonNull String packageName, @UserIdInt int userId) {
+        if (DEBUG) {
+            Slog.d(TAG, String.format(
+                    "Shortcut changes: package=%s, user=%d", packageName, userId));
+        }
         notifyListeners(packageName, userId);
         scheduleSaveUser(userId);
     }
@@ -1284,7 +1288,7 @@
                 ps.addDynamicShortcut(this, newShortcut);
             }
         }
-        userPackageChanged(packageName, userId);
+        packageShortcutsChanged(packageName, userId);
         return true;
     }
 
@@ -1323,7 +1327,7 @@
                 }
             }
         }
-        userPackageChanged(packageName, userId);
+        packageShortcutsChanged(packageName, userId);
 
         return true;
     }
@@ -1353,7 +1357,7 @@
                 ps.addDynamicShortcut(this, newShortcut);
             }
         }
-        userPackageChanged(packageName, userId);
+        packageShortcutsChanged(packageName, userId);
 
         return true;
     }
@@ -1370,7 +1374,7 @@
                         Preconditions.checkStringNotEmpty((String) shortcutIds.get(i)));
             }
         }
-        userPackageChanged(packageName, userId);
+        packageShortcutsChanged(packageName, userId);
     }
 
     @Override
@@ -1380,7 +1384,7 @@
         synchronized (mLock) {
             getPackageShortcutsLocked(packageName, userId).deleteAllDynamicShortcuts(this);
         }
-        userPackageChanged(packageName, userId);
+        packageShortcutsChanged(packageName, userId);
     }
 
     @Override
@@ -1729,7 +1733,7 @@
                 launcher.pinShortcuts(
                         ShortcutService.this, userId, packageName, shortcutIds);
             }
-            userPackageChanged(packageName, userId);
+            packageShortcutsChanged(packageName, userId);
         }
 
         @Override
@@ -1841,13 +1845,15 @@
     };
 
     /**
-     * Called when a user is unlocked.  Check all known packages still exist, and otherwise
-     * perform cleanup.
+     * 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.
      */
     @VisibleForTesting
-    void cleanupGonePackages(@UserIdInt int ownerUserId) {
+    void checkPackageChanges(@UserIdInt int ownerUserId) {
         if (DEBUG) {
-            Slog.d(TAG, "cleanupGonePackages() ownerUserId=" + ownerUserId);
+            Slog.d(TAG, "checkPackageChanges() ownerUserId=" + ownerUserId);
         }
         final ArrayList<PackageWithUser> gonePackages = new ArrayList<>();
 
@@ -1858,10 +1864,15 @@
                 if (spi.getPackageInfo().isShadow()) {
                     return; // Don't delete shadow information.
                 }
-                if (isPackageInstalled(spi.getPackageName(), spi.getPackageUserId())) {
-                    return; // Package not gone.
+                final int versionCode = getApplicationVersionCode(
+                        spi.getPackageName(), spi.getPackageUserId());
+                if (versionCode >= 0) {
+                    // Package still installed, see if it's updated.
+                    getUserShortcutsLocked(ownerUserId).handlePackageUpdated(
+                            this, spi.getPackageName(), versionCode);
+                } else {
+                    gonePackages.add(PackageWithUser.of(spi));
                 }
-                gonePackages.add(PackageWithUser.of(spi));
             });
             if (gonePackages.size() > 0) {
                 for (int i = gonePackages.size() - 1; i >= 0; i--) {
@@ -1890,6 +1901,12 @@
         synchronized (mLock) {
             forEachLoadedUserLocked(user ->
                     user.attemptToRestoreIfNeededAndSave(this, packageName, userId));
+
+            final int versionCode = getApplicationVersionCode(packageName, userId);
+            if (versionCode < 0) {
+                return; // shouldn't happen
+            }
+            getUserShortcutsLocked(userId).handlePackageUpdated(this, packageName, versionCode);
         }
     }
 
@@ -1978,6 +1995,17 @@
         return isApplicationFlagSet(packageName, userId, ApplicationInfo.FLAG_INSTALLED);
     }
 
+    /**
+     * @return the version code of the package, or -1 if the app is not installed.
+     */
+    int getApplicationVersionCode(String packageName, int userId) {
+        final ApplicationInfo ai = injectApplicationInfo(packageName, userId);
+        if ((ai == null) || ((ai.flags & ApplicationInfo.FLAG_INSTALLED) == 0)) {
+            return -1;
+        }
+        return ai.versionCode;
+    }
+
     // === Backup & restore ===
 
     boolean shouldBackupApp(String packageName, int userId) {
@@ -2100,7 +2128,7 @@
             pw.println(mResetInterval);
             pw.print("    maxUpdatesPerInterval: ");
             pw.println(mMaxUpdatesPerInterval);
-            pw.print("    maxDynamicShortcuts:");
+            pw.print("    maxDynamicShortcuts: ");
             pw.println(mMaxDynamicShortcuts);
             pw.println();
 
diff --git a/services/core/java/com/android/server/pm/ShortcutUser.java b/services/core/java/com/android/server/pm/ShortcutUser.java
index 0b8c3a2..3d2e2ec 100644
--- a/services/core/java/com/android/server/pm/ShortcutUser.java
+++ b/services/core/java/com/android/server/pm/ShortcutUser.java
@@ -176,6 +176,17 @@
         });
     }
 
+    /**
+     * Called when a package is updated.
+     */
+    public void handlePackageUpdated(ShortcutService s, @NonNull String packageName,
+            int newVersionCode) {
+        if (!mPackages.containsKey(packageName)) {
+            return;
+        }
+        getPackageShortcuts(s, packageName).handlePackageUpdated(s, newVersionCode);
+    }
+
     public void attemptToRestoreIfNeededAndSave(ShortcutService s, @NonNull String packageName,
             @UserIdInt int packageUserId) {
         forPackageItem(packageName, packageUserId, spi -> {
diff --git a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest.java
index 5387f31..bc43576 100644
--- a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest.java
@@ -36,6 +36,7 @@
 import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertDynamicOnly;
 import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertExpectException;
 import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.assertShortcutIds;
+import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.findShortcut;
 import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.hashSet;
 import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.list;
 import static com.android.server.pm.shortcutmanagertest.ShortcutManagerTestUtils.makeBundle;
@@ -644,6 +645,7 @@
         pi.applicationInfo.flags = ApplicationInfo.FLAG_INSTALLED
                 | ApplicationInfo.FLAG_ALLOW_BACKUP;
         pi.versionCode = version;
+        pi.applicationInfo.versionCode = version;
         pi.signatures = genSignatures(signatures);
 
         return pi;
@@ -657,6 +659,13 @@
         c.accept(mInjectedPackages.get(packageName));
     }
 
+    private void updatePackageVersion(String packageName, int increment) {
+        updatePackageInfo(packageName, pi -> {
+            pi.versionCode += increment;
+            pi.applicationInfo.versionCode += increment;
+        });
+    }
+
     private void uninstallPackage(int userId, String packageName) {
         if (ENABLE_DUMP) {
             Log.i(TAG, "Unnstall package " + packageName + " / " + userId);
@@ -3839,7 +3848,7 @@
 
         // Start uninstalling.
         uninstallPackage(USER_10, LAUNCHER_1);
-        mService.cleanupGonePackages(USER_10);
+        mService.checkPackageChanges(USER_10);
 
         assertDynamicAndPinned(getPackageShortcut(CALLING_PACKAGE_1, "s1", USER_0));
         assertDynamicAndPinned(getPackageShortcut(CALLING_PACKAGE_1, "s2", USER_0));
@@ -3859,7 +3868,7 @@
 
         // Uninstall.
         uninstallPackage(USER_10, CALLING_PACKAGE_1);
-        mService.cleanupGonePackages(USER_10);
+        mService.checkPackageChanges(USER_10);
 
         assertDynamicAndPinned(getPackageShortcut(CALLING_PACKAGE_1, "s1", USER_0));
         assertDynamicAndPinned(getPackageShortcut(CALLING_PACKAGE_1, "s2", USER_0));
@@ -3878,7 +3887,7 @@
         assertNull(getPackageShortcut(CALLING_PACKAGE_1, "s3", USER_10));
 
         uninstallPackage(USER_P0, LAUNCHER_1);
-        mService.cleanupGonePackages(USER_0);
+        mService.checkPackageChanges(USER_0);
 
         assertDynamicAndPinned(getPackageShortcut(CALLING_PACKAGE_1, "s1", USER_0));
         assertDynamicOnly(getPackageShortcut(CALLING_PACKAGE_1, "s2", USER_0));
@@ -3896,7 +3905,7 @@
         assertNull(getPackageShortcut(CALLING_PACKAGE_1, "s2", USER_10));
         assertNull(getPackageShortcut(CALLING_PACKAGE_1, "s3", USER_10));
 
-        mService.cleanupGonePackages(USER_P0);
+        mService.checkPackageChanges(USER_P0);
         
         assertDynamicAndPinned(getPackageShortcut(CALLING_PACKAGE_1, "s1", USER_0));
         assertDynamicOnly(getPackageShortcut(CALLING_PACKAGE_1, "s2", USER_0));
@@ -4150,6 +4159,193 @@
         assertFalse(bitmapDirectoryExists(CALLING_PACKAGE_3, USER_10));
     }
 
+    public void testHandlePackageUpdate() throws Throwable {
+
+        // Set up shortcuts and launchers.
+
+        final Icon res32x32 = Icon.createWithResource(getTestContext(), R.drawable.black_32x32);
+        final Icon bmp32x32 = Icon.createWithBitmap(BitmapFactory.decodeResource(
+                getTestContext().getResources(), R.drawable.black_32x32));
+
+        runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
+            assertTrue(mManager.setDynamicShortcuts(list(
+                    makeShortcut("s1"),
+                    makeShortcutWithIcon("s2", res32x32),
+                    makeShortcutWithIcon("s3", res32x32),
+                    makeShortcutWithIcon("s4", bmp32x32))));
+        });
+        runWithCaller(CALLING_PACKAGE_2, USER_0, () -> {
+            assertTrue(mManager.setDynamicShortcuts(list(
+                    makeShortcut("s1"),
+                    makeShortcutWithIcon("s2", bmp32x32))));
+        });
+        runWithCaller(CALLING_PACKAGE_3, USER_0, () -> {
+            assertTrue(mManager.setDynamicShortcuts(list(
+                    makeShortcutWithIcon("s1", res32x32))));
+        });
+
+        runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
+            assertTrue(mManager.setDynamicShortcuts(list(
+                    makeShortcutWithIcon("s1", res32x32),
+                    makeShortcutWithIcon("s2", res32x32))));
+        });
+        runWithCaller(CALLING_PACKAGE_2, USER_10, () -> {
+            assertTrue(mManager.setDynamicShortcuts(list(
+                    makeShortcutWithIcon("s1", bmp32x32),
+                    makeShortcutWithIcon("s2", bmp32x32))));
+        });
+
+        LauncherApps.Callback c0 = mock(LauncherApps.Callback.class);
+        LauncherApps.Callback c10 = mock(LauncherApps.Callback.class);
+
+        runWithCaller(LAUNCHER_1, USER_0, () -> {
+            mLauncherApps.registerCallback(c0, new Handler(Looper.getMainLooper()));
+        });
+        runWithCaller(LAUNCHER_1, USER_10, () -> {
+            mLauncherApps.registerCallback(c10, new Handler(Looper.getMainLooper()));
+        });
+
+        mInjectedCurrentTimeLillis = START_TIME + 100;
+
+        ArgumentCaptor<List> shortcuts;
+
+        // First, call the event without updating the versions.
+        reset(c0);
+        reset(c10);
+
+        mService.mPackageMonitor.onReceive(getTestContext(),
+                genPackageUpdateIntent(CALLING_PACKAGE_1, USER_0));
+        mService.mPackageMonitor.onReceive(getTestContext(),
+                genPackageUpdateIntent(CALLING_PACKAGE_1, USER_10));
+
+        waitOnMainThread();
+
+        // Version not changed, so no callback.
+        verify(c0, times(0)).onShortcutsChanged(
+                eq(CALLING_PACKAGE_1),
+                any(List.class),
+                any(UserHandle.class));
+        verify(c10, times(0)).onShortcutsChanged(
+                eq(CALLING_PACKAGE_1),
+                any(List.class),
+                any(UserHandle.class));
+
+        // Next, update the version info for package 1.
+        reset(c0);
+        reset(c10);
+        updatePackageVersion(CALLING_PACKAGE_1, 1);
+
+        // Then send the broadcast, to only user-0.
+        mService.mPackageMonitor.onReceive(getTestContext(),
+                genPackageUpdateIntent(CALLING_PACKAGE_1, USER_0));
+
+        waitOnMainThread();
+
+        // User-0 should get the notification.
+        shortcuts = ArgumentCaptor.forClass(List.class);
+        verify(c0).onShortcutsChanged(
+                eq(CALLING_PACKAGE_1),
+                shortcuts.capture(),
+                eq(HANDLE_USER_0));
+
+        // User-10 shouldn't yet get the notification.
+        verify(c10, times(0)).onShortcutsChanged(
+                eq(CALLING_PACKAGE_1),
+                any(List.class),
+                any(UserHandle.class));
+        assertShortcutIds(shortcuts.getValue(), "s1", "s2", "s3", "s4");
+        assertEquals(START_TIME,
+                findShortcut(shortcuts.getValue(), "s1").getLastChangedTimestamp());
+        assertEquals(START_TIME + 100,
+                findShortcut(shortcuts.getValue(), "s2").getLastChangedTimestamp());
+        assertEquals(START_TIME + 100,
+                findShortcut(shortcuts.getValue(), "s3").getLastChangedTimestamp());
+        assertEquals(START_TIME,
+                findShortcut(shortcuts.getValue(), "s4").getLastChangedTimestamp());
+
+        // Next, send unlock even on user-10.  Now we scan packages on this user and send a
+        // notification to the launcher.
+        mInjectedCurrentTimeLillis = START_TIME + 200;
+
+        when(mMockUserManager.isUserRunning(eq(USER_10))).thenReturn(true);
+
+        reset(c0);
+        reset(c10);
+        mService.handleUnlockUser(USER_10);
+
+        shortcuts = ArgumentCaptor.forClass(List.class);
+        verify(c0, times(0)).onShortcutsChanged(
+                eq(CALLING_PACKAGE_1),
+                any(List.class),
+                any(UserHandle.class));
+
+        verify(c10).onShortcutsChanged(
+                eq(CALLING_PACKAGE_1),
+                shortcuts.capture(),
+                eq(HANDLE_USER_10));
+
+        assertShortcutIds(shortcuts.getValue(), "s1", "s2");
+        assertEquals(START_TIME + 200,
+                findShortcut(shortcuts.getValue(), "s1").getLastChangedTimestamp());
+        assertEquals(START_TIME + 200,
+                findShortcut(shortcuts.getValue(), "s2").getLastChangedTimestamp());
+
+
+        // Do the same thing for package 2, which doesn't have resource icons.
+        mInjectedCurrentTimeLillis = START_TIME + 300;
+
+        reset(c0);
+        reset(c10);
+        updatePackageVersion(CALLING_PACKAGE_2, 10);
+
+        // Then send the broadcast, to only user-0.
+        mService.mPackageMonitor.onReceive(getTestContext(),
+                genPackageUpdateIntent(CALLING_PACKAGE_2, USER_0));
+        mService.handleUnlockUser(USER_10);
+
+        waitOnMainThread();
+
+        verify(c0, times(0)).onShortcutsChanged(
+                eq(CALLING_PACKAGE_1),
+                any(List.class),
+                any(UserHandle.class));
+
+        verify(c10, times(0)).onShortcutsChanged(
+                eq(CALLING_PACKAGE_1),
+                any(List.class),
+                any(UserHandle.class));
+
+        // Do the same thing for package 3
+        mInjectedCurrentTimeLillis = START_TIME + 400;
+
+        reset(c0);
+        reset(c10);
+        updatePackageVersion(CALLING_PACKAGE_3, 100);
+
+        // Then send the broadcast, to only user-0.
+        mService.mPackageMonitor.onReceive(getTestContext(),
+                genPackageUpdateIntent(CALLING_PACKAGE_3, USER_0));
+        mService.handleUnlockUser(USER_10);
+
+        waitOnMainThread();
+
+        shortcuts = ArgumentCaptor.forClass(List.class);
+        verify(c0).onShortcutsChanged(
+                eq(CALLING_PACKAGE_3),
+                shortcuts.capture(),
+                eq(HANDLE_USER_0));
+
+        // User 10 doesn't have package 3, so no callback.
+        verify(c10, times(0)).onShortcutsChanged(
+                eq(CALLING_PACKAGE_3),
+                any(List.class),
+                any(UserHandle.class));
+
+        assertShortcutIds(shortcuts.getValue(), "s1");
+        assertEquals(START_TIME + 400,
+                findShortcut(shortcuts.getValue(), "s1").getLastChangedTimestamp());
+    }
+
     private void backupAndRestore() {
         int prevUid = mInjectedCallingUid;
 
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 d09b62c..ad49c2f 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
@@ -413,6 +413,16 @@
         }
     }
 
+    public static ShortcutInfo findShortcut(List<ShortcutInfo> list, String id) {
+        for (ShortcutInfo si : list) {
+            if (si.getId().equals(id)) {
+                return si;
+            }
+        }
+        fail("Shortcut " + id + " not found in the list");
+        return null;
+    }
+
     public static Bitmap pfdToBitmap(ParcelFileDescriptor pfd) {
         assertNotNull(pfd);
         try {