Merge "Bound progress to 0 to 1" into ub-launcher3-master
diff --git a/OWNERS b/OWNERS
index 538ca33..7340e84 100644
--- a/OWNERS
+++ b/OWNERS
@@ -10,6 +10,7 @@
 sunnygoyal@google.com
 twickham@google.com
 winsonc@google.com
+zakcohen@google.com
 
 per-file FeatureFlags.java = sunnygoyal@google.com, adamcohen@google.com
 per-file BaseFlags.java = sunnygoyal@google.com, adamcohen@google.com
diff --git a/iconloaderlib/src/com/android/launcher3/icons/cache/CachingLogic.java b/iconloaderlib/src/com/android/launcher3/icons/cache/CachingLogic.java
index ea1ca53..a89ede7 100644
--- a/iconloaderlib/src/com/android/launcher3/icons/cache/CachingLogic.java
+++ b/iconloaderlib/src/com/android/launcher3/icons/cache/CachingLogic.java
@@ -17,6 +17,7 @@
 
 import android.content.ComponentName;
 import android.content.Context;
+import android.content.pm.PackageInfo;
 import android.os.LocaleList;
 import android.os.UserHandle;
 
@@ -45,6 +46,13 @@
     }
 
     /**
+     * Returns the timestamp the entry was last updated in cache.
+     */
+    default long getLastUpdatedTime(T object, PackageInfo info) {
+        return info.lastUpdateTime;
+    }
+
+    /**
      * Returns true the object should be added to mem cache; otherwise returns false.
      */
     default boolean addToMemCache() {
diff --git a/iconloaderlib/src/com/android/launcher3/icons/cache/IconCacheUpdateHandler.java b/iconloaderlib/src/com/android/launcher3/icons/cache/IconCacheUpdateHandler.java
index 8224966..bcdbce5 100644
--- a/iconloaderlib/src/com/android/launcher3/icons/cache/IconCacheUpdateHandler.java
+++ b/iconloaderlib/src/com/android/launcher3/icons/cache/IconCacheUpdateHandler.java
@@ -171,7 +171,8 @@
                 long updateTime = c.getLong(indexLastUpdate);
                 int version = c.getInt(indexVersion);
                 T app = componentMap.remove(component);
-                if (version == info.versionCode && updateTime == info.lastUpdateTime
+                if (version == info.versionCode
+                        && updateTime == cachingLogic.getLastUpdatedTime(app, info)
                         && TextUtils.equals(c.getString(systemStateIndex),
                                 mIconCache.getIconSystemState(info.packageName))) {
 
@@ -231,7 +232,6 @@
         }
     }
 
-
     /**
      * A runnable that updates invalid icons and adds missing icons in the DB for the provided
      * LauncherActivityInfo list. Items are updated/added one at a time, so that the
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/HotseatPredictionController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/HotseatPredictionController.java
index ba44213..32846dc 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/HotseatPredictionController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/HotseatPredictionController.java
@@ -15,7 +15,15 @@
  */
 package com.android.launcher3;
 
+import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
+
 import android.animation.Animator;
+import android.animation.ObjectAnimator;
+import android.app.prediction.AppPredictionContext;
+import android.app.prediction.AppPredictionManager;
+import android.app.prediction.AppPredictor;
+import android.app.prediction.AppTarget;
+import android.content.ComponentName;
 import android.util.Log;
 import android.view.View;
 import android.view.ViewGroup;
@@ -25,14 +33,16 @@
 import com.android.launcher3.allapps.AllAppsStore;
 import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.launcher3.appprediction.ComponentKeyMapper;
-import com.android.launcher3.appprediction.PredictionUiStateManager;
-import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.appprediction.DynamicItemCache;
 import com.android.launcher3.dragndrop.DragController;
 import com.android.launcher3.dragndrop.DragOptions;
+import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.popup.PopupContainerWithArrow;
 import com.android.launcher3.popup.SystemShortcut;
+import com.android.launcher3.shortcuts.ShortcutKey;
 import com.android.launcher3.touch.ItemLongClickListener;
 import com.android.launcher3.uioverrides.QuickstepLauncher;
+import com.android.launcher3.util.ComponentKey;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -43,73 +53,86 @@
  * pinning of predicted apps and manages replacement of predicted apps with user drag.
  */
 public class HotseatPredictionController implements DragController.DragListener,
-        View.OnAttachStateChangeListener, SystemShortcut.Factory<QuickstepLauncher> {
+        View.OnAttachStateChangeListener, SystemShortcut.Factory<QuickstepLauncher>,
+        InvariantDeviceProfile.OnIDPChangeListener, AllAppsStore.OnUpdateListener,
+        IconCache.ItemInfoUpdateReceiver {
 
     private static final String TAG = "PredictiveHotseat";
     private static final boolean DEBUG = false;
 
+    private static final String PREDICTION_CLIENT = "hotseat";
 
     private boolean mDragStarted = false;
-    private PredictionUiStateManager mPredictionUiStateManager;
-    private ArrayList<WorkspaceItemInfo> mPredictedApps = new ArrayList<>();
+    private int mHotSeatItemsCount;
+
     private Launcher mLauncher;
+    private Hotseat mHotseat;
+
+    private List<ComponentKeyMapper> mComponentKeyMappers = new ArrayList<>();
+
+    private DynamicItemCache mDynamicItemCache;
+
+    private AppPredictor mAppPredictor;
+    private AllAppsStore mAllAppsStore;
 
     public HotseatPredictionController(Launcher launcher) {
         mLauncher = launcher;
-        mPredictionUiStateManager = PredictionUiStateManager.INSTANCE.get(mLauncher);
-        if (FeatureFlags.ENABLE_HYBRID_HOTSEAT.get()) {
-            mLauncher.getHotseat().addOnAttachStateChangeListener(this);
-        }
+        mHotseat = launcher.getHotseat();
+        mAllAppsStore = mLauncher.getAppsView().getAppsStore();
+        mAllAppsStore.addUpdateListener(this);
+        mDynamicItemCache = new DynamicItemCache(mLauncher, () -> fillGapsWithPrediction(false));
+        mHotSeatItemsCount = mLauncher.getDeviceProfile().inv.numHotseatIcons;
+        launcher.getDeviceProfile().inv.addOnChangeListener(this);
+        mHotseat.addOnAttachStateChangeListener(this);
+        createPredictor();
     }
 
     @Override
     public void onViewAttachedToWindow(View view) {
-        mPredictionUiStateManager.setHotseatPredictionController(this);
         mLauncher.getDragController().addDragListener(this);
     }
 
     @Override
     public void onViewDetachedFromWindow(View view) {
-        mPredictionUiStateManager.setHotseatPredictionController(null);
         mLauncher.getDragController().removeDragListener(this);
     }
 
     /**
-     * sets the list of predicted items. gets called from PredictionUiStateManager
-     */
-    public void setPredictedApps(List<ComponentKeyMapper> apps) {
-        mPredictedApps.clear();
-        mPredictedApps.addAll(mapToWorkspaceItemInfo(apps));
-        fillGapsWithPrediction(false);
-    }
-
-    /**
      * Fills gaps in the hotseat with predictions
      */
     public void fillGapsWithPrediction(boolean animate) {
-        if (!FeatureFlags.ENABLE_HYBRID_HOTSEAT.get() || mDragStarted) {
+        if (mDragStarted) {
             return;
         }
-        removePredictedApps(false);
+        List<WorkspaceItemInfo> predictedApps = mapToWorkspaceItemInfo(mComponentKeyMappers);
         int predictionIndex = 0;
-        ArrayList<ItemInfo> itemInfos = new ArrayList<>();
-        int cellCount = mLauncher.getWallpaperDeviceProfile().inv.numHotseatIcons;
-        for (int rank = 0; rank < cellCount; rank++) {
-            if (mPredictedApps.size() == predictionIndex) {
-                break;
-            }
-            View child = mLauncher.getHotseat().getChildAt(
-                    mLauncher.getHotseat().getCellXFromOrder(rank),
-                    mLauncher.getHotseat().getCellYFromOrder(rank));
-            if (child != null) {
-                // we already have an item there. skip cell
+        ArrayList<ItemInfo> newItemsToAdd = new ArrayList<>();
+        for (int rank = 0; rank < mHotSeatItemsCount; rank++) {
+            View child = mHotseat.getChildAt(
+                    mHotseat.getCellXFromOrder(rank),
+                    mHotseat.getCellYFromOrder(rank));
+
+            if (child != null && !isPredictedIcon(child)) {
                 continue;
             }
-            WorkspaceItemInfo predictedItem = mPredictedApps.get(predictionIndex++);
+            if (predictedApps.size() <= predictionIndex) {
+                // Remove predicted apps from the past
+                if (isPredictedIcon(child)) {
+                    mHotseat.removeView(child);
+                }
+                continue;
+            }
+
+            WorkspaceItemInfo predictedItem = predictedApps.get(predictionIndex++);
+            if (isPredictedIcon(child)) {
+                BubbleTextView icon = (BubbleTextView) child;
+                icon.applyFromWorkspaceItem(predictedItem);
+            } else {
+                newItemsToAdd.add(predictedItem);
+            }
             preparePredictionInfo(predictedItem, rank);
-            itemInfos.add(predictedItem);
         }
-        mLauncher.bindItems(itemInfos, animate);
+        mLauncher.bindItems(newItemsToAdd, animate);
         for (BubbleTextView icon : getPredictedIcons()) {
             icon.verifyHighRes();
             icon.setOnLongClickListener((v) -> {
@@ -119,29 +142,73 @@
         }
     }
 
+    /**
+     * Unregisters callbacks and frees resources
+     */
+    public void destroy() {
+        mAllAppsStore.removeUpdateListener(this);
+        mLauncher.getDeviceProfile().inv.removeOnChangeListener(this);
+        mHotseat.removeOnAttachStateChangeListener(this);
+        if (mAppPredictor != null) {
+            mAppPredictor.destroy();
+        }
+    }
+
+    private void createPredictor() {
+        AppPredictionManager apm = mLauncher.getSystemService(AppPredictionManager.class);
+        if (apm == null) {
+            return;
+        }
+        if (mAppPredictor != null) {
+            mAppPredictor.destroy();
+        }
+        mAppPredictor = apm.createAppPredictionSession(
+                new AppPredictionContext.Builder(mLauncher)
+                        .setUiSurface(PREDICTION_CLIENT)
+                        .setPredictedTargetCount(mHotSeatItemsCount)
+                        .build());
+        mAppPredictor.registerPredictionUpdates(mLauncher.getMainExecutor(),
+                this::setPredictedApps);
+        mAppPredictor.requestPredictionUpdate();
+    }
+
+    private void setPredictedApps(List<AppTarget> appTargets) {
+        mComponentKeyMappers.clear();
+        for (AppTarget appTarget : appTargets) {
+            ComponentKey key;
+            if (appTarget.getShortcutInfo() != null) {
+                key = ShortcutKey.fromInfo(appTarget.getShortcutInfo());
+            } else {
+                key = new ComponentKey(new ComponentName(appTarget.getPackageName(),
+                        appTarget.getClassName()), appTarget.getUser());
+            }
+            mComponentKeyMappers.add(new ComponentKeyMapper(key, mDynamicItemCache));
+        }
+        updateDependencies();
+        fillGapsWithPrediction(false);
+    }
+
+    private void updateDependencies() {
+        mDynamicItemCache.updateDependencies(mComponentKeyMappers, mAllAppsStore, this,
+                mHotSeatItemsCount);
+    }
+
     private void pinPrediction(ItemInfo info) {
-        BubbleTextView icon = (BubbleTextView) mLauncher.getHotseat().getChildAt(
-                mLauncher.getHotseat().getCellXFromOrder(info.rank),
-                mLauncher.getHotseat().getCellYFromOrder(info.rank));
+        BubbleTextView icon = (BubbleTextView) mHotseat.getChildAt(
+                mHotseat.getCellXFromOrder(info.rank),
+                mHotseat.getCellYFromOrder(info.rank));
         if (icon == null) {
             return;
         }
         WorkspaceItemInfo workspaceItemInfo = new WorkspaceItemInfo((WorkspaceItemInfo) info);
-        workspaceItemInfo.container = LauncherSettings.Favorites.CONTAINER_HOTSEAT;
         mLauncher.getModelWriter().addItemToDatabase(workspaceItemInfo,
-                workspaceItemInfo.container, workspaceItemInfo.screenId,
+                LauncherSettings.Favorites.CONTAINER_HOTSEAT, workspaceItemInfo.screenId,
                 workspaceItemInfo.cellX, workspaceItemInfo.cellY);
-        icon.animate().scaleY(0.8f).scaleX(0.8f).setListener(new AnimationSuccessListener() {
-            @Override
-            public void onAnimationSuccess(Animator animator) {
-                icon.animate().scaleY(1).scaleX(1);
-            }
-        });
+        ObjectAnimator.ofFloat(icon, SCALE_PROPERTY, 1, 0.8f, 1).start();
         icon.applyFromWorkspaceItem(workspaceItemInfo);
         icon.setOnLongClickListener(ItemLongClickListener.INSTANCE_WORKSPACE);
     }
 
-
     private List<WorkspaceItemInfo> mapToWorkspaceItemInfo(
             List<ComponentKeyMapper> components) {
         AllAppsStore allAppsStore = mLauncher.getAppsView().getAppsStore();
@@ -163,7 +230,7 @@
                 }
             }
             // Stop at the number of hotseat items
-            if (predictedApps.size() == mLauncher.getDeviceProfile().inv.numHotseatIcons) {
+            if (predictedApps.size() == mHotSeatItemsCount) {
                 break;
             }
         }
@@ -172,12 +239,10 @@
 
     private List<BubbleTextView> getPredictedIcons() {
         List<BubbleTextView> icons = new ArrayList<>();
-        ViewGroup vg = mLauncher.getHotseat().getShortcutsAndWidgets();
+        ViewGroup vg = mHotseat.getShortcutsAndWidgets();
         for (int i = 0; i < vg.getChildCount(); i++) {
             View child = vg.getChildAt(i);
-            if (child instanceof BubbleTextView && child.getTag() instanceof WorkspaceItemInfo
-                    && ((WorkspaceItemInfo) child.getTag()).container
-                    == LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION) {
+            if (isPredictedIcon(child)) {
                 icons.add((BubbleTextView) child);
             }
         }
@@ -191,13 +256,13 @@
                     @Override
                     public void onAnimationSuccess(Animator animator) {
                         if (icon.getParent() != null) {
-                            ((ViewGroup) icon.getParent()).removeView(icon);
+                            mHotseat.removeView(icon);
                         }
                     }
                 });
             } else {
                 if (icon.getParent() != null) {
-                    ((ViewGroup) icon.getParent()).removeView(icon);
+                    mHotseat.removeView(icon);
                 }
             }
         }
@@ -222,7 +287,6 @@
     @Override
     public SystemShortcut<QuickstepLauncher> getShortcut(QuickstepLauncher activity,
             ItemInfo itemInfo) {
-        if (!FeatureFlags.ENABLE_HYBRID_HOTSEAT.get()) return null;
         if (itemInfo.container != LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION) {
             return null;
         }
@@ -233,10 +297,27 @@
         itemInfo.container = LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION;
         itemInfo.rank = rank;
         itemInfo.cellX = rank;
-        itemInfo.cellY =  LauncherAppState.getIDP(mLauncher).numHotseatIcons - rank - 1;
+        itemInfo.cellY = mHotSeatItemsCount - rank - 1;
         itemInfo.screenId = rank;
     }
 
+    @Override
+    public void onIdpChanged(int changeFlags, InvariantDeviceProfile profile) {
+        this.mHotSeatItemsCount = profile.numHotseatIcons;
+        createPredictor();
+    }
+
+    @Override
+    public void onAppsUpdated() {
+        updateDependencies();
+        fillGapsWithPrediction(false);
+    }
+
+    @Override
+    public void reapplyItemInfo(ItemInfoWithIcon info) {
+
+    }
+
     private class PinPrediction extends SystemShortcut<QuickstepLauncher> {
 
         private PinPrediction(QuickstepLauncher target, ItemInfo itemInfo) {
@@ -250,4 +331,10 @@
             pinPrediction(mItemInfo);
         }
     }
+
+    private static boolean isPredictedIcon(View view) {
+        return view instanceof BubbleTextView && view.getTag() instanceof WorkspaceItemInfo
+                && ((WorkspaceItemInfo) view.getTag()).container
+                == LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION;
+    }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/LauncherInitListenerEx.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/LauncherInitListenerEx.java
deleted file mode 100644
index 76050d5..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/LauncherInitListenerEx.java
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Copyright (C) 2019 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.launcher3;
-
-import com.android.launcher3.appprediction.PredictionUiStateManager;
-import com.android.launcher3.appprediction.PredictionUiStateManager.Client;
-
-import java.util.function.BiPredicate;
-
-public class LauncherInitListenerEx extends LauncherInitListener {
-
-    public LauncherInitListenerEx(BiPredicate<Launcher, Boolean> onInitListener) {
-        super(onInitListener);
-    }
-
-    @Override
-    public boolean init(Launcher launcher, boolean alreadyOnHome) {
-        PredictionUiStateManager.INSTANCE.get(launcher).switchClient(Client.OVERVIEW);
-        return super.init(launcher, alreadyOnHome);
-    }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/DynamicItemCache.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/DynamicItemCache.java
index 06580b9..38bb180 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/DynamicItemCache.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/DynamicItemCache.java
@@ -18,6 +18,7 @@
 import static android.content.pm.PackageManager.MATCH_INSTANT;
 
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
+import static com.android.quickstep.InstantAppResolverImpl.COMPONENT_CLASS_MARKER;
 
 import android.content.Context;
 import android.content.Intent;
@@ -37,8 +38,10 @@
 import androidx.annotation.UiThread;
 import androidx.annotation.WorkerThread;
 
+import com.android.launcher3.AppInfo;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.WorkspaceItemInfo;
+import com.android.launcher3.allapps.AllAppsStore;
 import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.icons.LauncherIcons;
 import com.android.launcher3.shortcuts.DeepShortcutManager;
@@ -72,6 +75,7 @@
     private final Handler mUiHandler;
     private final InstantAppResolver mInstantAppResolver;
     private final Runnable mOnUpdateCallback;
+    private final IconCache mIconCache;
 
     private final Map<ShortcutKey, WorkspaceItemInfo> mShortcuts;
     private final Map<String, InstantAppItemInfo> mInstantApps;
@@ -82,6 +86,7 @@
         mUiHandler = new Handler(Looper.getMainLooper(), this::handleUiMessage);
         mInstantAppResolver = InstantAppResolver.newInstance(context);
         mOnUpdateCallback = onUpdateCallback;
+        mIconCache = LauncherAppState.getInstance(mContext).getIconCache();
 
         mShortcuts = new HashMap<>();
         mInstantApps = new HashMap<>();
@@ -240,4 +245,35 @@
     public WorkspaceItemInfo getShortcutInfo(ShortcutKey key) {
         return mShortcuts.get(key);
     }
+
+    /**
+     * requests and caches icons for app targets
+     */
+    public void updateDependencies(List<ComponentKeyMapper> componentKeyMappers,
+            AllAppsStore appsStore, IconCache.ItemInfoUpdateReceiver callback, int itemCount) {
+        List<String> instantAppsToLoad = new ArrayList<>();
+        List<ShortcutKey> shortcutsToLoad = new ArrayList<>();
+        int total = componentKeyMappers.size();
+        for (int i = 0, count = 0; i < total && count < itemCount; i++) {
+            ComponentKeyMapper mapper = componentKeyMappers.get(i);
+            // Update instant apps
+            if (COMPONENT_CLASS_MARKER.equals(mapper.getComponentClass())) {
+                instantAppsToLoad.add(mapper.getPackage());
+                count++;
+            } else if (mapper.getComponentKey() instanceof ShortcutKey) {
+                shortcutsToLoad.add((ShortcutKey) mapper.getComponentKey());
+                count++;
+            } else {
+                // Reload high res icon
+                AppInfo info = (AppInfo) mapper.getApp(appsStore);
+                if (info != null) {
+                    if (info.usingLowResIcon()) {
+                        mIconCache.updateIconInBackground(callback, info);
+                    }
+                    count++;
+                }
+            }
+        }
+        cacheItems(shortcutsToLoad, instantAppsToLoad);
+    }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionUiStateManager.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionUiStateManager.java
index 8fade5c..8338c2e 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionUiStateManager.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionUiStateManager.java
@@ -1,4 +1,4 @@
-/**
+/*
  * Copyright (C) 2019 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
@@ -18,15 +18,12 @@
 
 import static com.android.launcher3.LauncherState.BACKGROUND_APP;
 import static com.android.launcher3.LauncherState.OVERVIEW;
-import static com.android.quickstep.InstantAppResolverImpl.COMPONENT_CLASS_MARKER;
 
 import android.app.prediction.AppPredictor;
 import android.app.prediction.AppTarget;
 import android.content.ComponentName;
 import android.content.Context;
 
-import com.android.launcher3.AppInfo;
-import com.android.launcher3.HotseatPredictionController;
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.InvariantDeviceProfile.OnIDPChangeListener;
 import com.android.launcher3.ItemInfoWithIcon;
@@ -37,7 +34,6 @@
 import com.android.launcher3.Utilities;
 import com.android.launcher3.allapps.AllAppsContainerView;
 import com.android.launcher3.allapps.AllAppsStore.OnUpdateListener;
-import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.icons.IconCache.ItemInfoUpdateReceiver;
 import com.android.launcher3.shortcuts.ShortcutKey;
 import com.android.launcher3.util.ComponentKey;
@@ -90,8 +86,6 @@
 
     private AllAppsContainerView mAppsView;
 
-    private HotseatPredictionController mHotseatPredictionController;
-
     private PredictionState mPendingState;
     private PredictionState mCurrentState;
 
@@ -153,10 +147,6 @@
         updateDependencies(mCurrentState);
     }
 
-    public void setHotseatPredictionController(HotseatPredictionController controller) {
-        mHotseatPredictionController = controller;
-    }
-
     @Override
     public void reapplyItemInfo(ItemInfoWithIcon info) { }
 
@@ -193,9 +183,6 @@
             mAppsView.getFloatingHeaderView().findFixedRowByType(PredictionRowView.class)
                     .setPredictedApps(mCurrentState.apps);
         }
-        if (mHotseatPredictionController != null) {
-            mHotseatPredictionController.setPredictedApps(mCurrentState.apps);
-        }
     }
 
     private void updatePredictionStateAfterCallback() {
@@ -260,33 +247,8 @@
         if (!state.isEnabled || mAppsView == null) {
             return;
         }
-
-        IconCache iconCache = LauncherAppState.getInstance(mContext).getIconCache();
-        List<String> instantAppsToLoad = new ArrayList<>();
-        List<ShortcutKey> shortcutsToLoad = new ArrayList<>();
-        int total = state.apps.size();
-        for (int i = 0, count = 0; i < total && count < mMaxIconsPerRow; i++) {
-            ComponentKeyMapper mapper = state.apps.get(i);
-            // Update instant apps
-            if (COMPONENT_CLASS_MARKER.equals(mapper.getComponentClass())) {
-                instantAppsToLoad.add(mapper.getPackage());
-                count++;
-            } else if (mapper.getComponentKey() instanceof ShortcutKey) {
-                shortcutsToLoad.add((ShortcutKey) mapper.getComponentKey());
-                count++;
-            } else {
-                // Reload high res icon
-                AppInfo info = (AppInfo) mapper.getApp(mAppsView.getAppsStore());
-                if (info != null) {
-                    if (info.usingLowResIcon()) {
-                        // TODO: Update icon cache to support null callbacks.
-                        iconCache.updateIconInBackground(this, info);
-                    }
-                    count++;
-                }
-            }
-        }
-        mDynamicItemCache.cacheItems(shortcutsToLoad, instantAppsToLoad);
+        mDynamicItemCache.updateDependencies(state.apps, mAppsView.getAppsStore(), this,
+                mMaxIconsPerRow);
     }
 
     @Override
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index a6fcf44..eefb7dc 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -1,4 +1,4 @@
-/**
+/*
  * Copyright (C) 2019 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
@@ -31,6 +31,7 @@
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.graphics.RotationMode;
 import com.android.launcher3.popup.SystemShortcut;
 import com.android.launcher3.uioverrides.touchcontrollers.FlingAndHoldTouchController;
@@ -137,7 +138,9 @@
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
-        mHotseatPredictionController = new HotseatPredictionController(this);
+        if (FeatureFlags.ENABLE_HYBRID_HOTSEAT.get()) {
+            mHotseatPredictionController = new HotseatPredictionController(this);
+        }
     }
 
     @Override
@@ -165,8 +168,12 @@
 
     @Override
     public Stream<SystemShortcut.Factory> getSupportedShortcuts() {
-        return Stream.concat(super.getSupportedShortcuts(),
-                Stream.of(mHotseatPredictionController));
+        if (mHotseatPredictionController != null) {
+            return Stream.concat(super.getSupportedShortcuts(),
+                    Stream.of(mHotseatPredictionController));
+        } else {
+            return super.getSupportedShortcuts();
+        }
     }
 
     /**
@@ -187,7 +194,17 @@
     @Override
     public void finishBindingItems(int pageBoundFirst) {
         super.finishBindingItems(pageBoundFirst);
-        mHotseatPredictionController.fillGapsWithPrediction(false);
+        if (mHotseatPredictionController != null) {
+            mHotseatPredictionController.fillGapsWithPrediction(false);
+        }
+    }
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+        if (mHotseatPredictionController != null) {
+            mHotseatPredictionController.destroy();
+        }
     }
 
     @Override
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/QuickSwitchState.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/QuickSwitchState.java
index 1279270..7b4bb02 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/QuickSwitchState.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/QuickSwitchState.java
@@ -26,7 +26,7 @@
 
 /**
  * State to indicate we are about to launch a recent task. Note that this state is only used when
- * quick switching from launcher; quick switching from an app uses WindowTransformSwipeHelper.
+ * quick switching from launcher; quick switching from an app uses LauncherSwipeHandler.
  * @see GestureState.GestureEndTarget#NEW_TASK
  */
 public class QuickSwitchState extends BackgroundAppState {
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java
index 1897fb2..ad4a343 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java
@@ -22,6 +22,7 @@
 import static com.android.launcher3.LauncherState.OVERVIEW;
 import static com.android.launcher3.allapps.AllAppsTransitionController.ALL_APPS_PROGRESS;
 import static com.android.launcher3.anim.Interpolators.DEACCEL_3;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
 import static com.android.launcher3.touch.AbstractStateChangeTouchController.SUCCESS_TRANSITION_PROGRESS;
 import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
 
@@ -138,8 +139,13 @@
             if (!recentsView.isRtl()) {
                 pullbackDist = -pullbackDist;
             }
-            Animator pullback = ObjectAnimator.ofFloat(recentsView, TRANSLATION_X, pullbackDist);
+            ObjectAnimator pullback = ObjectAnimator.ofFloat(recentsView, TRANSLATION_X,
+                    pullbackDist);
             pullback.setInterpolator(PULLBACK_INTERPOLATOR);
+            if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+                pullback.addUpdateListener(
+                        valueAnimator -> recentsView.redrawLiveTile(false /* mightNeedToRefill */));
+            }
             anim.play(pullback);
         } else if (mStartState == ALL_APPS) {
             AnimatorSetBuilder builder = new AnimatorSetBuilder();
@@ -192,6 +198,11 @@
         boolean success = interpolatedProgress >= SUCCESS_TRANSITION_PROGRESS
                 || (velocity < 0 && fling);
         if (success) {
+            if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+                RecentsView recentsView = mLauncher.getOverviewPanel();
+                recentsView.switchToScreenshot(null,
+                        () -> recentsView.finishRecentsAnimation(true /* toRecents */, null));
+            }
             mLauncher.getStateManager().goToState(mEndState, true,
                     () -> onSwipeInteractionCompleted(mEndState));
             if (mStartState != mEndState) {
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/FallbackNoButtonInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackSwipeHandler.java
similarity index 95%
rename from quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/FallbackNoButtonInputConsumer.java
rename to quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackSwipeHandler.java
index b6cd456..24f247b 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/FallbackNoButtonInputConsumer.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackSwipeHandler.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.quickstep.inputconsumers;
+package com.android.quickstep;
 
 import static com.android.quickstep.GestureState.GestureEndTarget.HOME;
 import static com.android.quickstep.GestureState.GestureEndTarget.LAST_TASK;
@@ -22,7 +22,7 @@
 import static com.android.quickstep.MultiStateCallback.DEBUG_STATES;
 import static com.android.quickstep.RecentsActivity.EXTRA_TASK_ID;
 import static com.android.quickstep.RecentsActivity.EXTRA_THUMBNAIL;
-import static com.android.quickstep.WindowTransformSwipeHandler.MIN_PROGRESS_FOR_OVERVIEW;
+import static com.android.quickstep.LauncherSwipeHandler.MIN_PROGRESS_FOR_OVERVIEW;
 import static com.android.quickstep.views.RecentsView.UPDATE_SYSUI_FLAGS_THRESHOLD;
 
 import android.animation.Animator;
@@ -40,26 +40,19 @@
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.util.ObjectWrapper;
 import com.android.quickstep.BaseActivityInterface.HomeAnimationFactory;
-import com.android.quickstep.AnimatedFloat;
-import com.android.quickstep.BaseSwipeUpHandler;
-import com.android.quickstep.GestureState;
 import com.android.quickstep.GestureState.GestureEndTarget;
-import com.android.quickstep.InputConsumer;
-import com.android.quickstep.MultiStateCallback;
-import com.android.quickstep.RecentsActivity;
-import com.android.quickstep.RecentsAnimationController;
-import com.android.quickstep.RecentsAnimationDeviceState;
 import com.android.quickstep.fallback.FallbackRecentsView;
 import com.android.quickstep.util.RectFSpringAnim;
-import com.android.quickstep.RecentsAnimationTargets;
 import com.android.quickstep.views.TaskView;
 import com.android.systemui.shared.recents.model.ThumbnailData;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.ActivityOptionsCompat;
 import com.android.systemui.shared.system.InputConsumerController;
 
-public class FallbackNoButtonInputConsumer extends
-        BaseSwipeUpHandler<RecentsActivity, FallbackRecentsView> {
+/**
+ * Handles the navigation gestures when a 3rd party launcher is the default home activity.
+ */
+public class FallbackSwipeHandler extends BaseSwipeUpHandler<RecentsActivity, FallbackRecentsView> {
 
     private static final String[] STATE_NAMES = DEBUG_STATES ? new String[5] : null;
 
@@ -108,7 +101,7 @@
     private final PointF mEndVelocityPxPerMs = new PointF(0, 0.5f);
     private RunningWindowAnim mFinishAnimation;
 
-    public FallbackNoButtonInputConsumer(Context context, RecentsAnimationDeviceState deviceState,
+    public FallbackSwipeHandler(Context context, RecentsAnimationDeviceState deviceState,
             GestureState gestureState, InputConsumerController inputConsumer,
             boolean isLikelyToStartNewTask, boolean continuingLastGesture) {
         super(context, deviceState, gestureState, inputConsumer);
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityInterface.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityInterface.java
index 48b8fc6..01cf4bb 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityInterface.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityInterface.java
@@ -29,7 +29,7 @@
 import static com.android.launcher3.anim.Interpolators.ACCEL_DEACCEL;
 import static com.android.launcher3.anim.Interpolators.INSTANT;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
-import static com.android.quickstep.WindowTransformSwipeHandler.RECENTS_ATTACH_DURATION;
+import static com.android.quickstep.LauncherSwipeHandler.RECENTS_ATTACH_DURATION;
 
 import android.animation.Animator;
 import android.animation.AnimatorSet;
@@ -49,12 +49,13 @@
 
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherInitListenerEx;
+import com.android.launcher3.LauncherInitListener;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.LauncherStateManager;
 import com.android.launcher3.allapps.DiscoveryBounce;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.AnimatorSetBuilder;
+import com.android.launcher3.appprediction.PredictionUiStateManager;
 import com.android.launcher3.uioverrides.states.OverviewState;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.views.FloatingIconView;
@@ -413,7 +414,7 @@
 
     @Override
     public ActivityInitListener createActivityInitListener(Predicate<Boolean> onInitListener) {
-        return new LauncherInitListenerEx((activity, alreadyOnHome) ->
+        return new LauncherInitListener((activity, alreadyOnHome) ->
                 onInitListener.test(alreadyOnHome));
     }
 
@@ -541,4 +542,14 @@
         }
         launcher.setOnDeferredActivityLaunchCallback(r);
     }
+
+    @Override
+    public void updateOverviewPredictionState() {
+        Launcher launcher = getCreatedActivity();
+        if (launcher == null) {
+            return;
+        }
+        PredictionUiStateManager.INSTANCE.get(launcher).switchClient(
+                PredictionUiStateManager.Client.OVERVIEW);
+    }
 }
\ No newline at end of file
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/WindowTransformSwipeHandler.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherSwipeHandler.java
similarity index 96%
rename from quickstep/recents_ui_overrides/src/com/android/quickstep/WindowTransformSwipeHandler.java
rename to quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherSwipeHandler.java
index ad6a852..3eb183e 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/WindowTransformSwipeHandler.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherSwipeHandler.java
@@ -32,6 +32,7 @@
 import static com.android.quickstep.GestureState.GestureEndTarget.RECENTS;
 import static com.android.quickstep.GestureState.STATE_END_TARGET_ANIMATION_FINISHED;
 import static com.android.quickstep.MultiStateCallback.DEBUG_STATES;
+import static com.android.quickstep.SysUINavigationMode.Mode.TWO_BUTTONS;
 import static com.android.quickstep.views.RecentsView.UPDATE_SYSUI_FLAGS_THRESHOLD;
 
 import android.animation.Animator;
@@ -83,10 +84,13 @@
 import com.android.systemui.shared.system.LatencyTrackerCompat;
 import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
 
+/**
+ * Handles the navigation gestures when Launcher is the default home activity.
+ */
 @TargetApi(Build.VERSION_CODES.O)
-public class WindowTransformSwipeHandler<T extends BaseDraggingActivity>
+public class LauncherSwipeHandler<T extends BaseDraggingActivity>
         extends BaseSwipeUpHandler<T, RecentsView> implements OnApplyWindowInsetsListener {
-    private static final String TAG = WindowTransformSwipeHandler.class.getSimpleName();
+    private static final String TAG = LauncherSwipeHandler.class.getSimpleName();
 
     private static final String[] STATE_NAMES = DEBUG_STATES ? new String[16] : null;
 
@@ -189,7 +193,7 @@
 
     private final Runnable mOnDeferredActivityLaunch = this::onDeferredActivityLaunch;
 
-    public WindowTransformSwipeHandler(Context context, RecentsAnimationDeviceState deviceState,
+    public LauncherSwipeHandler(Context context, RecentsAnimationDeviceState deviceState,
             TaskAnimationManager taskAnimationManager, GestureState gestureState,
             long touchTimeMs, boolean continuingLastGesture,
             InputConsumerController inputConsumer) {
@@ -293,6 +297,12 @@
         }
 
         setupRecentsViewUi();
+
+        if (mDeviceState.getNavMode() == TWO_BUTTONS) {
+            // If the device is in two button mode, swiping up will show overview with predictions
+            // so we need to kick off switching to the overview predictions as soon as possible
+            mActivityInterface.updateOverviewPredictionState();
+        }
         return true;
     }
 
@@ -423,6 +433,12 @@
     @Override
     public void onMotionPauseChanged(boolean isPaused) {
         setShelfState(isPaused ? PEEK : HIDE, OVERSHOOT_1_2, SHELF_ANIM_DURATION);
+
+        if (mDeviceState.isFullyGesturalNavMode() && isPaused) {
+            // In fully gestural nav mode, switch to overview predictions once the user has paused
+            // (this is a no-op if the predictions are already in that state)
+            mActivityInterface.updateOverviewPredictionState();
+        }
     }
 
     public void maybeUpdateRecentsAttachedState() {
@@ -666,7 +682,7 @@
         endLauncherTransitionController();
         if (!ENABLE_QUICKSTEP_LIVE_TILE.get()) {
             // Hide the task view, if not already hidden
-            setTargetAlphaProvider(WindowTransformSwipeHandler::getHiddenTargetAlpha);
+            setTargetAlphaProvider(LauncherSwipeHandler::getHiddenTargetAlpha);
         }
 
         BaseDraggingActivity activity = mActivityInterface.getCreatedActivity();
@@ -1172,20 +1188,21 @@
     private void finishCurrentTransitionToRecents() {
         if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
             mStateCallback.setStateOnUiThread(STATE_CURRENT_TASK_FINISHED);
-        } else if (!hasTargets()) {
-            // If there are no targets, then there is nothing to finish
+        } else if (!hasTargets() || mRecentsAnimationController == null) {
+            // If there are no targets or the animation not started, then there is nothing to finish
             mStateCallback.setStateOnUiThread(STATE_CURRENT_TASK_FINISHED);
         } else {
-            synchronized (mRecentsAnimationController) {
-                mRecentsAnimationController.finish(true /* toRecents */,
-                        () -> mStateCallback.setStateOnUiThread(STATE_CURRENT_TASK_FINISHED));
-            }
+            mRecentsAnimationController.finish(true /* toRecents */,
+                    () -> mStateCallback.setStateOnUiThread(STATE_CURRENT_TASK_FINISHED));
         }
         ActiveGestureLog.INSTANCE.addLog("finishRecentsAnimation", true);
     }
 
     private void finishCurrentTransitionToHome() {
-        synchronized (mRecentsAnimationController) {
+        if (!hasTargets() || mRecentsAnimationController == null) {
+            // If there are no targets or the animation not started, then there is nothing to finish
+            mStateCallback.setStateOnUiThread(STATE_CURRENT_TASK_FINISHED);
+        } else {
             mRecentsAnimationController.finish(true /* toRecents */,
                     () -> mStateCallback.setStateOnUiThread(STATE_CURRENT_TASK_FINISHED),
                     true /* sendUserLeaveHint */);
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/OverviewCommandHelper.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/OverviewCommandHelper.java
index 7f53d83..c4733bd 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/OverviewCommandHelper.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/OverviewCommandHelper.java
@@ -29,6 +29,7 @@
 
 import androidx.annotation.BinderThread;
 import com.android.launcher3.BaseDraggingActivity;
+import com.android.launcher3.appprediction.PredictionUiStateManager;
 import com.android.launcher3.logging.UserEventDispatcher;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.quickstep.util.ActivityInitListener;
@@ -212,6 +213,10 @@
                         LauncherLogProto.ContainerType.TASKSWITCHER);
                 mUserEventLogged = true;
             }
+
+            // Switch prediction client to overview
+            PredictionUiStateManager.INSTANCE.get(activity).switchClient(
+                    PredictionUiStateManager.Client.OVERVIEW);
             return mAnimationProvider.onActivityReady(activity, wasVisible);
         }
 
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
index 7ad3f37..71f8ba4 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
@@ -66,7 +66,6 @@
 import com.android.quickstep.inputconsumers.AccessibilityInputConsumer;
 import com.android.quickstep.inputconsumers.AssistantInputConsumer;
 import com.android.quickstep.inputconsumers.DeviceLockedInputConsumer;
-import com.android.quickstep.inputconsumers.FallbackNoButtonInputConsumer;
 import com.android.quickstep.inputconsumers.OtherActivityInputConsumer;
 import com.android.quickstep.inputconsumers.OverscrollInputConsumer;
 import com.android.quickstep.inputconsumers.OverviewInputConsumer;
@@ -242,10 +241,10 @@
         return sIsInitialized;
     }
 
-    private final BaseSwipeUpHandler.Factory mWindowTransformFactory =
-            this::createWindowTransformSwipeHandler;
-    private final BaseSwipeUpHandler.Factory mFallbackNoButtonFactory =
-            this::createFallbackNoButtonSwipeHandler;
+    private final BaseSwipeUpHandler.Factory mLauncherSwipeHandlerFactory =
+            this::createLauncherSwipeHandler;
+    private final BaseSwipeUpHandler.Factory mFallbackSwipeHandlerFactory =
+            this::createFallbackSwipeHandler;
 
     private ActivityManagerWrapper mAM;
     private OverviewCommandHelper mOverviewCommandHelper;
@@ -564,11 +563,11 @@
         if (mDeviceState.isFullyGesturalNavMode()
                 && !mOverviewComponentObserver.isHomeAndOverviewSame()) {
             shouldDefer = previousGestureState.getFinishingRecentsAnimationTaskId() < 0;
-            factory = mFallbackNoButtonFactory;
+            factory = mFallbackSwipeHandlerFactory;
         } else {
             shouldDefer = gestureState.getActivityInterface().deferStartingActivity(mDeviceState,
                     event);
-            factory = mWindowTransformFactory;
+            factory = mLauncherSwipeHandlerFactory;
         }
 
         final boolean disableHorizontalSwipe = mDeviceState.isInExclusionRegion(event);
@@ -716,15 +715,15 @@
         }
     }
 
-    private BaseSwipeUpHandler createWindowTransformSwipeHandler(GestureState gestureState,
+    private BaseSwipeUpHandler createLauncherSwipeHandler(GestureState gestureState,
             long touchTimeMs, boolean continuingLastGesture, boolean isLikelyToStartNewTask) {
-        return  new WindowTransformSwipeHandler(this, mDeviceState, mTaskAnimationManager,
+        return  new LauncherSwipeHandler(this, mDeviceState, mTaskAnimationManager,
                 gestureState, touchTimeMs, continuingLastGesture, mInputConsumer);
     }
 
-    private BaseSwipeUpHandler createFallbackNoButtonSwipeHandler(GestureState gestureState,
+    private BaseSwipeUpHandler createFallbackSwipeHandler(GestureState gestureState,
             long touchTimeMs, boolean continuingLastGesture, boolean isLikelyToStartNewTask) {
-        return new FallbackNoButtonInputConsumer(this, mDeviceState, gestureState,
+        return new FallbackSwipeHandler(this, mDeviceState, gestureState,
                 mInputConsumer, isLikelyToStartNewTask, continuingLastGesture);
     }
 
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java
index e448c5b..5a34520 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java
@@ -22,7 +22,7 @@
 import static com.android.launcher3.Utilities.squaredHypot;
 import static com.android.launcher3.Utilities.squaredTouchSlop;
 import static com.android.quickstep.MultiStateCallback.DEBUG_STATES;
-import static com.android.quickstep.WindowTransformSwipeHandler.MIN_PROGRESS_FOR_OVERVIEW;
+import static com.android.quickstep.LauncherSwipeHandler.MIN_PROGRESS_FOR_OVERVIEW;
 import static com.android.quickstep.util.ActiveGestureLog.INTENT_EXTRA_LOG_TRACE_ID;
 
 import android.content.ComponentName;
diff --git a/quickstep/src/com/android/launcher3/LauncherInitListener.java b/quickstep/src/com/android/launcher3/LauncherInitListener.java
index 663b125..96340b2 100644
--- a/quickstep/src/com/android/launcher3/LauncherInitListener.java
+++ b/quickstep/src/com/android/launcher3/LauncherInitListener.java
@@ -22,6 +22,7 @@
 import android.os.CancellationSignal;
 import android.os.Handler;
 
+import com.android.launcher3.util.ActivityTracker;
 import com.android.quickstep.util.ActivityInitListener;
 import com.android.quickstep.util.RemoteAnimationProvider;
 
@@ -32,6 +33,11 @@
 
     private RemoteAnimationProvider mRemoteAnimationProvider;
 
+    /**
+     * @param onInitListener a callback made when the activity is initialized. The callback should
+     *                       return true to continue receiving callbacks (ie. for if the activity is
+     *                       recreated).
+     */
     public LauncherInitListener(BiPredicate<Launcher, Boolean> onInitListener) {
         super(onInitListener, Launcher.ACTIVITY_TRACKER);
     }
diff --git a/quickstep/src/com/android/launcher3/model/WellbeingModel.java b/quickstep/src/com/android/launcher3/model/WellbeingModel.java
index 852a08e..5aa4388 100644
--- a/quickstep/src/com/android/launcher3/model/WellbeingModel.java
+++ b/quickstep/src/com/android/launcher3/model/WellbeingModel.java
@@ -335,8 +335,8 @@
      * Shortcut factory for generating wellbeing action
      */
     public static final SystemShortcut.Factory SHORTCUT_FACTORY = (activity, info) ->
-            WellbeingModel.get(activity).getShortcutForApp(
-                    info.getTargetComponent().getPackageName(),
-                    info.user.getIdentifier(),
-                    activity, info);
+            (info.getTargetComponent() == null) ? null : WellbeingModel.get(activity)
+                    .getShortcutForApp(
+                            info.getTargetComponent().getPackageName(), info.user.getIdentifier(),
+                            activity, info);
 }
diff --git a/quickstep/src/com/android/quickstep/BaseActivityInterface.java b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
index cb18001..86ffd22 100644
--- a/quickstep/src/com/android/quickstep/BaseActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
@@ -89,6 +89,13 @@
     }
 
     /**
+     * Updates the prediction state to the overview state.
+     */
+    default void updateOverviewPredictionState() {
+        // By default overview predictions are not supported
+    }
+
+    /**
      * Used for containerType in {@link com.android.launcher3.logging.UserEventDispatcher}
      */
     int getContainerType();
diff --git a/quickstep/src/com/android/quickstep/util/ActivityInitListener.java b/quickstep/src/com/android/quickstep/util/ActivityInitListener.java
index fe37d60..b1c72ce 100644
--- a/quickstep/src/com/android/quickstep/util/ActivityInitListener.java
+++ b/quickstep/src/com/android/quickstep/util/ActivityInitListener.java
@@ -31,6 +31,11 @@
     private final BiPredicate<T, Boolean> mOnInitListener;
     private final ActivityTracker<T> mActivityTracker;
 
+    /**
+     * @param onInitListener a callback made when the activity is initialized. The callback should
+     *                       return true to continue receiving callbacks (ie. for if the activity is
+     *                       recreated).
+     */
     public ActivityInitListener(BiPredicate<T, Boolean> onInitListener,
             ActivityTracker<T> tracker) {
         mOnInitListener = onInitListener;
@@ -42,6 +47,10 @@
         return mOnInitListener.test(activity, alreadyOnHome);
     }
 
+    /**
+     * Registers the activity-created listener. If the activity is already created, then the
+     * callback provided in the constructor will be called synchronously.
+     */
     public void register() {
         mActivityTracker.schedule(this);
     }
diff --git a/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java b/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
index 13731b6..ca81343 100644
--- a/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
+++ b/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
@@ -175,7 +175,7 @@
             }
             result[0] = f.apply(activity);
             return true;
-        }).get(), DEFAULT_UI_TIMEOUT);
+        }).get(), DEFAULT_UI_TIMEOUT, mLauncher);
         return (T) result[0];
     }
 
@@ -196,7 +196,7 @@
         startAppFastAndWaitForRecentTask(resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR));
         startTestActivity(2);
         Wait.atMost("Expected three apps in the task list",
-                () -> mLauncher.getRecentTasks().size() >= 3, DEFAULT_ACTIVITY_TIMEOUT);
+                () -> mLauncher.getRecentTasks().size() >= 3, DEFAULT_ACTIVITY_TIMEOUT, mLauncher);
 
         BaseOverview overview = mLauncher.getBackground().switchToOverview();
         executeOnRecents(recents ->
@@ -255,7 +255,8 @@
     private void startAppFastAndWaitForRecentTask(String packageName) {
         startAppFast(packageName);
         Wait.atMost("Expected app in task list",
-                () -> containsRecentTaskWithPackage(packageName), DEFAULT_ACTIVITY_TIMEOUT);
+                () -> containsRecentTaskWithPackage(packageName), DEFAULT_ACTIVITY_TIMEOUT,
+                mLauncher);
     }
 
     private boolean containsRecentTaskWithPackage(String packageName) {
diff --git a/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java b/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java
index c2197ab..fa4c7b9 100644
--- a/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java
+++ b/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java
@@ -30,6 +30,7 @@
 import android.content.pm.PackageManager;
 import android.util.Log;
 
+import androidx.test.uiautomator.By;
 import androidx.test.uiautomator.UiDevice;
 
 import com.android.launcher3.tapl.LauncherInstrumentation;
@@ -80,7 +81,13 @@
             Mode mode = description.getAnnotation(NavigationModeSwitch.class).mode();
             return new Statement() {
                 private void assertTrue(String message, boolean condition) {
-                    if(!condition) {
+                    if (mLauncher.getDevice().hasObject(By.textStartsWith(""))) {
+                        // The condition above is "screen is not empty". We are not treating
+                        // "Screen is empty" as an anomaly here. It's an acceptable state when
+                        // Launcher just starts under instrumentation.
+                        mLauncher.checkForAnomaly();
+                    }
+                    if (!condition) {
                         final AssertionError assertionError = new AssertionError(message);
                         FailureWatcher.onError(mLauncher.getDevice(), description, assertionError);
                         throw assertionError;
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
index 8cd3bb6..428e647 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
@@ -248,6 +248,7 @@
     @Test
     @NavigationModeSwitch
     @PortraitLandscape
+    @Ignore // b/143285809
     public void testQuickSwitchFromApp() throws Exception {
         startTestActivity(2);
         startTestActivity(3);
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index ff5f33d..01e9b6e 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -496,7 +496,8 @@
         // Text should be visible everywhere but the hotseat.
         Object tag = getParent() instanceof FolderIcon ? ((View) getParent()).getTag() : getTag();
         ItemInfo info = tag instanceof ItemInfo ? (ItemInfo) tag : null;
-        return info == null || info.container != LauncherSettings.Favorites.CONTAINER_HOTSEAT;
+        return info == null || (info.container != LauncherSettings.Favorites.CONTAINER_HOTSEAT
+                && info.container != LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION);
     }
 
     public void setTextVisibility(boolean visible) {
diff --git a/src/com/android/launcher3/InstallShortcutReceiver.java b/src/com/android/launcher3/InstallShortcutReceiver.java
index 125332d..21359f1 100644
--- a/src/com/android/launcher3/InstallShortcutReceiver.java
+++ b/src/com/android/launcher3/InstallShortcutReceiver.java
@@ -17,6 +17,7 @@
 package com.android.launcher3;
 
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
+import static com.android.launcher3.util.ShortcutUtil.fetchAndUpdateShortcutIconAsync;
 
 import android.appwidget.AppWidgetManager;
 import android.appwidget.AppWidgetProviderInfo;
@@ -44,6 +45,7 @@
 import androidx.annotation.WorkerThread;
 
 import com.android.launcher3.compat.UserManagerCompat;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.icons.BitmapInfo;
 import com.android.launcher3.icons.GraphicsUtils;
 import com.android.launcher3.icons.LauncherIcons;
@@ -489,9 +491,13 @@
                 return Pair.create(si, null);
             } else if (shortcutInfo != null) {
                 WorkspaceItemInfo itemInfo = new WorkspaceItemInfo(shortcutInfo, mContext);
-                LauncherIcons li = LauncherIcons.obtain(mContext);
-                itemInfo.bitmap = li.createShortcutIcon(shortcutInfo);
-                li.recycle();
+                if (FeatureFlags.ENABLE_DEEP_SHORTCUT_ICON_CACHE.get()) {
+                    fetchAndUpdateShortcutIconAsync(mContext, itemInfo, shortcutInfo, true);
+                } else {
+                    LauncherIcons li = LauncherIcons.obtain(mContext);
+                    itemInfo.bitmap = li.createShortcutIcon(shortcutInfo);
+                    li.recycle();
+                }
                 return Pair.create(itemInfo, shortcutInfo);
             } else if (providerInfo != null) {
                 LauncherAppWidgetProviderInfo info = LauncherAppWidgetProviderInfo
diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java
index 2032845..2bec0ba 100644
--- a/src/com/android/launcher3/Utilities.java
+++ b/src/com/android/launcher3/Utilities.java
@@ -586,7 +586,7 @@
             LauncherIcons li = LauncherIcons.obtain(appState.getContext());
             Bitmap badge = li.getShortcutInfoBadge(si, appState.getIconCache()).bitmap.icon;
             li.recycle();
-            float badgeSize = iconSize * LauncherIcons.getBadgeSizeForIconSize(iconSize);
+            float badgeSize = LauncherIcons.getBadgeSizeForIconSize(iconSize);
             float insetFraction = (iconSize - badgeSize) / iconSize;
             return new InsetDrawable(new FastBitmapDrawable(badge),
                     insetFraction, insetFraction, 0, 0);
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index 5689846..d622037 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -127,6 +127,9 @@
     public static final TogglableFlag ENABLE_HYBRID_HOTSEAT = new TogglableFlag(
             "ENABLE_HYBRID_HOTSEAT", false, "Fill gaps in hotseat with predicted apps");
 
+    public static final TogglableFlag ENABLE_DEEP_SHORTCUT_ICON_CACHE = new TogglableFlag(
+            "ENABLE_DEEP_SHORTCUT_ICON_CACHE", true, "R/W deep shortcut in IconCache");
+
     public static void initialize(Context context) {
         // Avoid the disk read for user builds
         if (Utilities.IS_DEBUG_DEVICE) {
diff --git a/src/com/android/launcher3/folder/FolderPagedView.java b/src/com/android/launcher3/folder/FolderPagedView.java
index 54b363e..3b5fd59 100644
--- a/src/com/android/launcher3/folder/FolderPagedView.java
+++ b/src/com/android/launcher3/folder/FolderPagedView.java
@@ -277,6 +277,7 @@
             page.removeAllViews();
             pages.add(page);
         }
+        mOrganizer.setFolderInfo(mFolder.getInfo());
         setupContentDimensions(itemCount);
 
         Iterator<CellLayout> pageItr = pages.iterator();
@@ -285,7 +286,6 @@
         int position = 0;
         int rank = 0;
 
-        mOrganizer.setFolderInfo(mFolder.getInfo());
         for (int i = 0; i < itemCount; i++) {
             View v = list.size() > i ? list.get(i) : null;
             if (currentPage == null || position >= mOrganizer.getMaxItemsPerPage()) {
diff --git a/src/com/android/launcher3/icons/IconCache.java b/src/com/android/launcher3/icons/IconCache.java
index f7faca6..4ac6ff4 100644
--- a/src/com/android/launcher3/icons/IconCache.java
+++ b/src/com/android/launcher3/icons/IconCache.java
@@ -28,6 +28,7 @@
 import android.content.pm.PackageInstaller;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ShortcutInfo;
 import android.graphics.drawable.Drawable;
 import android.os.Handler;
 import android.os.Process;
@@ -49,6 +50,7 @@
 import com.android.launcher3.icons.cache.CachingLogic;
 import com.android.launcher3.icons.cache.HandlerRunnable;
 import com.android.launcher3.model.PackageItemInfo;
+import com.android.launcher3.shortcuts.ShortcutKey;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.InstantAppResolver;
 import com.android.launcher3.util.PackageUserKey;
@@ -65,6 +67,7 @@
 
     private final CachingLogic<ComponentWithLabel> mComponentWithLabelCachingLogic;
     private final CachingLogic<LauncherActivityInfo> mLauncherActivityInfoCachingLogic;
+    private final CachingLogic<ShortcutInfo> mShortcutCachingLogic;
 
     private final LauncherApps mLauncherApps;
     private final UserManagerCompat mUserManager;
@@ -78,6 +81,7 @@
                 inv.fillResIconDpi, inv.iconBitmapSize, true /* inMemoryCache */);
         mComponentWithLabelCachingLogic = new ComponentCachingLogic(context, false);
         mLauncherActivityInfoCachingLogic = LauncherActivityCachingLogic.newInstance(context);
+        mShortcutCachingLogic = new ShortcutCachingLogic();
         mLauncherApps = mContext.getSystemService(LauncherApps.class);
         mUserManager = UserManagerCompat.getInstance(mContext);
         mInstantAppResolver = InstantAppResolver.newInstance(mContext);
@@ -176,6 +180,14 @@
     }
 
     /**
+     * Fill in info with the icon and label for deep shortcut.
+     */
+    public synchronized CacheEntry getDeepShortcutTitleAndIcon(ShortcutInfo info) {
+        return cacheLocked(ShortcutKey.fromInfo(info).componentName, info.getUserHandle(),
+                () -> info, mShortcutCachingLogic, false, false);
+    }
+
+    /**
      * Fill in {@param info} with the icon and label. If the
      * corresponding activity is not found, it reverts to the package icon.
      */
diff --git a/src/com/android/launcher3/icons/LauncherIcons.java b/src/com/android/launcher3/icons/LauncherIcons.java
index 3c4041a..4d3599e 100644
--- a/src/com/android/launcher3/icons/LauncherIcons.java
+++ b/src/com/android/launcher3/icons/LauncherIcons.java
@@ -25,12 +25,14 @@
 import android.os.Process;
 
 import androidx.annotation.Nullable;
+import androidx.annotation.WorkerThread;
 
 import com.android.launcher3.AppInfo;
 import com.android.launcher3.FastBitmapDrawable;
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.ItemInfoWithIcon;
 import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.graphics.IconShape;
 import com.android.launcher3.model.PackageItemInfo;
 import com.android.launcher3.shortcuts.DeepShortcutManager;
@@ -112,7 +114,6 @@
     }
 
     // below methods should also migrate to BaseIconFactory
-
     public BitmapInfo createShortcutIcon(ShortcutInfo shortcutInfo) {
         return createShortcutIcon(shortcutInfo, true /* badged */);
     }
@@ -121,12 +122,20 @@
         return createShortcutIcon(shortcutInfo, badged, null);
     }
 
-    public BitmapInfo createShortcutIcon(ShortcutInfo shortcutInfo,
-            boolean badged, @Nullable Supplier<ItemInfoWithIcon> fallbackIconProvider) {
+    public BitmapInfo createShortcutIcon(ShortcutInfo shortcutInfo, boolean badged,
+            @Nullable Supplier<ItemInfoWithIcon> fallbackIconProvider) {
+        if (FeatureFlags.ENABLE_DEEP_SHORTCUT_ICON_CACHE.get()) {
+            return createShortcutIconCached(shortcutInfo, badged, true, fallbackIconProvider);
+        } else {
+            return createShortcutIconLegacy(shortcutInfo, badged, fallbackIconProvider);
+        }
+    }
+
+    public BitmapInfo createShortcutIconLegacy(ShortcutInfo shortcutInfo, boolean badged,
+            @Nullable Supplier<ItemInfoWithIcon> fallbackIconProvider) {
         Drawable unbadgedDrawable = DeepShortcutManager.getInstance(mContext)
                 .getShortcutIconDrawable(shortcutInfo, mFillResIconDpi);
         IconCache cache = LauncherAppState.getInstance(mContext).getIconCache();
-
         final Bitmap unbadgedBitmap;
         if (unbadgedDrawable != null) {
             unbadgedBitmap = createScaledBitmapWithoutShadow(unbadgedDrawable, 0);
@@ -155,6 +164,44 @@
         return BitmapInfo.of(icon, badge.bitmap.color);
     }
 
+    @WorkerThread
+    public BitmapInfo createShortcutIconCached(ShortcutInfo shortcutInfo, boolean badged,
+            boolean useCache, @Nullable Supplier<ItemInfoWithIcon> fallbackIconProvider) {
+        IconCache cache = LauncherAppState.getInstance(mContext).getIconCache();
+        final BitmapInfo bitmapInfo;
+        if (useCache) {
+            bitmapInfo = cache.getDeepShortcutTitleAndIcon(shortcutInfo).bitmap;
+        } else {
+            bitmapInfo = new ShortcutCachingLogic().loadIcon(mContext, shortcutInfo);
+        }
+        final Bitmap unbadgedBitmap;
+        if (bitmapInfo.icon != null) {
+            unbadgedBitmap = bitmapInfo.icon;
+        } else {
+            if (fallbackIconProvider != null) {
+                // Fallback icons are already badged and with appropriate shadow
+                ItemInfoWithIcon fullIcon = fallbackIconProvider.get();
+                if (fullIcon != null && fullIcon.bitmap != null) {
+                    return fullIcon.bitmap;
+                }
+            }
+            unbadgedBitmap = cache.getDefaultIcon(Process.myUserHandle()).icon;
+        }
+
+        if (!badged) {
+            return BitmapInfo.of(unbadgedBitmap, Themes.getColorAccent(mContext));
+        }
+
+        final Bitmap unbadgedfinal = unbadgedBitmap;
+        final ItemInfoWithIcon badge = getShortcutInfoBadge(shortcutInfo, cache);
+
+        Bitmap icon = BitmapRenderer.createHardwareBitmap(mIconBitmapSize, mIconBitmapSize, (c) -> {
+            getShadowGenerator().recreateIcon(unbadgedfinal, c);
+            badgeWithDrawable(c, new FastBitmapDrawable(badge.bitmap));
+        });
+        return BitmapInfo.of(icon, badge.bitmap.color);
+    }
+
     public ItemInfoWithIcon getShortcutInfoBadge(ShortcutInfo shortcutInfo, IconCache cache) {
         ComponentName cn = shortcutInfo.getActivity();
         String badgePkg = shortcutInfo.getPackage();
diff --git a/src/com/android/launcher3/icons/ShortcutCachingLogic.java b/src/com/android/launcher3/icons/ShortcutCachingLogic.java
new file mode 100644
index 0000000..5c21470
--- /dev/null
+++ b/src/com/android/launcher3/icons/ShortcutCachingLogic.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2019 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.launcher3.icons;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.ShortcutInfo;
+import android.graphics.drawable.Drawable;
+import android.os.UserHandle;
+
+import androidx.annotation.NonNull;
+
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.icons.cache.CachingLogic;
+import com.android.launcher3.shortcuts.DeepShortcutManager;
+import com.android.launcher3.shortcuts.ShortcutKey;
+import com.android.launcher3.util.Themes;
+
+/**
+ * Caching logic for shortcuts.
+ */
+public class ShortcutCachingLogic implements CachingLogic<ShortcutInfo> {
+
+    @Override
+    public ComponentName getComponent(ShortcutInfo info) {
+        return ShortcutKey.fromInfo(info).componentName;
+    }
+
+    @Override
+    public UserHandle getUser(ShortcutInfo info) {
+        return info.getUserHandle();
+    }
+
+    @Override
+    public CharSequence getLabel(ShortcutInfo info) {
+        return info.getShortLabel();
+    }
+
+    @NonNull
+    @Override
+    public BitmapInfo loadIcon(Context context, ShortcutInfo info) {
+        try (LauncherIcons li = LauncherIcons.obtain(context)) {
+            Drawable unbadgedDrawable = DeepShortcutManager.getInstance(context)
+                    .getShortcutIconDrawable(info, LauncherAppState.getIDP(context).fillResIconDpi);
+            if (unbadgedDrawable == null) return BitmapInfo.LOW_RES_INFO;
+            return new BitmapInfo(li.createScaledBitmapWithoutShadow(
+                    unbadgedDrawable, 0), Themes.getColorAccent(context));
+        }
+    }
+
+    @Override
+    public long getLastUpdatedTime(ShortcutInfo shortcutInfo, PackageInfo info) {
+        if (shortcutInfo == null || !FeatureFlags.ENABLE_DEEP_SHORTCUT_ICON_CACHE.get()) {
+            return info.lastUpdateTime;
+        }
+        return Math.max(shortcutInfo.getLastChangedTimestamp(), info.lastUpdateTime);
+    }
+
+    @Override
+    public boolean addToMemCache() {
+        return false;
+    }
+}
diff --git a/src/com/android/launcher3/model/AddWorkspaceItemsTask.java b/src/com/android/launcher3/model/AddWorkspaceItemsTask.java
index b46f465..fa0fe1b 100644
--- a/src/com/android/launcher3/model/AddWorkspaceItemsTask.java
+++ b/src/com/android/launcher3/model/AddWorkspaceItemsTask.java
@@ -140,6 +140,15 @@
                         // or app was already installed for another user.
                         itemInfo = new AppInfo(app.getContext(), activities.get(0), item.user)
                                 .makeWorkspaceItem();
+
+                        if (shortcutExists(dataModel, itemInfo.getIntent(), itemInfo.user)) {
+                            // We need this additional check here since we treat all auto added
+                            // workspace items as promise icons. At this point we now have the
+                            // correct intent to compare against existing workspace icons.
+                            // Icon already exists on the workspace and should not be auto-added.
+                            continue;
+                        }
+
                         WorkspaceItemInfo wii = (WorkspaceItemInfo) itemInfo;
                         wii.title = "";
                         wii.bitmap = app.getIconCache().getDefaultIcon(item.user);
diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java
index 6383a5f..3a4085c 100644
--- a/src/com/android/launcher3/model/LoaderTask.java
+++ b/src/com/android/launcher3/model/LoaderTask.java
@@ -63,6 +63,7 @@
 import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.icons.LauncherActivityCachingLogic;
 import com.android.launcher3.icons.LauncherIcons;
+import com.android.launcher3.icons.ShortcutCachingLogic;
 import com.android.launcher3.icons.cache.IconCacheUpdateHandler;
 import com.android.launcher3.logging.FileLog;
 import com.android.launcher3.pm.PackageInstallInfo;
@@ -71,7 +72,6 @@
 import com.android.launcher3.qsb.QsbContainerView;
 import com.android.launcher3.shortcuts.DeepShortcutManager;
 import com.android.launcher3.shortcuts.ShortcutKey;
-import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.IOUtils;
 import com.android.launcher3.util.LooperIdleLock;
@@ -174,7 +174,8 @@
         Object traceToken = TraceHelper.INSTANCE.beginSection(TAG);
         TimingLogger logger = new TimingLogger(TAG, "run");
         try (LauncherModel.LoaderTransaction transaction = mApp.getModel().beginLoader(this)) {
-            loadWorkspace();
+            List<ShortcutInfo> allShortcuts = new ArrayList<>();
+            loadWorkspace(allShortcuts);
             logger.addSplit("loadWorkspace");
 
             verifyNotStopped();
@@ -206,19 +207,33 @@
                     mApp.getModel()::onPackageIconsUpdated);
             logger.addSplit("update icon cache");
 
+            if (FeatureFlags.ENABLE_DEEP_SHORTCUT_ICON_CACHE.get()) {
+                verifyNotStopped();
+                logger.addSplit("save shortcuts in icon cache");
+                updateHandler.updateIcons(allShortcuts, new ShortcutCachingLogic(),
+                        mApp.getModel()::onPackageIconsUpdated);
+            }
+
             // Take a break
             waitForIdle();
             logger.addSplit("step 2 complete");
             verifyNotStopped();
 
             // third step
-            loadDeepShortcuts();
+            List<ShortcutInfo> allDeepShortcuts = loadDeepShortcuts();
             logger.addSplit("loadDeepShortcuts");
 
             verifyNotStopped();
             mResults.bindDeepShortcuts();
             logger.addSplit("bindDeepShortcuts");
 
+            if (FeatureFlags.ENABLE_DEEP_SHORTCUT_ICON_CACHE.get()) {
+                verifyNotStopped();
+                logger.addSplit("save deep shortcuts in icon cache");
+                updateHandler.updateIcons(allDeepShortcuts,
+                        new ShortcutCachingLogic(), (pkgs, user) -> { });
+            }
+
             // Take a break
             waitForIdle();
             logger.addSplit("step 3 complete");
@@ -256,7 +271,7 @@
         this.notify();
     }
 
-    private void loadWorkspace() {
+    private void loadWorkspace(List<ShortcutInfo> allDeepShortcuts) {
         final Context context = mApp.getContext();
         final ContentResolver contentResolver = context.getContentResolver();
         final PackageManagerHelper pmHelper = new PackageManagerHelper(context);
@@ -512,6 +527,7 @@
                                         info.runtimeStatusFlags |= FLAG_DISABLED_SUSPENDED;
                                     }
                                     intent = info.intent;
+                                    allDeepShortcuts.add(pinnedShortcut);
                                 } else {
                                     // Create a shortcut info in disabled mode for now.
                                     info = c.loadSimpleWorkspaceItem();
@@ -860,7 +876,8 @@
         return allActivityList;
     }
 
-    private void loadDeepShortcuts() {
+    private List<ShortcutInfo> loadDeepShortcuts() {
+        List<ShortcutInfo> allShortcuts = new ArrayList<>();
         mBgDataModel.deepShortcutMap.clear();
         mBgDataModel.hasShortcutHostPermission = mShortcutManager.hasHostPermission();
         if (mBgDataModel.hasShortcutHostPermission) {
@@ -868,10 +885,12 @@
                 if (mUserManager.isUserUnlocked(user)) {
                     List<ShortcutInfo> shortcuts =
                             mShortcutManager.queryForAllShortcuts(user);
+                    allShortcuts.addAll(shortcuts);
                     mBgDataModel.updateDeepShortcutCounts(null, user, shortcuts);
                 }
             }
         }
+        return allShortcuts;
     }
 
     public static boolean isValidProvider(AppWidgetProviderInfo provider) {
diff --git a/src/com/android/launcher3/pm/PinRequestHelper.java b/src/com/android/launcher3/pm/PinRequestHelper.java
index e13645c..a15399d 100644
--- a/src/com/android/launcher3/pm/PinRequestHelper.java
+++ b/src/com/android/launcher3/pm/PinRequestHelper.java
@@ -17,6 +17,7 @@
 package com.android.launcher3.pm;
 
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
+import static com.android.launcher3.util.ShortcutUtil.fetchAndUpdateShortcutIconAsync;
 
 import android.annotation.TargetApi;
 import android.content.Context;
@@ -31,6 +32,7 @@
 
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.WorkspaceItemInfo;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.icons.LauncherIcons;
 
 public class PinRequestHelper {
@@ -81,11 +83,15 @@
             ShortcutInfo si = request.getShortcutInfo();
             WorkspaceItemInfo info = new WorkspaceItemInfo(si, context);
             // Apply the unbadged icon and fetch the actual icon asynchronously.
-            LauncherIcons li = LauncherIcons.obtain(context);
-            info.bitmap = li.createShortcutIcon(si, false /* badged */);
-            li.recycle();
-            LauncherAppState.getInstance(context).getModel()
-                    .updateAndBindWorkspaceItem(info, si);
+            if (FeatureFlags.ENABLE_DEEP_SHORTCUT_ICON_CACHE.get()) {
+                fetchAndUpdateShortcutIconAsync(context, info, si, false);
+            } else {
+                LauncherIcons li = LauncherIcons.obtain(context);
+                info.bitmap = li.createShortcutIcon(si, false /* badged */);
+                li.recycle();
+                LauncherAppState.getInstance(context).getModel()
+                        .updateAndBindWorkspaceItem(info, si);
+            }
             return info;
         } else {
             return null;
diff --git a/src/com/android/launcher3/shortcuts/ShortcutDragPreviewProvider.java b/src/com/android/launcher3/shortcuts/ShortcutDragPreviewProvider.java
index ee97641..3e59b61 100644
--- a/src/com/android/launcher3/shortcuts/ShortcutDragPreviewProvider.java
+++ b/src/com/android/launcher3/shortcuts/ShortcutDragPreviewProvider.java
@@ -25,7 +25,9 @@
 
 import com.android.launcher3.Launcher;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.graphics.DragPreviewProvider;
+import com.android.launcher3.icons.BitmapRenderer;
 
 /**
  * Extension of {@link DragPreviewProvider} which generates bitmaps scaled to the default icon size.
@@ -39,16 +41,27 @@
         mPositionShift = shift;
     }
 
+    @Override
     public Bitmap createDragBitmap() {
+        if (FeatureFlags.ENABLE_DEEP_SHORTCUT_ICON_CACHE.get()) {
+            int size = Launcher.getLauncher(mView.getContext()).getDeviceProfile().iconSizePx;
+            return BitmapRenderer.createHardwareBitmap(
+                    size + blurSizeOutline,
+                    size + blurSizeOutline,
+                    (c) -> drawDragViewOnBackground(c, size));
+        } else {
+            return createDragBitmapLegacy();
+        }
+    }
+
+    private Bitmap createDragBitmapLegacy() {
         Drawable d = mView.getBackground();
         Rect bounds = getDrawableBounds(d);
-
         int size = Launcher.getLauncher(mView.getContext()).getDeviceProfile().iconSizePx;
         final Bitmap b = Bitmap.createBitmap(
                 size + blurSizeOutline,
                 size + blurSizeOutline,
                 Bitmap.Config.ARGB_8888);
-
         Canvas canvas = new Canvas(b);
         canvas.translate(blurSizeOutline / 2, blurSizeOutline / 2);
         canvas.scale(((float) size) / bounds.width(), ((float) size) / bounds.height(), 0, 0);
@@ -57,6 +70,16 @@
         return b;
     }
 
+    private void drawDragViewOnBackground(Canvas canvas, float size) {
+        Drawable d = mView.getBackground();
+        Rect bounds = getDrawableBounds(d);
+
+        canvas.translate(blurSizeOutline / 2, blurSizeOutline / 2);
+        canvas.scale(size / bounds.width(), size / bounds.height(), 0, 0);
+        canvas.translate(bounds.left, bounds.top);
+        d.draw(canvas);
+    }
+
     @Override
     public float getScaleAndPosition(Bitmap preview, int[] outPos) {
         Launcher launcher = Launcher.getLauncher(mView.getContext());
diff --git a/src/com/android/launcher3/touch/BaseSwipeDetector.java b/src/com/android/launcher3/touch/BaseSwipeDetector.java
index 08d73d0..12ca5ee 100644
--- a/src/com/android/launcher3/touch/BaseSwipeDetector.java
+++ b/src/com/android/launcher3/touch/BaseSwipeDetector.java
@@ -33,6 +33,7 @@
  * swipe action happens.
  *
  * @see SingleAxisSwipeDetector
+ * @see BothAxesSwipeDetector
  */
 public abstract class BaseSwipeDetector {
 
diff --git a/src/com/android/launcher3/touch/BothAxesSwipeDetector.java b/src/com/android/launcher3/touch/BothAxesSwipeDetector.java
new file mode 100644
index 0000000..944391e
--- /dev/null
+++ b/src/com/android/launcher3/touch/BothAxesSwipeDetector.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2019 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.launcher3.touch;
+
+import android.content.Context;
+import android.graphics.PointF;
+import android.view.MotionEvent;
+import android.view.ViewConfiguration;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.launcher3.Utilities;
+
+/**
+ * Two dimensional scroll/drag/swipe gesture detector that reports x and y displacement/velocity.
+ */
+public class BothAxesSwipeDetector extends BaseSwipeDetector {
+
+    public static final int DIRECTION_UP = 1 << 0;
+    // Note that this will track left instead of right in RTL.
+    public static final int DIRECTION_RIGHT = 1 << 1;
+    public static final int DIRECTION_DOWN = 1 << 2;
+    // Note that this will track right instead of left in RTL.
+    public static final int DIRECTION_LEFT = 1 << 3;
+
+    /* Client of this gesture detector can register a callback. */
+    private final Listener mListener;
+
+    private int mScrollDirections;
+
+    public BothAxesSwipeDetector(@NonNull Context context, @NonNull Listener l) {
+        this(ViewConfiguration.get(context), l, Utilities.isRtl(context.getResources()));
+    }
+
+    @VisibleForTesting
+    protected BothAxesSwipeDetector(@NonNull ViewConfiguration config, @NonNull Listener l,
+            boolean isRtl) {
+        super(config, isRtl);
+        mListener = l;
+    }
+
+    public void setDetectableScrollConditions(int scrollDirectionFlags, boolean ignoreSlop) {
+        mScrollDirections = scrollDirectionFlags;
+        mIgnoreSlopWhenSettling = ignoreSlop;
+    }
+
+    @Override
+    protected boolean shouldScrollStart(PointF displacement) {
+        // Check if the client is interested in scroll in current direction.
+        boolean canScrollUp = (mScrollDirections & DIRECTION_UP) > 0
+                && displacement.y <= -mTouchSlop;
+        boolean canScrollRight = (mScrollDirections & DIRECTION_RIGHT) > 0
+                && displacement.x >= mTouchSlop;
+        boolean canScrollDown = (mScrollDirections & DIRECTION_DOWN) > 0
+                && displacement.y >= mTouchSlop;
+        boolean canScrollLeft = (mScrollDirections & DIRECTION_LEFT) > 0
+                && displacement.x <= -mTouchSlop;
+        return canScrollUp || canScrollRight || canScrollDown || canScrollLeft;
+    }
+
+    @Override
+    protected void reportDragStartInternal(boolean recatch) {
+        mListener.onDragStart(!recatch);
+    }
+
+    @Override
+    protected void reportDraggingInternal(PointF displacement, MotionEvent event) {
+        mListener.onDrag(displacement, event);
+    }
+
+    @Override
+    protected void reportDragEndInternal(PointF velocity) {
+        mListener.onDragEnd(velocity);
+    }
+
+    /** Listener to receive updates on the swipe. */
+    public interface Listener {
+        /** @param start whether this was the original drag start, as opposed to a recatch. */
+        void onDragStart(boolean start);
+
+        boolean onDrag(PointF displacement, MotionEvent motionEvent);
+
+        void onDragEnd(PointF velocity);
+    }
+}
diff --git a/src/com/android/launcher3/touch/SingleAxisSwipeDetector.java b/src/com/android/launcher3/touch/SingleAxisSwipeDetector.java
index 0bf2ff6..f2ebc45 100644
--- a/src/com/android/launcher3/touch/SingleAxisSwipeDetector.java
+++ b/src/com/android/launcher3/touch/SingleAxisSwipeDetector.java
@@ -161,6 +161,7 @@
 
     /** Listener to receive updates on the swipe. */
     public interface Listener {
+        /** @param start whether this was the original drag start, as opposed to a recatch. */
         void onDragStart(boolean start);
 
         // TODO remove
diff --git a/src/com/android/launcher3/util/ActivityTracker.java b/src/com/android/launcher3/util/ActivityTracker.java
index 4b931f4..499f655 100644
--- a/src/com/android/launcher3/util/ActivityTracker.java
+++ b/src/com/android/launcher3/util/ActivityTracker.java
@@ -50,22 +50,41 @@
         }
     }
 
-    public void schedule(SchedulerCallback<? extends T> callback) {
+    /**
+     * Schedules the callback to be notified when the activity is created.
+     * @return true if the activity is already created, false otherwise
+     */
+    public boolean schedule(SchedulerCallback<? extends T> callback) {
         synchronized (this) {
             mPendingCallback = new WeakReference<>((SchedulerCallback<T>) callback);
         }
-        MAIN_EXECUTOR.execute(this);
+        if (!notifyInitIfPending()) {
+            // If the activity doesn't already exist, then post and wait for the activity to start
+            MAIN_EXECUTOR.execute(this);
+            return false;
+        }
+        return true;
     }
 
     @Override
     public void run() {
-        T activity = mCurrentActivity.get();
-        if (activity != null) {
-            initIfPending(activity, activity.isStarted());
-        }
+        notifyInitIfPending();
     }
 
-    public boolean initIfPending(T activity, boolean alreadyOnHome) {
+    /**
+     * Notifies the pending callback if the activity is now created.
+     * @return true if the activity is now created.
+     */
+    private boolean notifyInitIfPending() {
+        T activity = mCurrentActivity.get();
+        if (activity != null) {
+            notifyInitIfPending(activity, activity.isStarted());
+            return true;
+        }
+        return false;
+    }
+
+    public boolean notifyInitIfPending(T activity, boolean alreadyOnHome) {
         SchedulerCallback<T> pendingCallback = mPendingCallback.get();
         if (pendingCallback != null) {
             if (!pendingCallback.init(activity, alreadyOnHome)) {
@@ -114,7 +133,7 @@
             }
         }
         if (!result && !explicitIntent) {
-            result = initIfPending(activity, alreadyOnHome);
+            result = notifyInitIfPending(activity, alreadyOnHome);
         }
         return result;
     }
diff --git a/src/com/android/launcher3/util/ShortcutUtil.java b/src/com/android/launcher3/util/ShortcutUtil.java
index 49c97da..a03b743 100644
--- a/src/com/android/launcher3/util/ShortcutUtil.java
+++ b/src/com/android/launcher3/util/ShortcutUtil.java
@@ -15,10 +15,19 @@
  */
 package com.android.launcher3.util;
 
+import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
+
+import android.content.Context;
+import android.content.pm.ShortcutInfo;
+
+import androidx.annotation.NonNull;
+
 import com.android.launcher3.ItemInfo;
+import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.WorkspaceItemInfo;
+import com.android.launcher3.icons.LauncherIcons;
 import com.android.launcher3.model.WidgetsModel;
 import com.android.launcher3.shortcuts.ShortcutKey;
 
@@ -61,6 +70,21 @@
                 && info instanceof WorkspaceItemInfo;
     }
 
+    /**
+     * Fetch the shortcut icon in background, then update the UI.
+     */
+    public static void fetchAndUpdateShortcutIconAsync(
+            @NonNull Context context, @NonNull WorkspaceItemInfo info, @NonNull ShortcutInfo si,
+            boolean badged) {
+        MODEL_EXECUTOR.execute(() -> {
+            try (LauncherIcons li = LauncherIcons.obtain(context)) {
+                info.bitmap = li.createShortcutIcon(si, badged, null);
+                LauncherAppState.getInstance(context).getModel()
+                        .updateAndBindWorkspaceItem(info, si);
+            }
+        });
+    }
+
     private static boolean isActive(ItemInfo info) {
         boolean isLoading = info instanceof WorkspaceItemInfo
                 && ((WorkspaceItemInfo) info).hasPromiseIconUi();
diff --git a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
index 76b5d28..bb19515 100644
--- a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
+++ b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
@@ -26,8 +26,6 @@
 
 import static java.lang.System.exit;
 
-import static androidx.test.InstrumentationRegistry.getInstrumentation;
-
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.ContentResolver;
@@ -306,7 +304,7 @@
     protected void waitForLauncherCondition(
             String message, Function<Launcher, Boolean> condition, long timeout) {
         if (!TestHelpers.isInLauncherProcess()) return;
-        Wait.atMost(message, () -> getFromLauncher(condition), timeout);
+        Wait.atMost(message, () -> getFromLauncher(condition), timeout, mLauncher);
     }
 
     // Cannot be used in TaplTests after injecting any gesture using Tapl because this can hide
@@ -319,7 +317,7 @@
             final Object fromLauncher = getFromLauncher(f);
             output[0] = fromLauncher;
             return fromLauncher != null;
-        }, timeout);
+        }, timeout, mLauncher);
         return (T) output[0];
     }
 
@@ -333,7 +331,7 @@
         Wait.atMost(message, () -> {
             testThreadAction.run();
             return getFromLauncher(condition);
-        }, timeout);
+        }, timeout, mLauncher);
     }
 
     protected LauncherActivityInfo getSettingsApp() {
diff --git a/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java
index e1b3ede..0472ce1 100644
--- a/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java
@@ -103,12 +103,12 @@
 
         setResult(acceptConfig);
         if (acceptConfig) {
-            Wait.atMost(null, new WidgetSearchCondition(), DEFAULT_ACTIVITY_TIMEOUT);
+            Wait.atMost(null, new WidgetSearchCondition(), DEFAULT_ACTIVITY_TIMEOUT, mLauncher);
             assertNotNull(mAppWidgetManager.getAppWidgetInfo(mWidgetId));
         } else {
             // Verify that the widget id is deleted.
             Wait.atMost(null, () -> mAppWidgetManager.getAppWidgetInfo(mWidgetId) == null,
-                    DEFAULT_ACTIVITY_TIMEOUT);
+                    DEFAULT_ACTIVITY_TIMEOUT, mLauncher);
         }
     }
 
diff --git a/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java b/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java
index 07129dd..d909ad7 100644
--- a/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java
@@ -59,7 +59,8 @@
 @RunWith(AndroidJUnit4.class)
 public class RequestPinItemTest extends AbstractLauncherUiTest {
 
-    @Rule public ShellCommandRule mGrantWidgetRule = ShellCommandRule.grantWidgetBind();
+    @Rule
+    public ShellCommandRule mGrantWidgetRule = ShellCommandRule.grantWidgetBind();
 
     private String mCallbackAction;
     private String mShortcutId;
@@ -84,10 +85,10 @@
                         .equals(AppWidgetNoConfig.class.getName()));
     }
 
-        @Test
+    @Test
     public void testPinWidgetNoConfig_customPreview() throws Throwable {
         // Command to set custom preview
-        Intent command =  RequestPinItemActivity.getCommandIntent(
+        Intent command = RequestPinItemActivity.getCommandIntent(
                 RequestPinItemActivity.class, "setRemoteViewColor").putExtra(
                 RequestPinItemActivity.EXTRA_PARAM + "0", Color.RED);
 
@@ -169,7 +170,8 @@
 
         // Go back to home
         mLauncher.pressHome();
-        Wait.atMost(null, new ItemSearchCondition(itemMatcher), DEFAULT_ACTIVITY_TIMEOUT);
+        Wait.atMost(null, new ItemSearchCondition(itemMatcher), DEFAULT_ACTIVITY_TIMEOUT,
+                mLauncher);
     }
 
     /**
diff --git a/tests/src/com/android/launcher3/util/Wait.java b/tests/src/com/android/launcher3/util/Wait.java
index 899686b..2663d02 100644
--- a/tests/src/com/android/launcher3/util/Wait.java
+++ b/tests/src/com/android/launcher3/util/Wait.java
@@ -3,6 +3,8 @@
 import android.os.SystemClock;
 import android.util.Log;
 
+import com.android.launcher3.tapl.LauncherInstrumentation;
+
 import org.junit.Assert;
 
 /**
@@ -12,11 +14,13 @@
 
     private static final long DEFAULT_SLEEP_MS = 200;
 
-    public static void atMost(String message, Condition condition, long timeout) {
-        atMost(message, condition, timeout, DEFAULT_SLEEP_MS);
+    public static void atMost(String message, Condition condition, long timeout,
+            LauncherInstrumentation launcher) {
+        atMost(message, condition, timeout, DEFAULT_SLEEP_MS, launcher);
     }
 
-    public static void atMost(String message, Condition condition, long timeout, long sleepMillis) {
+    public static void atMost(String message, Condition condition, long timeout, long sleepMillis,
+            LauncherInstrumentation launcher) {
         final long startTime = SystemClock.uptimeMillis();
         long endTime = startTime + timeout;
         Log.d("Wait", "atMost: " + startTime + " - " + endTime);
@@ -40,6 +44,7 @@
             throw new RuntimeException(t);
         }
         Log.d("Wait", "atMost: timed out: " + SystemClock.uptimeMillis());
+        launcher.checkForAnomaly();
         Assert.fail(message);
     }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index 3713c14..1c851f4 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -64,10 +64,12 @@
 import com.android.launcher3.testing.TestProtocol;
 import com.android.systemui.shared.system.QuickStepContract;
 
-import java.util.ArrayList;
+import org.junit.Assert;
+
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.lang.ref.WeakReference;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Deque;
@@ -77,8 +79,6 @@
 import java.util.function.Consumer;
 import java.util.function.Function;
 
-import org.junit.Assert;
-
 /**
  * The main tapl object. The only object that can be explicitly constructed by the using code. It
  * produces all other objects.
@@ -298,6 +298,14 @@
         return null;
     }
 
+    public void checkForAnomaly() {
+        final String anomalyMessage = getAnomalyMessage();
+        if (anomalyMessage != null) {
+            failWithSystemHealth(
+                    "Tests are broken by a non-Launcher system error: " + anomalyMessage);
+        }
+    }
+
     private String getVisibleStateMessage() {
         if (hasLauncherObject(WIDGETS_RES_ID)) return "Widgets";
         if (hasLauncherObject(OVERVIEW_RES_ID)) return "Overview";
@@ -331,20 +339,17 @@
     }
 
     private void fail(String message) {
-        message = "http://go/tapl : " + getContextDescription() + message;
+        checkForAnomaly();
 
-        final String anomaly = getAnomalyMessage();
-        if (anomaly != null) {
-            message = anomaly + ", which causes:\n" + message;
-        } else {
-            message = message + " (visible state: " + getVisibleStateMessage() + ")";
-        }
+        failWithSystemHealth("http://go/tapl : " + getContextDescription() + message +
+                " (visible state: " + getVisibleStateMessage() + ")");
+    }
 
+    private void failWithSystemHealth(String message) {
         final String systemHealth = getSystemHealthMessage();
         if (systemHealth != null) {
             message = message
-                    + ", which might be a consequence of system health "
-                    + "problems:\n<<<<<<<<<<<<<<<<<<\n"
+                    + ", perhaps because of system health problems:\n<<<<<<<<<<<<<<<<<<\n"
                     + systemHealth + "\n>>>>>>>>>>>>>>>>>>";
         }
 
@@ -531,8 +536,7 @@
         // accessibility events prior to pressing Home.
         final String action;
         if (getNavigationModel() == NavigationModel.ZERO_BUTTON) {
-            final String anomaly = getAnomalyMessage();
-            if (anomaly != null) fail("Can't swipe up to Home: " + anomaly);
+            checkForAnomaly();
 
             final Point displaySize = getRealDisplaySize();