Merge "Offload saving bitmaps from binder threads" into oc-dev
diff --git a/core/java/android/content/pm/ShortcutInfo.java b/core/java/android/content/pm/ShortcutInfo.java
index d0c6397..d3a3560 100644
--- a/core/java/android/content/pm/ShortcutInfo.java
+++ b/core/java/android/content/pm/ShortcutInfo.java
@@ -97,6 +97,9 @@
     /** @hide */
     public static final int FLAG_RETURNED_BY_SERVICE = 1 << 10;
 
+    /** @hide When this is set, the bitmap icon is waiting to be saved. */
+    public static final int FLAG_ICON_FILE_PENDING_SAVE = 1 << 11;
+
     /** @hide */
     @IntDef(flag = true,
             value = {
@@ -110,7 +113,8 @@
             FLAG_STRINGS_RESOLVED,
             FLAG_IMMUTABLE,
             FLAG_ADAPTIVE_BITMAP,
-            FLAG_RETURNED_BY_SERVICE
+            FLAG_RETURNED_BY_SERVICE,
+            FLAG_ICON_FILE_PENDING_SAVE,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface ShortcutFlags {}
@@ -1471,6 +1475,21 @@
         return hasFlags(FLAG_ADAPTIVE_BITMAP);
     }
 
+    /** @hide */
+    public boolean isIconPendingSave() {
+        return hasFlags(FLAG_ICON_FILE_PENDING_SAVE);
+    }
+
+    /** @hide */
+    public void setIconPendingSave() {
+        addFlags(FLAG_ICON_FILE_PENDING_SAVE);
+    }
+
+    /** @hide */
+    public void clearIconPendingSave() {
+        clearFlags(FLAG_ICON_FILE_PENDING_SAVE);
+    }
+
     /**
      * Return whether a shortcut only contains "key" information only or not.  If true, only the
      * following fields are available.
@@ -1534,7 +1553,12 @@
         return mIconResId;
     }
 
-    /** @hide */
+    /**
+     * Bitmap path.  Note this will be null even if {@link #hasIconFile()} is set when the save
+     * is pending.  Use {@link #isIconPendingSave()} to check it.
+     *
+     * @hide
+     */
     public String getBitmapPath() {
         return mBitmapPath;
     }
@@ -1780,6 +1804,9 @@
         if (hasIconFile()) {
             sb.append("If");
         }
+        if (isIconPendingSave()) {
+            sb.append("^");
+        }
         if (hasIconResource()) {
             sb.append("Ir");
         }
diff --git a/services/core/java/com/android/server/pm/ShortcutBitmapSaver.java b/services/core/java/com/android/server/pm/ShortcutBitmapSaver.java
new file mode 100644
index 0000000..4f5d156
--- /dev/null
+++ b/services/core/java/com/android/server/pm/ShortcutBitmapSaver.java
@@ -0,0 +1,311 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.pm;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.pm.ShortcutInfo;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.CompressFormat;
+import android.graphics.drawable.Icon;
+import android.os.SystemClock;
+import android.util.Log;
+import android.util.Slog;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.Preconditions;
+import com.android.server.pm.ShortcutService.FileOutputStreamWithPath;
+
+import libcore.io.IoUtils;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.Deque;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.concurrent.LinkedBlockingDeque;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Class to save shortcut bitmaps on a worker thread.
+ *
+ * The methods with the "Locked" prefix must be called with the service lock held.
+ */
+public class ShortcutBitmapSaver {
+    private static final String TAG = ShortcutService.TAG;
+    private static final boolean DEBUG = ShortcutService.DEBUG;
+
+    private static final boolean ADD_DELAY_BEFORE_SAVE_FOR_TEST = false; // DO NOT submit with true.
+    private static final long SAVE_DELAY_MS_FOR_TEST = 1000; // DO NOT submit with true.
+
+    /**
+     * Before saving shortcuts.xml, and returning icons to the launcher, we wait for all pending
+     * saves to finish.  However if it takes more than this long, we just give up and proceed.
+     */
+    private final long SAVE_WAIT_TIMEOUT_MS = 30 * 1000;
+
+    private final ShortcutService mService;
+
+    /**
+     * Bitmaps are saved on this thread.
+     *
+     * Note: Just before saving shortcuts into the XML, we need to wait on all pending saves to
+     * finish, and we need to do it with the service lock held, which would still block incoming
+     * binder calls, meaning saving bitmaps *will* still actually block API calls too, which is
+     * not ideal but fixing it would be tricky, so this is still a known issue on the current
+     * version.
+     *
+     * In order to reduce the conflict, we use an own thread for this purpose, rather than
+     * reusing existing background threads, and also to avoid possible deadlocks.
+     */
+    private final Executor mExecutor = new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS,
+            new LinkedBlockingQueue<>());
+
+    /** Represents a bitmap to save. */
+    private static class PendingItem {
+        /** Hosting shortcut. */
+        public final ShortcutInfo shortcut;
+
+        /** Compressed bitmap data. */
+        public final byte[] bytes;
+
+        /** Instantiated time, only for dogfooding. */
+        private final long mInstantiatedUptimeMillis; // Only for dumpsys.
+
+        private PendingItem(ShortcutInfo shortcut, byte[] bytes) {
+            this.shortcut = shortcut;
+            this.bytes = bytes;
+            mInstantiatedUptimeMillis = SystemClock.uptimeMillis();
+        }
+
+        @Override
+        public String toString() {
+            return "PendingItem{size=" + bytes.length
+                    + " age=" + (SystemClock.uptimeMillis() - mInstantiatedUptimeMillis) + "ms"
+                    + " shortcut=" + shortcut.toInsecureString()
+                    + "}";
+        }
+    }
+
+    @GuardedBy("mPendingItems")
+    private final Deque<PendingItem> mPendingItems = new LinkedBlockingDeque<>();
+
+    public ShortcutBitmapSaver(ShortcutService service) {
+        mService = service;
+        // mLock = lock;
+    }
+
+    public boolean waitForAllSavesLocked() {
+        final CountDownLatch latch = new CountDownLatch(1);
+
+        mExecutor.execute(() -> latch.countDown());
+
+        try {
+            if (latch.await(SAVE_WAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
+                return true;
+            }
+            mService.wtf("Timed out waiting on saving bitmaps.");
+        } catch (InterruptedException e) {
+            Slog.w(TAG, "interrupted");
+        }
+        return false;
+    }
+
+    /**
+     * Wait for all pending saves to finish, and then return the given shortcut's bitmap path.
+     */
+    @Nullable
+    public String getBitmapPathMayWaitLocked(ShortcutInfo shortcut) {
+        final boolean success = waitForAllSavesLocked();
+        if (success && shortcut.hasIconFile()) {
+            return shortcut.getBitmapPath();
+        } else {
+            return null;
+        }
+    }
+
+    public void removeIcon(ShortcutInfo shortcut) {
+        // Do not remove the actual bitmap file yet, because if the device crashes before saving
+        // the XML we'd lose the icon.  We just remove all dangling files after saving the XML.
+        shortcut.setIconResourceId(0);
+        shortcut.setIconResName(null);
+        shortcut.setBitmapPath(null);
+        shortcut.clearFlags(ShortcutInfo.FLAG_HAS_ICON_FILE |
+                ShortcutInfo.FLAG_ADAPTIVE_BITMAP | ShortcutInfo.FLAG_HAS_ICON_RES |
+                ShortcutInfo.FLAG_ICON_FILE_PENDING_SAVE);
+    }
+
+    public void saveBitmapLocked(ShortcutInfo shortcut,
+            int maxDimension, CompressFormat format, int quality) {
+        final Icon icon = shortcut.getIcon();
+        Preconditions.checkNotNull(icon);
+
+        final Bitmap original = icon.getBitmap();
+        if (original == null) {
+            Log.e(TAG, "Missing icon: " + shortcut);
+            return;
+        }
+
+        // Compress it and enqueue to the requests.
+        final byte[] bytes;
+        try {
+            final Bitmap shrunk = mService.shrinkBitmap(original, maxDimension);
+            try {
+                try (final ByteArrayOutputStream out = new ByteArrayOutputStream(64 * 1024)) {
+                    if (!shrunk.compress(format, quality, out)) {
+                        Slog.wtf(ShortcutService.TAG, "Unable to compress bitmap");
+                    }
+                    out.flush();
+                    bytes = out.toByteArray();
+                    out.close();
+                }
+            } finally {
+                if (shrunk != original) {
+                    shrunk.recycle();
+                }
+            }
+        } catch (IOException | RuntimeException | OutOfMemoryError e) {
+            Slog.wtf(ShortcutService.TAG, "Unable to write bitmap to file", e);
+            return;
+        }
+
+        shortcut.addFlags(
+                ShortcutInfo.FLAG_HAS_ICON_FILE | ShortcutInfo.FLAG_ICON_FILE_PENDING_SAVE);
+
+        if (icon.getType() == Icon.TYPE_ADAPTIVE_BITMAP) {
+            shortcut.addFlags(ShortcutInfo.FLAG_ADAPTIVE_BITMAP);
+        }
+
+        // Enqueue a pending save.
+        final PendingItem item = new PendingItem(shortcut, bytes);
+        synchronized (mPendingItems) {
+            mPendingItems.add(item);
+        }
+
+        if (DEBUG) {
+            Slog.d(TAG, "Scheduling to save: " + item);
+        }
+
+        mExecutor.execute(mRunnable);
+    }
+
+    private final Runnable mRunnable = () -> {
+        // Process all pending items.
+        while (processPendingItems()) {
+        }
+    };
+
+    /**
+     * Takes a {@link PendingItem} from {@link #mPendingItems} and process it.
+     *
+     * Must be called {@link #mExecutor}.
+     *
+     * @return true if it processed an item, false if the queue is empty.
+     */
+    private boolean processPendingItems() {
+        if (ADD_DELAY_BEFORE_SAVE_FOR_TEST) {
+            Slog.w(TAG, "*** ARTIFICIAL SLEEP ***");
+            try {
+                Thread.sleep(SAVE_DELAY_MS_FOR_TEST);
+            } catch (InterruptedException e) {
+            }
+        }
+
+        // NOTE:
+        // Ideally we should be holding the service lock when accessing shortcut instances,
+        // but that could cause a deadlock so we don't do it.
+        //
+        // Instead, waitForAllSavesLocked() uses a latch to make sure changes made on this
+        // thread is visible on the caller thread.
+
+        ShortcutInfo shortcut = null;
+        try {
+            final PendingItem item;
+
+            synchronized (mPendingItems) {
+                if (mPendingItems.size() == 0) {
+                    return false;
+                }
+                item = mPendingItems.pop();
+            }
+
+            shortcut = item.shortcut;
+
+            // See if the shortcut is still relevant. (It might have been removed already.)
+            if (!shortcut.isIconPendingSave()) {
+                return true;
+            }
+
+            if (DEBUG) {
+                Slog.d(TAG, "Saving bitmap: " + item);
+            }
+
+            File file = null;
+            try {
+                final FileOutputStreamWithPath out = mService.openIconFileForWrite(
+                        shortcut.getUserId(), shortcut);
+                file = out.getFile();
+
+                try {
+                    out.write(item.bytes);
+                } finally {
+                    IoUtils.closeQuietly(out);
+                }
+
+                shortcut.setBitmapPath(file.getAbsolutePath());
+
+            } catch (IOException | RuntimeException e) {
+                Slog.e(ShortcutService.TAG, "Unable to write bitmap to file", e);
+
+                if (file != null && file.exists()) {
+                    file.delete();
+                }
+                return true;
+            }
+        } finally {
+            if (DEBUG) {
+                Slog.d(TAG, "Saved bitmap.");
+            }
+            if (shortcut != null) {
+                if (shortcut.getBitmapPath() == null) {
+                    removeIcon(shortcut);
+                }
+
+                // Whatever happened, remove this flag.
+                shortcut.clearFlags(ShortcutInfo.FLAG_ICON_FILE_PENDING_SAVE);
+            }
+        }
+        return true;
+    }
+
+    public void dumpLocked(@NonNull PrintWriter pw, @NonNull String prefix) {
+        synchronized (mPendingItems) {
+            final int N = mPendingItems.size();
+            pw.print(prefix);
+            pw.println("Pending saves: Num=" + N + " Executor=" + mExecutor);
+
+            for (PendingItem item : mPendingItems) {
+                pw.print(prefix);
+                pw.print("  ");
+                pw.println(item);
+            }
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/pm/ShortcutPackage.java b/services/core/java/com/android/server/pm/ShortcutPackage.java
index 5035e68..103b25d 100644
--- a/services/core/java/com/android/server/pm/ShortcutPackage.java
+++ b/services/core/java/com/android/server/pm/ShortcutPackage.java
@@ -198,7 +198,7 @@
     private ShortcutInfo deleteShortcutInner(@NonNull String id) {
         final ShortcutInfo shortcut = mShortcuts.remove(id);
         if (shortcut != null) {
-            mShortcutUser.mService.removeIcon(getPackageUserId(), shortcut);
+            mShortcutUser.mService.removeIconLocked(shortcut);
             shortcut.clearFlags(ShortcutInfo.FLAG_DYNAMIC | ShortcutInfo.FLAG_PINNED
                     | ShortcutInfo.FLAG_MANIFEST);
         }
@@ -211,7 +211,7 @@
         deleteShortcutInner(newShortcut.getId());
 
         // Extract Icon and update the icon res ID and the bitmap path.
-        s.saveIconAndFixUpShortcut(getPackageUserId(), newShortcut);
+        s.saveIconAndFixUpShortcutLocked(newShortcut);
         s.fixUpShortcutResourceNamesAndValues(newShortcut);
         mShortcuts.put(newShortcut.getId(), newShortcut);
     }
@@ -1263,13 +1263,21 @@
         out.endTag(null, TAG_ROOT);
     }
 
-    private static void saveShortcut(XmlSerializer out, ShortcutInfo si, boolean forBackup)
+    private void saveShortcut(XmlSerializer out, ShortcutInfo si, boolean forBackup)
             throws IOException, XmlPullParserException {
+
+        final ShortcutService s = mShortcutUser.mService;
+
         if (forBackup) {
             if (!(si.isPinned() && si.isEnabled())) {
                 return; // We only backup pinned shortcuts that are enabled.
             }
         }
+        // Note: at this point no shortcuts should have bitmaps pending save, but if they do,
+        // just remove the bitmap.
+        if (si.isIconPendingSave()) {
+            s.removeIconLocked(si);
+        }
         out.startTag(null, TAG_SHORTCUT);
         ShortcutService.writeAttr(out, ATTR_ID, si.getId());
         // writeAttr(out, "package", si.getPackageName()); // not needed
@@ -1293,6 +1301,7 @@
             ShortcutService.writeAttr(out, ATTR_FLAGS,
                     si.getFlags() &
                             ~(ShortcutInfo.FLAG_HAS_ICON_FILE | ShortcutInfo.FLAG_HAS_ICON_RES
+                            | ShortcutInfo.FLAG_ICON_FILE_PENDING_SAVE
                             | ShortcutInfo.FLAG_DYNAMIC));
         } else {
             // When writing for backup, ranks shouldn't be saved, since shortcuts won't be restored
diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java
index 7f0528a..ac4b828 100644
--- a/services/core/java/com/android/server/pm/ShortcutService.java
+++ b/services/core/java/com/android/server/pm/ShortcutService.java
@@ -306,6 +306,7 @@
     private final ActivityManagerInternal mActivityManagerInternal;
 
     private final ShortcutRequestPinProcessor mShortcutRequestPinProcessor;
+    private final ShortcutBitmapSaver mShortcutBitmapSaver;
 
     @GuardedBy("mLock")
     final SparseIntArray mUidState = new SparseIntArray();
@@ -426,6 +427,7 @@
                 LocalServices.getService(ActivityManagerInternal.class));
 
         mShortcutRequestPinProcessor = new ShortcutRequestPinProcessor(this, mLock);
+        mShortcutBitmapSaver = new ShortcutBitmapSaver(this);
 
         if (onlyForPackageManagerApis) {
             return; // Don't do anything further.  For unit tests only.
@@ -926,6 +928,9 @@
         if (DEBUG) {
             Slog.d(TAG, "Saving to " + path);
         }
+
+        mShortcutBitmapSaver.waitForAllSavesLocked();
+
         path.getParentFile().mkdirs();
         final AtomicFile file = new AtomicFile(path);
         FileOutputStream os = null;
@@ -1213,13 +1218,8 @@
 
     // === Caller validation ===
 
-    void removeIcon(@UserIdInt int userId, ShortcutInfo shortcut) {
-        // Do not remove the actual bitmap file yet, because if the device crashes before saving
-        // he XML we'd lose the icon.  We just remove all dangling files after saving the XML.
-        shortcut.setIconResourceId(0);
-        shortcut.setIconResName(null);
-        shortcut.clearFlags(ShortcutInfo.FLAG_HAS_ICON_FILE |
-            ShortcutInfo.FLAG_ADAPTIVE_BITMAP | ShortcutInfo.FLAG_HAS_ICON_RES);
+    void removeIconLocked(ShortcutInfo shortcut) {
+        mShortcutBitmapSaver.removeIcon(shortcut);
     }
 
     public void cleanupBitmapsForPackage(@UserIdInt int userId, String packageName) {
@@ -1232,6 +1232,13 @@
         }
     }
 
+    /**
+     * Remove dangling bitmap files for a user.
+     *
+     * Note this method must be called with the lock held after calling
+     * {@link ShortcutBitmapSaver#waitForAllSavesLocked()} to make sure there's no pending bitmap
+     * saves are going on.
+     */
     private void cleanupDanglingBitmapDirectoriesLocked(@UserIdInt int userId) {
         if (DEBUG) {
             Slog.d(TAG, "cleanupDanglingBitmaps: userId=" + userId);
@@ -1265,6 +1272,13 @@
         logDurationStat(Stats.CLEANUP_DANGLING_BITMAPS, start);
     }
 
+    /**
+     * Remove dangling bitmap files for a package.
+     *
+     * Note this method must be called with the lock held after calling
+     * {@link ShortcutBitmapSaver#waitForAllSavesLocked()} to make sure there's no pending bitmap
+     * saves are going on.
+     */
     private void cleanupDanglingBitmapFilesLocked(@UserIdInt int userId, @NonNull ShortcutUser user,
             @NonNull String packageName, @NonNull File path) {
         final ArraySet<String> usedFiles =
@@ -1303,7 +1317,6 @@
      *
      * The filename will be based on the ID, except certain characters will be escaped.
      */
-    @VisibleForTesting
     FileOutputStreamWithPath openIconFileForWrite(@UserIdInt int userId, ShortcutInfo shortcut)
             throws IOException {
         final File packagePath = new File(getUserBitmapFilePath(userId),
@@ -1329,7 +1342,7 @@
         }
     }
 
-    void saveIconAndFixUpShortcut(@UserIdInt int userId, ShortcutInfo shortcut) {
+    void saveIconAndFixUpShortcutLocked(ShortcutInfo shortcut) {
         if (shortcut.hasIconFile() || shortcut.hasIconResource()) {
             return;
         }
@@ -1337,7 +1350,7 @@
         final long token = injectClearCallingIdentity();
         try {
             // Clear icon info on the shortcut.
-            removeIcon(userId, shortcut);
+            removeIconLocked(shortcut);
 
             final Icon icon = shortcut.getIcon();
             if (icon == null) {
@@ -1364,41 +1377,8 @@
                         // just in case.
                         throw ShortcutInfo.getInvalidIconException();
                 }
-                if (bitmap == null) {
-                    Slog.e(TAG, "Null bitmap detected");
-                    return;
-                }
-                // Shrink and write to the file.
-                File path = null;
-                try {
-                    final FileOutputStreamWithPath out = openIconFileForWrite(userId, shortcut);
-                    try {
-                        path = out.getFile();
-
-                        Bitmap shrunk = shrinkBitmap(bitmap, mMaxIconDimension);
-                        try {
-                            shrunk.compress(mIconPersistFormat, mIconPersistQuality, out);
-                        } finally {
-                            if (bitmap != shrunk) {
-                                shrunk.recycle();
-                            }
-                        }
-
-                        shortcut.setBitmapPath(out.getFile().getAbsolutePath());
-                        shortcut.addFlags(ShortcutInfo.FLAG_HAS_ICON_FILE);
-                        if (icon.getType() == Icon.TYPE_ADAPTIVE_BITMAP) {
-                            shortcut.addFlags(ShortcutInfo.FLAG_ADAPTIVE_BITMAP);
-                        }
-                    } finally {
-                        IoUtils.closeQuietly(out);
-                    }
-                } 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()) {
-                        path.delete();
-                    }
-                }
+                mShortcutBitmapSaver.saveBitmapLocked(shortcut,
+                        mMaxIconDimension, mIconPersistFormat, mIconPersistQuality);
             } finally {
                 // Once saved, we won't use the original icon information, so null it out.
                 shortcut.clearIcon();
@@ -1418,7 +1398,6 @@
         }
     }
 
-    @VisibleForTesting
     static Bitmap shrinkBitmap(Bitmap in, int maxSize) {
         // Original width/height.
         final int ow = in.getWidth();
@@ -1787,7 +1766,7 @@
 
                 final boolean replacingIcon = (source.getIcon() != null);
                 if (replacingIcon) {
-                    removeIcon(userId, target);
+                    removeIconLocked(target);
                 }
 
                 // Note copyNonNullFieldsFrom() does the "updatable with?" check too.
@@ -1795,7 +1774,7 @@
                 target.setTimestamp(injectCurrentTimeMillis());
 
                 if (replacingIcon) {
-                    saveIconAndFixUpShortcut(userId, target);
+                    saveIconAndFixUpShortcutLocked(target);
                 }
 
                 // When we're updating any resource related fields, re-extract the res names and
@@ -2613,16 +2592,17 @@
                 if (shortcutInfo == null || !shortcutInfo.hasIconFile()) {
                     return null;
                 }
+                final String path = mShortcutBitmapSaver.getBitmapPathMayWaitLocked(shortcutInfo);
+                if (path == null) {
+                    Slog.w(TAG, "null bitmap detected in getShortcutIconFd()");
+                    return null;
+                }
                 try {
-                    if (shortcutInfo.getBitmapPath() == null) {
-                        Slog.w(TAG, "null bitmap detected in getShortcutIconFd()");
-                        return null;
-                    }
                     return ParcelFileDescriptor.open(
-                            new File(shortcutInfo.getBitmapPath()),
+                            new File(path),
                             ParcelFileDescriptor.MODE_READ_ONLY);
                 } catch (FileNotFoundException e) {
-                    Slog.e(TAG, "Icon file not found: " + shortcutInfo.getBitmapPath());
+                    Slog.e(TAG, "Icon file not found: " + path);
                     return null;
                 }
             }
@@ -3384,6 +3364,9 @@
             scheduleSaveUser(userId);
             saveDirtyInfo();
 
+            // Note, in case of backup, we don't have to wait on bitmap saving, because we don't
+            // back up bitmaps anyway.
+
             // Then create the backup payload.
             final ByteArrayOutputStream os = new ByteArrayOutputStream(32 * 1024);
             try {
@@ -3516,6 +3499,9 @@
                 pw.println(Log.getStackTraceString(mLastWtfStacktrace));
             }
 
+            pw.println();
+            mShortcutBitmapSaver.dumpLocked(pw, "  ");
+
             for (int i = 0; i < mUsers.size(); i++) {
                 pw.println();
                 mUsers.valueAt(i).dump(pw, "  ");
@@ -3827,6 +3813,11 @@
         return SystemClock.elapsedRealtime();
     }
 
+    @VisibleForTesting
+    long injectUptimeMillis() {
+        return SystemClock.uptimeMillis();
+    }
+
     // Injection point.
     @VisibleForTesting
     int injectBinderCallingUid() {
@@ -3997,4 +3988,11 @@
             forEachLoadedUserLocked(u -> u.forAllPackageItems(ShortcutPackageItem::verifyStates));
         }
     }
+
+    @VisibleForTesting
+    void waitForBitmapSavesForTest() {
+        synchronized (mLock) {
+            mShortcutBitmapSaver.waitForAllSavesLocked();
+        }
+    }
 }
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 d281e5a..f1d5927 100644
--- a/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java
@@ -279,6 +279,11 @@
         }
 
         @Override
+        long injectUptimeMillis() {
+            return mInjectedCurrentTimeMillis - START_TIME - mDeepSleepTime;
+        }
+
+        @Override
         int injectBinderCallingUid() {
             return mInjectedCallingUid;
         }
@@ -557,6 +562,7 @@
     protected boolean mSafeMode;
 
     protected long mInjectedCurrentTimeMillis;
+    protected long mDeepSleepTime; // Used to calculate "uptimeMillis".
 
     protected boolean mInjectedIsLowRamDevice;
 
@@ -1707,9 +1713,19 @@
         if (si == null) {
             return null;
         }
+        mService.waitForBitmapSavesForTest();
         return new File(si.getBitmapPath()).getName();
     }
 
+    protected String getBitmapAbsPath(int userId, String packageName, String shortcutId) {
+        final ShortcutInfo si = mService.getPackageShortcutForTest(packageName, shortcutId, userId);
+        if (si == null) {
+            return null;
+        }
+        mService.waitForBitmapSavesForTest();
+        return new File(si.getBitmapPath()).getAbsolutePath();
+    }
+
     /**
      * @return all shortcuts stored internally for the caller.  This reflects the *internal* view
      * of shortcuts, which may be different from what {@link #getCallerVisibleShortcuts} would
@@ -1826,6 +1842,7 @@
     }
 
     protected boolean bitmapDirectoryExists(String packageName, int userId) {
+        mService.waitForBitmapSavesForTest();
         final File path = new File(mService.getUserBitmapFilePath(userId), packageName);
         return path.isDirectory();
     }
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 2b40c51..3220ea9 100644
--- a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest2.java
+++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest2.java
@@ -2026,12 +2026,11 @@
                     makeShortcutWithIcon("bmp32x32", bmp32x32),
                     makeShortcutWithIcon("bmp64x64", bmp64x64))));
         });
+
         // We can't predict the compressed bitmap sizes, so get the real sizes here.
         final long bitmapTotal =
-                new File(getPackageShortcut(CALLING_PACKAGE_2, "bmp32x32", USER_0)
-                        .getBitmapPath()).length() +
-                new File(getPackageShortcut(CALLING_PACKAGE_2, "bmp64x64", USER_0)
-                        .getBitmapPath()).length();
+                new File(getBitmapAbsPath(USER_0, CALLING_PACKAGE_2, "bmp32x32")).length() +
+                new File(getBitmapAbsPath(USER_0, CALLING_PACKAGE_2, "bmp64x64")).length();
 
         // Read the expected output and inject the bitmap size.
         final String expected = readTestAsset("shortcut/dumpsys_expected.txt")