Merge "Adds zakcohen@ to OWNERS." into ub-launcher3-master
diff --git a/go/quickstep/src/com/android/quickstep/views/IconRecentsView.java b/go/quickstep/src/com/android/quickstep/views/IconRecentsView.java
index 87b4d4e..e380698 100644
--- a/go/quickstep/src/com/android/quickstep/views/IconRecentsView.java
+++ b/go/quickstep/src/com/android/quickstep/views/IconRecentsView.java
@@ -40,6 +40,7 @@
 import android.graphics.Rect;
 import android.graphics.RectF;
 import android.graphics.drawable.Drawable;
+import android.os.UserHandle;
 import android.util.ArraySet;
 import android.util.AttributeSet;
 import android.util.FloatProperty;
@@ -66,6 +67,7 @@
 import com.android.launcher3.util.Themes;
 import com.android.quickstep.ContentFillItemAnimator;
 import com.android.quickstep.RecentsModel;
+import com.android.quickstep.RecentsModel.TaskVisualsChangeListener;
 import com.android.quickstep.RecentsToActivityHelper;
 import com.android.quickstep.TaskActionController;
 import com.android.quickstep.TaskAdapter;
@@ -74,6 +76,7 @@
 import com.android.quickstep.TaskSwipeCallback;
 import com.android.quickstep.util.MultiValueUpdateListener;
 import com.android.systemui.shared.recents.model.Task;
+import com.android.systemui.shared.recents.model.ThumbnailData;
 import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
 import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat;
 import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat.SurfaceParams;
@@ -87,7 +90,8 @@
  * Root view for the icon recents view. Acts as the main interface to the rest of the Launcher code
  * base.
  */
-public final class IconRecentsView extends FrameLayout implements Insettable {
+public final class IconRecentsView extends FrameLayout
+        implements Insettable, TaskVisualsChangeListener {
 
     public static final FloatProperty<IconRecentsView> CONTENT_ALPHA =
             new FloatProperty<IconRecentsView>("contentAlpha") {
@@ -159,22 +163,6 @@
     private AnimatorSet mLayoutAnimation;
     private final ArraySet<View> mLayingOutViews = new ArraySet<>();
     private Rect mInsets;
-    private final RecentsModel.TaskThumbnailChangeListener listener = (taskId, thumbnailData) -> {
-        ArrayList<TaskItemView> itemViews = getTaskViews();
-        for (int i = 0, size = itemViews.size(); i < size; i++) {
-            TaskItemView taskView = itemViews.get(i);
-            TaskHolder taskHolder = (TaskHolder) mTaskRecyclerView.getChildViewHolder(taskView);
-            Optional<Task> optTask = taskHolder.getTask();
-            if (optTask.filter(task -> task.key.id == taskId).isPresent()) {
-                Task task = optTask.get();
-                // Update thumbnail on the task.
-                task.thumbnail = thumbnailData;
-                taskView.setThumbnail(thumbnailData);
-                return task;
-            }
-        }
-        return null;
-    };
 
     public IconRecentsView(Context context, AttributeSet attrs) {
         super(context, attrs);
@@ -189,10 +177,30 @@
                 mActivity.getStatsLogManager());
         mTaskAdapter.setActionController(mTaskActionController);
         mTaskLayoutManager = new LinearLayoutManager(mContext, VERTICAL, true /* reverseLayout */);
-        RecentsModel.INSTANCE.get(context).addThumbnailChangeListener(listener);
     }
 
     @Override
+    public Task onTaskThumbnailChanged(int taskId, ThumbnailData thumbnailData) {
+        ArrayList<TaskItemView> itemViews = getTaskViews();
+        for (int i = 0, size = itemViews.size(); i < size; i++) {
+            TaskItemView taskView = itemViews.get(i);
+            TaskHolder taskHolder = (TaskHolder) mTaskRecyclerView.getChildViewHolder(taskView);
+            Optional<Task> optTask = taskHolder.getTask();
+            if (optTask.filter(task -> task.key.id == taskId).isPresent()) {
+                Task task = optTask.get();
+                // Update thumbnail on the task.
+                task.thumbnail = thumbnailData;
+                taskView.setThumbnail(thumbnailData);
+                return task;
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public void onTaskIconChanged(String pkg, UserHandle user) { }
+
+    @Override
     protected void onFinishInflate() {
         super.onFinishInflate();
         if (mTaskRecyclerView == null) {
@@ -275,6 +283,18 @@
     }
 
     @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        RecentsModel.INSTANCE.get(getContext()).addThumbnailChangeListener(this);
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        RecentsModel.INSTANCE.get(getContext()).removeThumbnailChangeListener(this);
+    }
+
+    @Override
     public void setEnabled(boolean enabled) {
         super.setEnabled(enabled);
         int childCount = mTaskRecyclerView.getChildCount();
diff --git a/iconloaderlib/src/com/android/launcher3/icons/BaseIconFactory.java b/iconloaderlib/src/com/android/launcher3/icons/BaseIconFactory.java
index f491ed7..535c5d8 100644
--- a/iconloaderlib/src/com/android/launcher3/icons/BaseIconFactory.java
+++ b/iconloaderlib/src/com/android/launcher3/icons/BaseIconFactory.java
@@ -116,7 +116,7 @@
             icon = createIconBitmap(new BitmapDrawable(mContext.getResources(), icon), 1f);
         }
 
-        return BitmapInfo.fromBitmap(icon, mDisableColorExtractor ? null : mColorExtractor);
+        return BitmapInfo.of(icon, extractColor(icon));
     }
 
     public BitmapInfo createBadgedIconBitmap(Drawable icon, UserHandle user,
@@ -186,7 +186,10 @@
         } else {
             result = bitmap;
         }
-        return BitmapInfo.fromBitmap(result, mDisableColorExtractor ? null : mColorExtractor);
+        int color = extractColor(result);
+        return icon instanceof BitmapInfo.Extender
+                ? ((BitmapInfo.Extender) icon).getExtendedInfo(result, color, this)
+                : BitmapInfo.of(result, color);
     }
 
     public Bitmap createScaledBitmapWithoutShadow(Drawable icon, boolean shrinkNonAdaptiveIcons) {
@@ -337,6 +340,10 @@
                 iconDpi);
     }
 
+    private int extractColor(Bitmap bitmap) {
+        return mDisableColorExtractor ? 0 : mColorExtractor.findDominantColorByHue(bitmap);
+    }
+
     /**
      * Returns the correct badge size given an icon size
      */
diff --git a/iconloaderlib/src/com/android/launcher3/icons/BitmapInfo.java b/iconloaderlib/src/com/android/launcher3/icons/BitmapInfo.java
index 245561e..d33f9b1 100644
--- a/iconloaderlib/src/com/android/launcher3/icons/BitmapInfo.java
+++ b/iconloaderlib/src/com/android/launcher3/icons/BitmapInfo.java
@@ -18,32 +18,55 @@
 import android.graphics.Bitmap;
 import android.graphics.Bitmap.Config;
 
+import androidx.annotation.NonNull;
+
 public class BitmapInfo {
 
     public static final Bitmap LOW_RES_ICON = Bitmap.createBitmap(1, 1, Config.ALPHA_8);
+    public static final BitmapInfo LOW_RES_INFO = fromBitmap(LOW_RES_ICON);
 
-    public Bitmap icon;
-    public int color;
+    public final Bitmap icon;
+    public final int color;
 
-    public void applyTo(BitmapInfo info) {
-        info.icon = icon;
-        info.color = color;
+    public BitmapInfo(Bitmap icon, int color) {
+        this.icon = icon;
+        this.color = color;
+    }
+
+    /**
+     * Ideally icon should not be null, except in cases when generating hardware bitmap failed
+     */
+    public final boolean isNullOrLowRes() {
+        return icon == null || icon == LOW_RES_ICON;
     }
 
     public final boolean isLowRes() {
         return LOW_RES_ICON == icon;
     }
 
-    public static BitmapInfo fromBitmap(Bitmap bitmap) {
-        return fromBitmap(bitmap, null);
+    public static BitmapInfo fromBitmap(@NonNull Bitmap bitmap) {
+        return of(bitmap, 0);
     }
 
-    public static BitmapInfo fromBitmap(Bitmap bitmap, ColorExtractor dominantColorExtractor) {
-        BitmapInfo info = new BitmapInfo();
-        info.icon = bitmap;
-        info.color = dominantColorExtractor != null
-                ? dominantColorExtractor.findDominantColorByHue(bitmap)
-                : 0;
-        return info;
+    public static BitmapInfo of(@NonNull Bitmap bitmap, int color) {
+        return new BitmapInfo(bitmap, color);
+    }
+
+    /**
+     * Interface to be implemented by drawables to provide a custom BitmapInfo
+     */
+    public interface Extender {
+
+        /**
+         * Called for creating a custom BitmapInfo
+         */
+        default BitmapInfo getExtendedInfo(Bitmap bitmap, int color, BaseIconFactory iconFactory) {
+            return BitmapInfo.of(bitmap, color);
+        }
+
+        /**
+         * Notifies the drawable that it will be drawn directly in the UI, without any preprocessing
+         */
+        default void prepareToDrawOnUi() { }
     }
 }
diff --git a/iconloaderlib/src/com/android/launcher3/icons/cache/BaseIconCache.java b/iconloaderlib/src/com/android/launcher3/icons/cache/BaseIconCache.java
index 93f0538..6f63d88 100644
--- a/iconloaderlib/src/com/android/launcher3/icons/cache/BaseIconCache.java
+++ b/iconloaderlib/src/com/android/launcher3/icons/cache/BaseIconCache.java
@@ -71,7 +71,10 @@
     // Empty class name is used for storing package default entry.
     public static final String EMPTY_CLASS_NAME = ".";
 
-    public static class CacheEntry extends BitmapInfo {
+    public static class CacheEntry {
+
+        @NonNull
+        public BitmapInfo bitmap = BitmapInfo.LOW_RES_INFO;
         public CharSequence title = "";
         public CharSequence contentDescription = "";
     }
@@ -259,23 +262,23 @@
         if (!replaceExisting) {
             entry = mCache.get(key);
             // We can't reuse the entry if the high-res icon is not present.
-            if (entry == null || entry.icon == null || entry.isLowRes()) {
+            if (entry == null || entry.bitmap.isNullOrLowRes()) {
                 entry = null;
             }
         }
         if (entry == null) {
             entry = new CacheEntry();
-            cachingLogic.loadIcon(mContext, object, entry);
+            entry.bitmap = cachingLogic.loadIcon(mContext, object);
         }
         // Icon can't be loaded from cachingLogic, which implies alternative icon was loaded
         // (e.g. fallback icon, default icon). So we drop here since there's no point in caching
         // an empty entry.
-        if (entry.icon == null) return;
+        if (entry.bitmap.isNullOrLowRes()) return;
         entry.title = cachingLogic.getLabel(object);
         entry.contentDescription = mPackageManager.getUserBadgedLabel(entry.title, user);
         if (cachingLogic.addToMemCache()) mCache.put(key, entry);
 
-        ContentValues values = newContentValues(entry, entry.title.toString(),
+        ContentValues values = newContentValues(entry.bitmap, entry.title.toString(),
                 componentName.getPackageName(), cachingLogic.getKeywords(object, mLocaleList));
         addIconToDB(values, componentName, info, userSerial);
     }
@@ -300,8 +303,8 @@
         return mDefaultIcons.get(user);
     }
 
-    public boolean isDefaultIcon(Bitmap icon, UserHandle user) {
-        return getDefaultIcon(user).icon == icon;
+    public boolean isDefaultIcon(BitmapInfo icon, UserHandle user) {
+        return getDefaultIcon(user).icon == icon.icon;
     }
 
     /**
@@ -315,7 +318,7 @@
         assertWorkerThread();
         ComponentKey cacheKey = new ComponentKey(componentName, user);
         CacheEntry entry = mCache.get(cacheKey);
-        if (entry == null || (entry.isLowRes() && !useLowResIcon)) {
+        if (entry == null || (entry.bitmap.isLowRes() && !useLowResIcon)) {
             entry = new CacheEntry();
             if (cachingLogic.addToMemCache()) {
                 mCache.put(cacheKey, entry);
@@ -330,7 +333,7 @@
                 providerFetchedOnce = true;
 
                 if (object != null) {
-                    cachingLogic.loadIcon(mContext, object, entry);
+                    entry.bitmap = cachingLogic.loadIcon(mContext, object);
                 } else {
                     if (usePackageIcon) {
                         CacheEntry packageEntry = getEntryForPackageLocked(
@@ -338,15 +341,15 @@
                         if (packageEntry != null) {
                             if (DEBUG) Log.d(TAG, "using package default icon for " +
                                     componentName.toShortString());
-                            packageEntry.applyTo(entry);
+                            entry.bitmap = packageEntry.bitmap;
                             entry.title = packageEntry.title;
                             entry.contentDescription = packageEntry.contentDescription;
                         }
                     }
-                    if (entry.icon == null) {
+                    if (entry.bitmap == null) {
                         if (DEBUG) Log.d(TAG, "using default icon for " +
                                 componentName.toShortString());
-                        getDefaultIcon(user).applyTo(entry);
+                        entry.bitmap = getDefaultIcon(user);
                     }
                 }
             }
@@ -390,10 +393,10 @@
         }
         if (icon != null) {
             BaseIconFactory li = getIconFactory();
-            li.createIconBitmap(icon).applyTo(entry);
+            entry.bitmap = li.createIconBitmap(icon);
             li.close();
         }
-        if (!TextUtils.isEmpty(title) && entry.icon != null) {
+        if (!TextUtils.isEmpty(title) && entry.bitmap.icon != null) {
             mCache.put(cacheKey, entry);
         }
     }
@@ -413,7 +416,7 @@
         ComponentKey cacheKey = getPackageKey(packageName, user);
         CacheEntry entry = mCache.get(cacheKey);
 
-        if (entry == null || (entry.isLowRes() && !useLowResIcon)) {
+        if (entry == null || (entry.bitmap.isLowRes() && !useLowResIcon)) {
             entry = new CacheEntry();
             boolean entryUpdated = true;
 
@@ -438,8 +441,8 @@
 
                     entry.title = appInfo.loadLabel(mPackageManager);
                     entry.contentDescription = mPackageManager.getUserBadgedLabel(entry.title, user);
-                    entry.icon = useLowResIcon ? LOW_RES_ICON : iconInfo.icon;
-                    entry.color = iconInfo.color;
+                    entry.bitmap = BitmapInfo.of(
+                            useLowResIcon ? LOW_RES_ICON : iconInfo.icon, iconInfo.color);
 
                     // Add the icon in the DB here, since these do not get written during
                     // package updates.
@@ -461,7 +464,7 @@
         return entry;
     }
 
-    private boolean getEntryFromDB(ComponentKey cacheKey, CacheEntry entry, boolean lowRes) {
+    protected boolean getEntryFromDB(ComponentKey cacheKey, CacheEntry entry, boolean lowRes) {
         Cursor c = null;
         try {
             c = mIconDb.query(
@@ -472,7 +475,7 @@
                             Long.toString(getSerialNumberForUser(cacheKey.user))});
             if (c.moveToNext()) {
                 // Set the alpha to be 255, so that we never have a wrong color
-                entry.color = setColorAlphaBound(c.getInt(0), 255);
+                entry.bitmap = BitmapInfo.of(LOW_RES_ICON, setColorAlphaBound(c.getInt(0), 255));
                 entry.title = c.getString(1);
                 if (entry.title == null) {
                     entry.title = "";
@@ -482,13 +485,12 @@
                             entry.title, cacheKey.user);
                 }
 
-                if (lowRes) {
-                    entry.icon = LOW_RES_ICON;
-                } else {
+                if (!lowRes) {
                     byte[] data = c.getBlob(2);
                     try {
-                        entry.icon = BitmapFactory.decodeByteArray(data, 0, data.length,
-                                mDecodeOptions);
+                        entry.bitmap = BitmapInfo.of(
+                                BitmapFactory.decodeByteArray(data, 0, data.length, mDecodeOptions),
+                                entry.bitmap.color);
                     } catch (Exception e) { }
                 }
                 return true;
diff --git a/iconloaderlib/src/com/android/launcher3/icons/cache/CachingLogic.java b/iconloaderlib/src/com/android/launcher3/icons/cache/CachingLogic.java
index e40a9c2..a89ede7 100644
--- a/iconloaderlib/src/com/android/launcher3/icons/cache/CachingLogic.java
+++ b/iconloaderlib/src/com/android/launcher3/icons/cache/CachingLogic.java
@@ -17,9 +17,11 @@
 
 import android.content.ComponentName;
 import android.content.Context;
+import android.content.pm.PackageInfo;
 import android.os.LocaleList;
 import android.os.UserHandle;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.icons.BitmapInfo;
@@ -32,7 +34,8 @@
 
     CharSequence getLabel(T object);
 
-    void loadIcon(Context context, T object, BitmapInfo target);
+    @NonNull
+    BitmapInfo loadIcon(Context context, T object);
 
     /**
      * Provides a option list of keywords to associate with this object
@@ -43,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
new file mode 100644
index 0000000..32846dc
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/HotseatPredictionController.java
@@ -0,0 +1,340 @@
+/*
+ * 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 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;
+
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.allapps.AllAppsStore;
+import com.android.launcher3.anim.AnimationSuccessListener;
+import com.android.launcher3.appprediction.ComponentKeyMapper;
+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;
+import java.util.List;
+
+/**
+ * Provides prediction ability for the hotseat. Fills gaps in hotseat with predicted items, allows
+ * pinning of predicted apps and manages replacement of predicted apps with user drag.
+ */
+public class HotseatPredictionController implements DragController.DragListener,
+        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 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;
+        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) {
+        mLauncher.getDragController().addDragListener(this);
+    }
+
+    @Override
+    public void onViewDetachedFromWindow(View view) {
+        mLauncher.getDragController().removeDragListener(this);
+    }
+
+    /**
+     * Fills gaps in the hotseat with predictions
+     */
+    public void fillGapsWithPrediction(boolean animate) {
+        if (mDragStarted) {
+            return;
+        }
+        List<WorkspaceItemInfo> predictedApps = mapToWorkspaceItemInfo(mComponentKeyMappers);
+        int predictionIndex = 0;
+        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;
+            }
+            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);
+        }
+        mLauncher.bindItems(newItemsToAdd, animate);
+        for (BubbleTextView icon : getPredictedIcons()) {
+            icon.verifyHighRes();
+            icon.setOnLongClickListener((v) -> {
+                PopupContainerWithArrow.showForIcon((BubbleTextView) v);
+                return true;
+            });
+        }
+    }
+
+    /**
+     * 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) mHotseat.getChildAt(
+                mHotseat.getCellXFromOrder(info.rank),
+                mHotseat.getCellYFromOrder(info.rank));
+        if (icon == null) {
+            return;
+        }
+        WorkspaceItemInfo workspaceItemInfo = new WorkspaceItemInfo((WorkspaceItemInfo) info);
+        mLauncher.getModelWriter().addItemToDatabase(workspaceItemInfo,
+                LauncherSettings.Favorites.CONTAINER_HOTSEAT, workspaceItemInfo.screenId,
+                workspaceItemInfo.cellX, workspaceItemInfo.cellY);
+        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();
+        if (allAppsStore.getApps().length == 0) {
+            return Collections.emptyList();
+        }
+
+        List<WorkspaceItemInfo> predictedApps = new ArrayList<>();
+        for (ComponentKeyMapper mapper : components) {
+            ItemInfoWithIcon info = mapper.getApp(allAppsStore);
+            if (info instanceof AppInfo) {
+                WorkspaceItemInfo predictedApp = new WorkspaceItemInfo((AppInfo) info);
+                predictedApps.add(predictedApp);
+            } else if (info instanceof WorkspaceItemInfo) {
+                predictedApps.add(new WorkspaceItemInfo((WorkspaceItemInfo) info));
+            } else {
+                if (DEBUG) {
+                    Log.e(TAG, "Predicted app not found: " + mapper);
+                }
+            }
+            // Stop at the number of hotseat items
+            if (predictedApps.size() == mHotSeatItemsCount) {
+                break;
+            }
+        }
+        return predictedApps;
+    }
+
+    private List<BubbleTextView> getPredictedIcons() {
+        List<BubbleTextView> icons = new ArrayList<>();
+        ViewGroup vg = mHotseat.getShortcutsAndWidgets();
+        for (int i = 0; i < vg.getChildCount(); i++) {
+            View child = vg.getChildAt(i);
+            if (isPredictedIcon(child)) {
+                icons.add((BubbleTextView) child);
+            }
+        }
+        return icons;
+    }
+
+    private void removePredictedApps(boolean animate) {
+        for (BubbleTextView icon : getPredictedIcons()) {
+            if (animate) {
+                icon.animate().scaleY(0).scaleX(0).setListener(new AnimationSuccessListener() {
+                    @Override
+                    public void onAnimationSuccess(Animator animator) {
+                        if (icon.getParent() != null) {
+                            mHotseat.removeView(icon);
+                        }
+                    }
+                });
+            } else {
+                if (icon.getParent() != null) {
+                    mHotseat.removeView(icon);
+                }
+            }
+        }
+    }
+
+    @Override
+    public void onDragStart(DropTarget.DragObject dragObject, DragOptions options) {
+        removePredictedApps(true);
+        mDragStarted = true;
+    }
+
+    @Override
+    public void onDragEnd() {
+        if (!mDragStarted) {
+            return;
+        }
+        mDragStarted = false;
+        fillGapsWithPrediction(true);
+    }
+
+    @Nullable
+    @Override
+    public SystemShortcut<QuickstepLauncher> getShortcut(QuickstepLauncher activity,
+            ItemInfo itemInfo) {
+        if (itemInfo.container != LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION) {
+            return null;
+        }
+        return new PinPrediction(activity, itemInfo);
+    }
+
+    private void preparePredictionInfo(WorkspaceItemInfo itemInfo, int rank) {
+        itemInfo.container = LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION;
+        itemInfo.rank = rank;
+        itemInfo.cellX = rank;
+        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) {
+            super(R.drawable.ic_pin, R.string.pin_prediction, target,
+                    itemInfo);
+        }
+
+        @Override
+        public void onClick(View view) {
+            dismissTaskMenuView(mTarget);
+            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/ComponentKeyMapper.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/ComponentKeyMapper.java
index b9f4147..0712285 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/ComponentKeyMapper.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/ComponentKeyMapper.java
@@ -18,8 +18,6 @@
 
 import static com.android.quickstep.InstantAppResolverImpl.COMPONENT_CLASS_MARKER;
 
-import android.content.Context;
-
 import com.android.launcher3.AppInfo;
 import com.android.launcher3.ItemInfoWithIcon;
 import com.android.launcher3.allapps.AllAppsStore;
@@ -29,11 +27,9 @@
 public class ComponentKeyMapper {
 
     protected final ComponentKey componentKey;
-    private final Context mContext;
     private final DynamicItemCache mCache;
 
-    public ComponentKeyMapper(Context context, ComponentKey key, DynamicItemCache cache) {
-        mContext = context;
+    public ComponentKeyMapper(ComponentKey key, DynamicItemCache cache) {
         componentKey = key;
         mCache = cache;
     }
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 65e69b6..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<>();
@@ -170,7 +175,7 @@
         if (!details.isEmpty()) {
             WorkspaceItemInfo si = new WorkspaceItemInfo(details.get(0), mContext);
             try (LauncherIcons li = LauncherIcons.obtain(mContext)) {
-                si.applyFrom(li.createShortcutIcon(details.get(0), true /* badged */, null));
+                si.bitmap = li.createShortcutIcon(details.get(0), true /* badged */, null);
             } catch (Exception e) {
                 if (DEBUG) {
                     Log.e(TAG, "Error loading shortcut icon for " + shortcutKey.toString());
@@ -209,7 +214,7 @@
         InstantAppItemInfo info = new InstantAppItemInfo(intent, pkgName);
         IconCache iconCache = LauncherAppState.getInstance(mContext).getIconCache();
         iconCache.getTitleAndIcon(info, false);
-        if (info.iconBitmap == null || iconCache.isDefaultIcon(info.iconBitmap, info.user)) {
+        if (info.bitmap.icon == null || iconCache.isDefaultIcon(info.bitmap, info.user)) {
             return null;
         }
         return info;
@@ -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 1a59770..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,14 +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.InvariantDeviceProfile;
 import com.android.launcher3.InvariantDeviceProfile.OnIDPChangeListener;
 import com.android.launcher3.ItemInfoWithIcon;
@@ -36,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;
@@ -239,7 +236,7 @@
                     key = new ComponentKey(new ComponentName(appTarget.getPackageName(),
                             appTarget.getClassName()), appTarget.getUser());
                 }
-                state.apps.add(new ComponentKeyMapper(mContext, key, mDynamicItemCache));
+                state.apps.add(new ComponentKeyMapper(key, mDynamicItemCache));
             }
         }
         updateDependencies(state);
@@ -250,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 43cdbdb..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");
@@ -22,14 +22,18 @@
 import android.content.Context;
 import android.content.res.Configuration;
 import android.graphics.Rect;
+import android.os.Bundle;
 import android.view.Gravity;
 
 import com.android.launcher3.BaseQuickstepLauncher;
 import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.HotseatPredictionController;
 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;
 import com.android.launcher3.uioverrides.touchcontrollers.LandscapeEdgeSwipeController;
 import com.android.launcher3.uioverrides.touchcontrollers.NavBarToHomeTouchController;
@@ -48,17 +52,16 @@
 import com.android.quickstep.views.RecentsView;
 
 import java.util.ArrayList;
+import java.util.stream.Stream;
 
 public class QuickstepLauncher extends BaseQuickstepLauncher {
 
     public static final boolean GO_LOW_RAM_RECENTS_ENABLED = false;
-
     /**
      * Reusable command for applying the shelf height on the background thread.
      */
     public static final AsyncCommand SET_SHELF_HEIGHT = (context, arg1, arg2) ->
             SystemUiProxy.INSTANCE.get(context).setShelfHeight(arg1 != 0, arg2);
-
     public static RotationMode ROTATION_LANDSCAPE = new RotationMode(-90) {
         @Override
         public void mapRect(int left, int top, int right, int bottom, Rect out) {
@@ -85,7 +88,6 @@
             }
         }
     };
-
     public static RotationMode ROTATION_SEASCAPE = new RotationMode(90) {
         @Override
         public void mapRect(int left, int top, int right, int bottom, Rect out) {
@@ -131,6 +133,15 @@
                     | horizontalGravity | verticalGravity;
         }
     };
+    private HotseatPredictionController mHotseatPredictionController;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        if (FeatureFlags.ENABLE_HYBRID_HOTSEAT.get()) {
+            mHotseatPredictionController = new HotseatPredictionController(this);
+        }
+    }
 
     @Override
     protected RotationMode getFakeRotationMode(DeviceProfile dp) {
@@ -155,6 +166,16 @@
         }
     }
 
+    @Override
+    public Stream<SystemShortcut.Factory> getSupportedShortcuts() {
+        if (mHotseatPredictionController != null) {
+            return Stream.concat(super.getSupportedShortcuts(),
+                    Stream.of(mHotseatPredictionController));
+        } else {
+            return super.getSupportedShortcuts();
+        }
+    }
+
     /**
      * Recents logic that triggers when launcher state changes or launcher activity stops/resumes.
      */
@@ -171,6 +192,22 @@
     }
 
     @Override
+    public void finishBindingItems(int pageBoundFirst) {
+        super.finishBindingItems(pageBoundFirst);
+        if (mHotseatPredictionController != null) {
+            mHotseatPredictionController.fillGapsWithPrediction(false);
+        }
+    }
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+        if (mHotseatPredictionController != null) {
+            mHotseatPredictionController.destroy();
+        }
+    }
+
+    @Override
     public TouchController[] createTouchControllers() {
         Mode mode = SysUINavigationMode.getMode(this);
 
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/FlingAndHoldTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/FlingAndHoldTouchController.java
index c1312c5..626292e 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/FlingAndHoldTouchController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/FlingAndHoldTouchController.java
@@ -35,12 +35,12 @@
 import static com.android.launcher3.anim.Interpolators.DEACCEL;
 import static com.android.launcher3.anim.Interpolators.DEACCEL_3;
 import static com.android.launcher3.anim.Interpolators.OVERSHOOT_1_2;
+import static com.android.launcher3.util.VibratorWrapper.OVERVIEW_HAPTIC;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.AnimatorSet;
-import android.view.HapticFeedbackConstants;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewConfiguration;
@@ -51,6 +51,7 @@
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
 import com.android.quickstep.SystemUiProxy;
+import com.android.launcher3.util.VibratorWrapper;
 import com.android.quickstep.util.MotionPauseDetector;
 import com.android.quickstep.views.RecentsView;
 
@@ -106,8 +107,7 @@
                     }
                 });
                 mPeekAnim.start();
-                recentsView.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY,
-                        HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
+                VibratorWrapper.INSTANCE.get(mLauncher).vibrate(OVERVIEW_HAPTIC);
 
                 mLauncher.getDragLayer().getScrim().animateToSysuiMultiplier(isPaused ? 0 : 1,
                         peekDuration, 0);
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 738436a..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,7 +22,9 @@
 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;
 
 import android.animation.Animator;
 import android.animation.AnimatorSet;
@@ -43,11 +45,14 @@
 import com.android.launcher3.anim.AnimatorSetBuilder;
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.compat.AccessibilityManagerCompat;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.touch.SingleAxisSwipeDetector;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
 import com.android.launcher3.util.TouchController;
+import com.android.quickstep.util.AssistantUtilities;
 import com.android.quickstep.views.RecentsView;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
 
 /**
  * Handles swiping up on the nav bar to go home from launcher, e.g. overview or all apps.
@@ -104,6 +109,10 @@
         if (AbstractFloatingView.getTopOpenView(mLauncher) != null) {
             return true;
         }
+        if (FeatureFlags.ASSISTANT_GIVES_LAUNCHER_FOCUS.get()
+                && AssistantUtilities.isExcludedAssistantRunning()) {
+            return true;
+        }
         return false;
     }
 
@@ -130,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();
@@ -184,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) {
@@ -194,6 +213,8 @@
                 AbstractFloatingView.closeAllOpenViews(mLauncher);
                 logStateChange(topOpenView.getLogContainerType(), logAction);
             }
+            ActivityManagerWrapper.getInstance()
+                    .closeSystemWindows(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS);
         } else {
             // Quickly return to the state we came from (we didn't move far).
             ValueAnimator anim = mCurrentAnimation.getAnimationPlayer();
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java
index a4ac1b0..912be98 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java
@@ -30,6 +30,7 @@
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.util.SystemUiController.UI_STATE_OVERVIEW;
 import static com.android.quickstep.views.RecentsView.UPDATE_SYSUI_FLAGS_THRESHOLD;
+import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED;
 
 import android.view.MotionEvent;
@@ -50,6 +51,7 @@
 import com.android.quickstep.SystemUiProxy;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.TaskView;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
 
 /**
  * Handles quick switching to a recent task from the home screen.
@@ -94,6 +96,8 @@
         super.onDragStart(start);
         mStartContainerType = LauncherLogProto.ContainerType.NAVBAR;
         mTaskToLaunch = mLauncher.<RecentsView>getOverviewPanel().getTaskViewAt(0);
+        ActivityManagerWrapper.getInstance()
+                .closeSystemWindows(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS);
     }
 
     @Override
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java
index 2755492..4f50e33 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java
@@ -15,20 +15,15 @@
  */
 package com.android.quickstep;
 
-import static android.os.VibrationEffect.EFFECT_CLICK;
-import static android.os.VibrationEffect.createPredefined;
-
-import static com.android.launcher3.Utilities.postAsyncCallback;
 import static com.android.launcher3.anim.Interpolators.ACCEL_1_5;
 import static com.android.launcher3.anim.Interpolators.DEACCEL;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
+import static com.android.launcher3.util.VibratorWrapper.OVERVIEW_HAPTIC;
 import static com.android.launcher3.views.FloatingIconView.SHAPE_PROGRESS_DURATION;
 
 import android.animation.Animator;
 import android.annotation.TargetApi;
-import android.app.ActivityManager.RunningTaskInfo;
 import android.content.Context;
 import android.content.Intent;
 import android.graphics.Point;
@@ -36,11 +31,6 @@
 import android.graphics.Rect;
 import android.graphics.RectF;
 import android.os.Build;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.VibrationEffect;
-import android.os.Vibrator;
-import android.provider.Settings;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.animation.Interpolator;
@@ -55,9 +45,9 @@
 import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.graphics.RotationMode;
+import com.android.launcher3.util.VibratorWrapper;
 import com.android.launcher3.views.FloatingIconView;
 import com.android.quickstep.BaseActivityInterface.HomeAnimationFactory;
-import com.android.quickstep.SysUINavigationMode.Mode;
 import com.android.quickstep.util.ActiveGestureLog;
 import com.android.quickstep.util.ActivityInitListener;
 import com.android.quickstep.util.AppWindowAnimationHelper;
@@ -98,16 +88,12 @@
     protected final Context mContext;
     protected final RecentsAnimationDeviceState mDeviceState;
     protected final GestureState mGestureState;
-    protected final OverviewComponentObserver mOverviewComponentObserver;
     protected final BaseActivityInterface<T> mActivityInterface;
-    protected final RecentsModel mRecentsModel;
-    protected final int mRunningTaskId;
+    protected final InputConsumerController mInputConsumer;
 
     protected final AppWindowAnimationHelper mAppWindowAnimationHelper;
     protected final TransformParams mTransformParams = new TransformParams();
 
-    private final Vibrator mVibrator;
-
     // Shift in the range of [0, 1].
     // 0 => preview snapShot is completely visible, and hotseat is completely translated down
     // 1 => preview snapShot is completely aligned with the recents view and hotseat is completely
@@ -115,7 +101,6 @@
     protected final AnimatedFloat mCurrentShift = new AnimatedFloat(this::updateFinalShift);
 
     protected final ActivityInitListener mActivityInitListener;
-    protected final InputConsumerController mInputConsumer;
 
     protected RecentsAnimationController mRecentsAnimationController;
     protected RecentsAnimationTargets mRecentsAnimationTargets;
@@ -136,40 +121,24 @@
     protected int mFinishingRecentsAnimationForNewTaskId = -1;
 
     protected BaseSwipeUpHandler(Context context, RecentsAnimationDeviceState deviceState,
-            GestureState gestureState, OverviewComponentObserver overviewComponentObserver,
-            RecentsModel recentsModel, InputConsumerController inputConsumer, int runningTaskId) {
+            GestureState gestureState, InputConsumerController inputConsumer) {
         mContext = context;
         mDeviceState = deviceState;
         mGestureState = gestureState;
-        mOverviewComponentObserver = overviewComponentObserver;
         mActivityInterface = gestureState.getActivityInterface();
-        mRecentsModel = recentsModel;
         mActivityInitListener =
                 mActivityInterface.createActivityInitListener(this::onActivityInit);
-        mRunningTaskId = runningTaskId;
         mInputConsumer = inputConsumer;
 
         mAppWindowAnimationHelper = new AppWindowAnimationHelper(context);
         mPageSpacing = context.getResources().getDimensionPixelSize(R.dimen.recents_page_spacing);
-        mVibrator = context.getSystemService(Vibrator.class);
+
         initTransitionEndpoints(InvariantDeviceProfile.INSTANCE.get(mContext)
                 .getDeviceProfile(mContext));
     }
 
     protected void performHapticFeedback() {
-        if (!mVibrator.hasVibrator()) {
-            return;
-        }
-        if (Settings.System.getInt(
-                mContext.getContentResolver(), Settings.System.HAPTIC_FEEDBACK_ENABLED, 0) == 0) {
-            return;
-        }
-
-        VibrationEffect effect = createPredefined(EFFECT_CLICK);
-        if (effect == null) {
-            return;
-        }
-        UI_HELPER_EXECUTOR.execute(() -> mVibrator.vibrate(effect));
+        VibratorWrapper.INSTANCE.get(mContext).vibrate(OVERVIEW_HAPTIC);
     }
 
     public Consumer<MotionEvent> getRecentsViewDispatcher(RotationMode rotationMode) {
@@ -281,7 +250,8 @@
         mRecentsAnimationTargets = targets;
         DeviceProfile dp = InvariantDeviceProfile.INSTANCE.get(mContext).getDeviceProfile(mContext);
         final Rect overviewStackBounds;
-        RemoteAnimationTargetCompat runningTaskTarget = targets.findTask(mRunningTaskId);
+        RemoteAnimationTargetCompat runningTaskTarget = targets.findTask(
+                mGestureState.getRunningTaskId());
 
         if (targets.minimizedHomeBounds != null && runningTaskTarget != null) {
             overviewStackBounds = mActivityInterface
@@ -393,7 +363,7 @@
 
     public void initWhenReady() {
         // Preload the plan
-        mRecentsModel.getTasks(null);
+        RecentsModel.INSTANCE.get(mContext).getTasks(null);
 
         mActivityInitListener.register();
     }
@@ -510,8 +480,8 @@
 
     public interface Factory {
 
-        BaseSwipeUpHandler newHandler(GestureState gestureState, RunningTaskInfo runningTask,
-                long touchTimeMs, boolean continuingLastGesture, boolean isLikelyToStartNewTask);
+        BaseSwipeUpHandler newHandler(GestureState gestureState, long touchTimeMs,
+                boolean continuingLastGesture, boolean isLikelyToStartNewTask);
     }
 
     protected interface RunningWindowAnim {
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 88%
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 7b24bd9..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,12 +22,11 @@
 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;
 import android.animation.AnimatorSet;
-import android.app.ActivityManager.RunningTaskInfo;
 import android.app.ActivityOptions;
 import android.content.Context;
 import android.content.Intent;
@@ -41,28 +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.OverviewComponentObserver;
-import com.android.quickstep.RecentsActivity;
-import com.android.quickstep.RecentsAnimationController;
-import com.android.quickstep.RecentsAnimationDeviceState;
-import com.android.quickstep.RecentsModel;
 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,24 +98,18 @@
     private final boolean mRunningOverHome;
     private final boolean mSwipeUpOverHome;
 
-    private final RunningTaskInfo mRunningTaskInfo;
-
     private final PointF mEndVelocityPxPerMs = new PointF(0, 0.5f);
     private RunningWindowAnim mFinishAnimation;
 
-    public FallbackNoButtonInputConsumer(Context context, RecentsAnimationDeviceState deviceState,
-            GestureState gestureState, OverviewComponentObserver overviewComponentObserver,
-            RunningTaskInfo runningTaskInfo, RecentsModel recentsModel,
-            InputConsumerController inputConsumer,
+    public FallbackSwipeHandler(Context context, RecentsAnimationDeviceState deviceState,
+            GestureState gestureState, InputConsumerController inputConsumer,
             boolean isLikelyToStartNewTask, boolean continuingLastGesture) {
-        super(context, deviceState, gestureState, overviewComponentObserver, recentsModel,
-                inputConsumer, runningTaskInfo.id);
+        super(context, deviceState, gestureState, inputConsumer);
         mLauncherAlpha.value = 1;
 
-        mRunningTaskInfo = runningTaskInfo;
         mInQuickSwitchMode = isLikelyToStartNewTask || continuingLastGesture;
         mContinuingLastGesture = continuingLastGesture;
-        mRunningOverHome = ActivityManagerWrapper.isHomeTask(runningTaskInfo);
+        mRunningOverHome = ActivityManagerWrapper.isHomeTask(mGestureState.getRunningTask());
         mSwipeUpOverHome = mRunningOverHome && !mInQuickSwitchMode;
 
         if (mSwipeUpOverHome) {
@@ -182,9 +166,9 @@
 
         if (!mContinuingLastGesture) {
             if (mRunningOverHome) {
-                mRecentsView.onGestureAnimationStart(mRunningTaskInfo);
+                mRecentsView.onGestureAnimationStart(mGestureState.getRunningTask());
             } else {
-                mRecentsView.onGestureAnimationStart(mRunningTaskId);
+                mRecentsView.onGestureAnimationStart(mGestureState.getRunningTaskId());
             }
         }
         mStateCallback.setStateOnUiThread(STATE_RECENTS_PRESENT);
@@ -230,9 +214,9 @@
     @Override
     public Intent getLaunchIntent() {
         if (mInQuickSwitchMode || mSwipeUpOverHome) {
-            return mOverviewComponentObserver.getOverviewIntent();
+            return mGestureState.getOverviewIntent();
         } else {
-            return mOverviewComponentObserver.getHomeIntent();
+            return mGestureState.getHomeIntent();
         }
     }
 
@@ -329,7 +313,7 @@
                 if (mSwipeUpOverHome) {
                     mRecentsAnimationController.finish(false, null, false);
                     // Send a home intent to clear the task stack
-                    mContext.startActivity(mOverviewComponentObserver.getHomeIntent());
+                    mContext.startActivity(mGestureState.getHomeIntent());
                 } else {
                     mRecentsAnimationController.finish(true, null, true);
                 }
@@ -344,7 +328,8 @@
                     break;
                 }
 
-                ThumbnailData thumbnail = mRecentsAnimationController.screenshotTask(mRunningTaskId);
+                final int runningTaskId = mGestureState.getRunningTaskId();
+                ThumbnailData thumbnail = mRecentsAnimationController.screenshotTask(runningTaskId);
                 mRecentsAnimationController.setDeferCancelUntilNextTransition(true /* defer */,
                         false /* screenshot */);
 
@@ -353,9 +338,9 @@
 
                 Bundle extras = new Bundle();
                 extras.putBinder(EXTRA_THUMBNAIL, new ObjectWrapper<>(thumbnail));
-                extras.putInt(EXTRA_TASK_ID, mRunningTaskId);
+                extras.putInt(EXTRA_TASK_ID, runningTaskId);
 
-                Intent intent = new Intent(mOverviewComponentObserver.getOverviewIntent())
+                Intent intent = new Intent(mGestureState.getOverviewIntent())
                         .putExtras(extras);
                 mContext.startActivity(intent, options.toBundle());
                 mRecentsAnimationController.cleanupScreenshot();
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 94%
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 065f3eb..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;
@@ -39,7 +40,6 @@
 import android.animation.TimeInterpolator;
 import android.animation.ValueAnimator;
 import android.annotation.TargetApi;
-import android.app.ActivityManager.RunningTaskInfo;
 import android.content.Context;
 import android.content.Intent;
 import android.graphics.PointF;
@@ -84,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;
 
@@ -190,13 +193,11 @@
 
     private final Runnable mOnDeferredActivityLaunch = this::onDeferredActivityLaunch;
 
-    public WindowTransformSwipeHandler(Context context, RecentsAnimationDeviceState deviceState,
+    public LauncherSwipeHandler(Context context, RecentsAnimationDeviceState deviceState,
             TaskAnimationManager taskAnimationManager, GestureState gestureState,
-            RunningTaskInfo runningTaskInfo, long touchTimeMs,
-            OverviewComponentObserver overviewComponentObserver, boolean continuingLastGesture,
-            InputConsumerController inputConsumer, RecentsModel recentsModel) {
-        super(context, deviceState, gestureState, overviewComponentObserver, recentsModel,
-                inputConsumer, runningTaskInfo.id);
+            long touchTimeMs, boolean continuingLastGesture,
+            InputConsumerController inputConsumer) {
+        super(context, deviceState, gestureState, inputConsumer);
         mTaskAnimationManager = taskAnimationManager;
         mTouchTimeMs = touchTimeMs;
         mContinuingLastGesture = continuingLastGesture;
@@ -290,12 +291,18 @@
 
         mStateCallback.setState(STATE_LAUNCHER_PRESENT);
         if (alreadyOnHome) {
-            onLauncherStart(activity);
+            onLauncherStart();
         } else {
-            activity.setOnStartCallback(this::onLauncherStart);
+            activity.runOnceOnStart(this::onLauncherStart);
         }
 
         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;
     }
 
@@ -304,7 +311,8 @@
         return mGestureState.getEndTarget() != HOME;
     }
 
-    private void onLauncherStart(final T activity) {
+    private void onLauncherStart() {
+        final T activity = mActivityInterface.getCreatedActivity();
         if (mActivity != activity) {
             return;
         }
@@ -379,7 +387,7 @@
 
     private void onDeferredActivityLaunch() {
         if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
-            mOverviewComponentObserver.getActivityInterface().switchRunningTaskViewToScreenshot(
+            mActivityInterface.switchRunningTaskViewToScreenshot(
                     null, () -> {
                         mTaskAnimationManager.finishRunningRecentsAnimation(true /* toHome */);
                     });
@@ -393,7 +401,7 @@
             updateSysUiFlags(mCurrentShift.value);
             return;
         }
-        mRecentsView.onGestureAnimationStart(mRunningTaskId);
+        mRecentsView.onGestureAnimationStart(mGestureState.getRunningTaskId());
     }
 
     private void launcherFrameDrawn() {
@@ -425,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() {
@@ -442,9 +456,9 @@
         if (!mDeviceState.isFullyGesturalNavMode() || mRecentsView == null) {
             return;
         }
-        RemoteAnimationTargetCompat runningTaskTarget = mRecentsAnimationTargets == null
-                ? null
-                : mRecentsAnimationTargets.findTask(mRunningTaskId);
+        RemoteAnimationTargetCompat runningTaskTarget = mRecentsAnimationTargets != null
+                ? mRecentsAnimationTargets.findTask(mGestureState.getRunningTaskId())
+                : null;
         final boolean recentsAttachedToAppWindow;
         if (mGestureState.getEndTarget() != null) {
             recentsAttachedToAppWindow = mGestureState.getEndTarget().recentsAttachedToAppWindow;
@@ -521,7 +535,7 @@
 
     @Override
     public Intent getLaunchIntent() {
-        return mOverviewComponentObserver.getOverviewIntent();
+        return mGestureState.getOverviewIntent();
     }
 
     @Override
@@ -668,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();
@@ -1005,7 +1019,9 @@
     @Override
     public void onConsumerAboutToBeSwitched() {
         if (mActivity != null) {
-            mActivity.setOnStartCallback(null);
+            // In the off chance that the gesture ends before Launcher is started, we should clear
+            // the callback here so that it doesn't update with the wrong state
+            mActivity.clearRunOnceOnStartCallback();
         }
         if (mGestureState.getEndTarget() != null && !mGestureState.isRunningAnimationToLauncher()) {
             cancelCurrentAnimation();
@@ -1028,6 +1044,15 @@
 
     @UiThread
     private void startNewTask() {
+        if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+            mRecentsAnimationController.finish(true /* toRecents */, this::startNewTaskInternal);
+        } else {
+            startNewTaskInternal();
+        }
+    }
+
+    @UiThread
+    private void startNewTaskInternal() {
         startNewTask(STATE_HANDLER_INVALIDATED, success -> {
             if (!success) {
                 // We couldn't launch the task, so take user to overview so they can
@@ -1114,13 +1139,14 @@
     }
 
     private void switchToScreenshot() {
+        final int runningTaskId = mGestureState.getRunningTaskId();
         if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
             if (mRecentsAnimationController != null) {
                 // Update the screenshot of the task
                 if (mTaskSnapshot == null) {
-                    mTaskSnapshot = mRecentsAnimationController.screenshotTask(mRunningTaskId);
+                    mTaskSnapshot = mRecentsAnimationController.screenshotTask(runningTaskId);
                 }
-                mRecentsView.updateThumbnail(mRunningTaskId, mTaskSnapshot, false /* refreshNow */);
+                mRecentsView.updateThumbnail(runningTaskId, mTaskSnapshot, false /* refreshNow */);
             }
             mStateCallback.setStateOnUiThread(STATE_SCREENSHOT_CAPTURED);
         } else if (!hasTargets()) {
@@ -1131,7 +1157,7 @@
             if (mRecentsAnimationController != null) {
                 // Update the screenshot of the task
                 if (mTaskSnapshot == null) {
-                    mTaskSnapshot = mRecentsAnimationController.screenshotTask(mRunningTaskId);
+                    mTaskSnapshot = mRecentsAnimationController.screenshotTask(runningTaskId);
                 }
                 final TaskView taskView;
                 if (mGestureState.getEndTarget() == HOME) {
@@ -1139,7 +1165,7 @@
                     // taken in the correct orientation, but no need to update the thumbnail.
                     taskView = null;
                 } else {
-                    taskView = mRecentsView.updateThumbnail(mRunningTaskId, mTaskSnapshot);
+                    taskView = mRecentsView.updateThumbnail(runningTaskId, mTaskSnapshot);
                 }
                 if (taskView != null && !mCanceled) {
                     // Defer finishing the animation until the next launcher frame with the
@@ -1162,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 b2626e5..71f8ba4 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
@@ -33,7 +33,6 @@
 import android.app.ActivityManager;
 import android.app.ActivityManager.RunningTaskInfo;
 import android.app.Service;
-import android.app.TaskInfo;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
@@ -57,6 +56,7 @@
 import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.allapps.DiscoveryBounce;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.logging.UserEventDispatcher;
 import com.android.launcher3.model.AppLaunchTracker;
 import com.android.launcher3.provider.RestoreDbTask;
@@ -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;
@@ -74,6 +73,7 @@
 import com.android.quickstep.inputconsumers.ResetGestureInputConsumer;
 import com.android.quickstep.inputconsumers.ScreenPinnedInputConsumer;
 import com.android.quickstep.util.ActiveGestureLog;
+import com.android.quickstep.util.AssistantUtilities;
 import com.android.systemui.plugins.OverscrollPlugin;
 import com.android.systemui.plugins.PluginListener;
 import com.android.systemui.shared.recents.IOverviewProxy;
@@ -83,7 +83,6 @@
 import com.android.systemui.shared.system.InputConsumerController;
 import com.android.systemui.shared.system.InputMonitorCompat;
 import com.android.systemui.shared.system.RecentsAnimationListener;
-import com.android.systemui.shared.system.TaskInfoCompat;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
@@ -242,13 +241,12 @@
         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 RecentsModel mRecentsModel;
     private OverviewCommandHelper mOverviewCommandHelper;
     private OverviewComponentObserver mOverviewComponentObserver;
     private InputConsumerController mInputConsumer;
@@ -327,7 +325,6 @@
     @UiThread
     public void onUserUnlocked() {
         mTaskAnimationManager = new TaskAnimationManager();
-        mRecentsModel = RecentsModel.INSTANCE.get(this);
         mOverviewComponentObserver = new OverviewComponentObserver(this, mDeviceState);
         mOverviewCommandHelper = new OverviewCommandHelper(this, mDeviceState,
                 mOverviewComponentObserver);
@@ -431,9 +428,10 @@
                 TraceHelper.FLAG_ALLOW_BINDER_TRACKING);
         MotionEvent event = (MotionEvent) ev;
         if (event.getAction() == ACTION_DOWN) {
-            GestureState newGestureState = new GestureState(
-                    mOverviewComponentObserver.getActivityInterface(),
+            GestureState newGestureState = new GestureState(mOverviewComponentObserver,
                     ActiveGestureLog.INSTANCE.generateAndSetLogId());
+            newGestureState.updateRunningTask(TraceHelper.whitelistIpcs("getRunningTask.0",
+                    () -> mAM.getRunningTask(0)));
 
             if (mDeviceState.isInSwipeUpTouchRegion(event)) {
                 mConsumer.onConsumerAboutToBeSwitched();
@@ -470,8 +468,7 @@
             if (canStartSystemGesture) {
                 // This handles apps launched in direct boot mode (e.g. dialer) as well as apps
                 // launched while device is locked even after exiting direct boot mode (e.g. camera).
-                return createDeviceLockedInputConsumer(newGestureState,
-                        mAM.getRunningTask(ACTIVITY_TYPE_ASSISTANT));
+                return createDeviceLockedInputConsumer(newGestureState);
             } else {
                 return mResetGestureInputConsumer;
             }
@@ -514,26 +511,22 @@
 
     private InputConsumer newBaseConsumer(GestureState previousGestureState,
             GestureState gestureState, MotionEvent event) {
-        RunningTaskInfo runningTaskInfo = TraceHelper.whitelistIpcs("getRunningTask.0",
-                () -> mAM.getRunningTask(0));
         if (mDeviceState.isKeyguardShowingOccluded()) {
             // This handles apps showing over the lockscreen (e.g. camera)
-            return createDeviceLockedInputConsumer(gestureState, runningTaskInfo);
+            return createDeviceLockedInputConsumer(gestureState);
         }
 
         boolean forceOverviewInputConsumer = false;
-        if (isExcludedAssistant(runningTaskInfo)) {
+        if (AssistantUtilities.isExcludedAssistant(gestureState.getRunningTask())) {
             // In the case where we are in the excluded assistant state, ignore it and treat the
             // running activity as the task behind the assistant
-
-            runningTaskInfo = TraceHelper.whitelistIpcs("getRunningTask.assistant",
-                    () -> mAM.getRunningTask(ACTIVITY_TYPE_ASSISTANT));
-            if (!ActivityManagerWrapper.isHomeTask(runningTaskInfo)) {
-                final ComponentName homeComponent =
-                        mOverviewComponentObserver.getHomeIntent().getComponent();
-                forceOverviewInputConsumer =
-                        runningTaskInfo.baseIntent.getComponent().equals(homeComponent);
-            }
+            gestureState.updateRunningTask(TraceHelper.whitelistIpcs("getRunningTask.assistant",
+                    () -> mAM.getRunningTask(ACTIVITY_TYPE_ASSISTANT /* ignoreActivityType */)));
+            ComponentName homeComponent = mOverviewComponentObserver.getHomeIntent().getComponent();
+            ComponentName runningComponent =
+                    gestureState.getRunningTask().baseIntent.getComponent();
+            forceOverviewInputConsumer =
+                    runningComponent != null && runningComponent.equals(homeComponent);
         }
 
         if (previousGestureState.getFinishingRecentsAnimationTaskId() > 0) {
@@ -541,34 +534,28 @@
             // consumer but with the next task as the running task
             RunningTaskInfo info = new ActivityManager.RunningTaskInfo();
             info.id = previousGestureState.getFinishingRecentsAnimationTaskId();
-            return createOtherActivityInputConsumer(previousGestureState, gestureState, event,
-                    info);
-        } else if (runningTaskInfo == null) {
+            gestureState.updateRunningTask(info);
+            return createOtherActivityInputConsumer(previousGestureState, gestureState, event);
+        } else if (gestureState.getRunningTask() == null) {
             return mResetGestureInputConsumer;
         } else if (previousGestureState.isRunningAnimationToLauncher()
                 || gestureState.getActivityInterface().isResumed()
                 || forceOverviewInputConsumer) {
-            return createOverviewInputConsumer(previousGestureState, gestureState, event);
+            return createOverviewInputConsumer(
+                    previousGestureState, gestureState, event, forceOverviewInputConsumer);
         } else if (ENABLE_QUICKSTEP_LIVE_TILE.get()
                 && gestureState.getActivityInterface().isInLiveTileMode()) {
-            return createOverviewInputConsumer(previousGestureState, gestureState, event);
-        } else if (mDeviceState.isGestureBlockedActivity(runningTaskInfo)) {
+            return createOverviewInputConsumer(
+                    previousGestureState, gestureState, event, forceOverviewInputConsumer);
+        } else if (mDeviceState.isGestureBlockedActivity(gestureState.getRunningTask())) {
             return mResetGestureInputConsumer;
         } else {
-            return createOtherActivityInputConsumer(previousGestureState, gestureState, event,
-                    runningTaskInfo);
+            return createOtherActivityInputConsumer(previousGestureState, gestureState, event);
         }
     }
 
-    private boolean isExcludedAssistant(TaskInfo info) {
-        return info != null
-                && TaskInfoCompat.getActivityType(info) == ACTIVITY_TYPE_ASSISTANT
-                && (info.baseIntent.getFlags() & Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) != 0;
-    }
-
     private InputConsumer createOtherActivityInputConsumer(GestureState previousGestureState,
-            GestureState gestureState,
-            MotionEvent event, RunningTaskInfo runningTaskInfo) {
+            GestureState gestureState, MotionEvent event) {
 
         final boolean shouldDefer;
         final BaseSwipeUpHandler.Factory factory;
@@ -576,43 +563,45 @@
         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);
         return new OtherActivityInputConsumer(this, mDeviceState, mTaskAnimationManager,
-                gestureState, runningTaskInfo, shouldDefer, this::onConsumerInactive,
+                gestureState, shouldDefer, this::onConsumerInactive,
                 mInputMonitorCompat, disableHorizontalSwipe, factory);
     }
 
-    private InputConsumer createDeviceLockedInputConsumer(GestureState gestureState,
-            RunningTaskInfo taskInfo) {
-        if (mDeviceState.isFullyGesturalNavMode() && taskInfo != null) {
+    private InputConsumer createDeviceLockedInputConsumer(GestureState gestureState) {
+        if (mDeviceState.isFullyGesturalNavMode() && gestureState.getRunningTask() != null) {
             return new DeviceLockedInputConsumer(this, mDeviceState, mTaskAnimationManager,
-                    gestureState, mInputMonitorCompat, taskInfo.taskId);
+                    gestureState, mInputMonitorCompat);
         } else {
             return mResetGestureInputConsumer;
         }
     }
 
     public InputConsumer createOverviewInputConsumer(GestureState previousGestureState,
-            GestureState gestureState, MotionEvent event) {
+            GestureState gestureState, MotionEvent event,
+            boolean forceOverviewInputConsumer) {
         BaseDraggingActivity activity = gestureState.getActivityInterface().getCreatedActivity();
         if (activity == null) {
             return mResetGestureInputConsumer;
         }
 
         if (activity.getRootView().hasWindowFocus()
-                || previousGestureState.isRunningAnimationToLauncher()) {
+                || previousGestureState.isRunningAnimationToLauncher()
+                || (FeatureFlags.ASSISTANT_GIVES_LAUNCHER_FOCUS.get()
+                    && forceOverviewInputConsumer)) {
             return new OverviewInputConsumer(gestureState, activity, mInputMonitorCompat,
                     false /* startingInActivityBounds */);
         } else {
             final boolean disableHorizontalSwipe = mDeviceState.isInExclusionRegion(event);
-            return new OverviewWithoutFocusInputConsumer(activity, gestureState,
+            return new OverviewWithoutFocusInputConsumer(activity, mDeviceState, gestureState,
                     mInputMonitorCompat, disableHorizontalSwipe);
         }
     }
@@ -726,20 +715,16 @@
         }
     }
 
-    private BaseSwipeUpHandler createWindowTransformSwipeHandler(GestureState gestureState,
-            RunningTaskInfo runningTask, long touchTimeMs, boolean continuingLastGesture,
-            boolean isLikelyToStartNewTask) {
-        return  new WindowTransformSwipeHandler(this, mDeviceState, mTaskAnimationManager,
-                gestureState, runningTask, touchTimeMs, mOverviewComponentObserver,
-                continuingLastGesture, mInputConsumer, mRecentsModel);
+    private BaseSwipeUpHandler createLauncherSwipeHandler(GestureState gestureState,
+            long touchTimeMs, boolean continuingLastGesture, boolean isLikelyToStartNewTask) {
+        return  new LauncherSwipeHandler(this, mDeviceState, mTaskAnimationManager,
+                gestureState, touchTimeMs, continuingLastGesture, mInputConsumer);
     }
 
-    private BaseSwipeUpHandler createFallbackNoButtonSwipeHandler(GestureState gestureState,
-            RunningTaskInfo runningTask, long touchTimeMs, boolean continuingLastGesture,
-            boolean isLikelyToStartNewTask) {
-        return new FallbackNoButtonInputConsumer(this, mDeviceState, gestureState,
-                mOverviewComponentObserver, runningTask, mRecentsModel, mInputConsumer,
-                isLikelyToStartNewTask, continuingLastGesture);
+    private BaseSwipeUpHandler createFallbackSwipeHandler(GestureState gestureState,
+            long touchTimeMs, boolean continuingLastGesture, boolean isLikelyToStartNewTask) {
+        return new FallbackSwipeHandler(this, mDeviceState, gestureState,
+                mInputConsumer, isLikelyToStartNewTask, continuingLastGesture);
     }
 
     protected boolean shouldNotifyBackGesture() {
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 370f161..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;
@@ -85,7 +85,6 @@
     private final AppWindowAnimationHelper.TransformParams mTransformParams;
     private final Point mDisplaySize;
     private final MultiStateCallback mStateCallback;
-    public final int mRunningTaskId;
 
     private VelocityTracker mVelocityTracker;
     private float mProgress;
@@ -97,7 +96,7 @@
 
     public DeviceLockedInputConsumer(Context context, RecentsAnimationDeviceState deviceState,
             TaskAnimationManager taskAnimationManager, GestureState gestureState,
-            InputMonitorCompat inputMonitorCompat, int runningTaskId) {
+            InputMonitorCompat inputMonitorCompat) {
         mContext = context;
         mDeviceState = deviceState;
         mTaskAnimationManager = taskAnimationManager;
@@ -106,7 +105,6 @@
         mAppWindowAnimationHelper = new AppWindowAnimationHelper(context);
         mTransformParams = new AppWindowAnimationHelper.TransformParams();
         mInputMonitorCompat = inputMonitorCompat;
-        mRunningTaskId = runningTaskId;
 
         // Do not use DeviceProfile as the user data might be locked
         mDisplaySize = DefaultDisplay.INSTANCE.get(context).getInfo().realSize;
@@ -221,7 +219,8 @@
         mRecentsAnimationTargets = targets;
 
         Rect displaySize = new Rect(0, 0, mDisplaySize.x, mDisplaySize.y);
-        RemoteAnimationTargetCompat targetCompat = targets.findTask(mRunningTaskId);
+        RemoteAnimationTargetCompat targetCompat = targets.findTask(
+                mGestureState.getRunningTaskId());
         if (targetCompat != null) {
             mAppWindowAnimationHelper.updateSource(displaySize, targetCompat);
         }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
index aeab4b5..bf2128d 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
@@ -30,7 +30,6 @@
 import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
 
 import android.annotation.TargetApi;
-import android.app.ActivityManager.RunningTaskInfo;
 import android.content.Context;
 import android.content.ContextWrapper;
 import android.content.Intent;
@@ -58,7 +57,6 @@
 import com.android.quickstep.util.ActiveGestureLog;
 import com.android.quickstep.util.CachedEventDispatcher;
 import com.android.quickstep.util.MotionPauseDetector;
-import com.android.quickstep.util.NavBarPosition;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.InputMonitorCompat;
 
@@ -81,14 +79,11 @@
     private final GestureState mGestureState;
     private RecentsAnimationCallbacks mActiveCallbacks;
     private final CachedEventDispatcher mRecentsViewDispatcher = new CachedEventDispatcher();
-    private final RunningTaskInfo mRunningTask;
     private final InputMonitorCompat mInputMonitorCompat;
     private final BaseActivityInterface mActivityInterface;
 
     private final BaseSwipeUpHandler.Factory mHandlerFactory;
 
-    private final NavBarPosition mNavBarPosition;
-
     private final Consumer<OtherActivityInputConsumer> mOnCompleteCallback;
     private final MotionPauseDetector mMotionPauseDetector;
     private final float mMotionPauseMinDisplacement;
@@ -124,8 +119,7 @@
 
     public OtherActivityInputConsumer(Context base, RecentsAnimationDeviceState deviceState,
             TaskAnimationManager taskAnimationManager, GestureState gestureState,
-            RunningTaskInfo runningTaskInfo, boolean isDeferredDownTarget,
-            Consumer<OtherActivityInputConsumer> onCompleteCallback,
+            boolean isDeferredDownTarget, Consumer<OtherActivityInputConsumer> onCompleteCallback,
             InputMonitorCompat inputMonitorCompat, boolean disableHorizontalSwipe,
             Factory handlerFactory) {
         super(base);
@@ -133,7 +127,6 @@
         mTaskAnimationManager = taskAnimationManager;
         mGestureState = gestureState;
         mMainThreadHandler = new Handler(Looper.getMainLooper());
-        mRunningTask = runningTaskInfo;
         mHandlerFactory = handlerFactory;
         mActivityInterface = mGestureState.getActivityInterface();
 
@@ -146,8 +139,6 @@
 
         boolean continuingPreviousGesture = mTaskAnimationManager.isRecentsAnimationRunning();
         mIsDeferredDownTarget = !continuingPreviousGesture && isDeferredDownTarget;
-
-        mNavBarPosition = new NavBarPosition(base);
         mTouchSlop = ViewConfiguration.get(this).getScaledTouchSlop();
 
         float slop = QUICKSTEP_TOUCH_SLOP_RATIO * mTouchSlop;
@@ -179,7 +170,7 @@
         if (mPassedWindowMoveSlop && mInteractionHandler != null
                 && !mRecentsViewDispatcher.hasConsumer()) {
             mRecentsViewDispatcher.setConsumer(mInteractionHandler.getRecentsViewDispatcher(
-                    mNavBarPosition.getRotationMode()));
+                    mDeviceState.getNavBarPosition().getRotationMode()));
         }
         int edgeFlags = ev.getEdgeFlags();
         ev.setEdgeFlags(edgeFlags | EDGE_NAV_BAR);
@@ -325,7 +316,7 @@
             long touchTimeMs, boolean isLikelyToStartNewTask) {
         ActiveGestureLog.INSTANCE.addLog("startRecentsAnimation");
 
-        mInteractionHandler = mHandlerFactory.newHandler(mGestureState, mRunningTask, touchTimeMs,
+        mInteractionHandler = mHandlerFactory.newHandler(mGestureState, touchTimeMs,
                 mTaskAnimationManager.isRecentsAnimationRunning(), isLikelyToStartNewTask);
         mInteractionHandler.setGestureEndCallback(this::onInteractionGestureFinished);
         mMotionPauseDetector.setOnMotionPauseListener(mInteractionHandler::onMotionPauseChanged);
@@ -360,8 +351,10 @@
                         ViewConfiguration.get(this).getScaledMaximumFlingVelocity());
                 float velocityX = mVelocityTracker.getXVelocity(mActivePointerId);
                 float velocityY = mVelocityTracker.getYVelocity(mActivePointerId);
-                float velocity = mNavBarPosition.isRightEdge() ? velocityX
-                        : mNavBarPosition.isLeftEdge() ? -velocityX
+                float velocity = mDeviceState.getNavBarPosition().isRightEdge()
+                        ? velocityX
+                        : mDeviceState.getNavBarPosition().isLeftEdge()
+                                ? -velocityX
                                 : velocityY;
 
                 mInteractionHandler.updateDisplacement(getDisplacement(ev) - mStartDisplacement);
@@ -414,9 +407,9 @@
     }
 
     private float getDisplacement(MotionEvent ev) {
-        if (mNavBarPosition.isRightEdge()) {
+        if (mDeviceState.getNavBarPosition().isRightEdge()) {
             return ev.getX() - mDownPos.x;
-        } else if (mNavBarPosition.isLeftEdge()) {
+        } else if (mDeviceState.getNavBarPosition().isLeftEdge()) {
             return mDownPos.x - ev.getX();
         } else {
             return ev.getY() - mDownPos.y;
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverviewWithoutFocusInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverviewWithoutFocusInputConsumer.java
index 50069ea..875ec29 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverviewWithoutFocusInputConsumer.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverviewWithoutFocusInputConsumer.java
@@ -21,9 +21,9 @@
 import static android.view.MotionEvent.ACTION_UP;
 
 import static com.android.launcher3.Utilities.squaredHypot;
-import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
 
 import android.content.Context;
+import android.content.Intent;
 import android.graphics.PointF;
 import android.view.MotionEvent;
 import android.view.VelocityTracker;
@@ -35,36 +35,34 @@
 import com.android.launcher3.logging.StatsLogUtils;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
-import com.android.quickstep.BaseActivityInterface;
-import com.android.quickstep.InputConsumer;
 import com.android.quickstep.GestureState;
+import com.android.quickstep.InputConsumer;
+import com.android.quickstep.RecentsAnimationDeviceState;
 import com.android.quickstep.util.ActiveGestureLog;
-import com.android.quickstep.util.NavBarPosition;
-import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.InputMonitorCompat;
 
 public class OverviewWithoutFocusInputConsumer implements InputConsumer {
 
+    private final Context mContext;
+    private final RecentsAnimationDeviceState mDeviceState;
+    private final GestureState mGestureState;
     private final InputMonitorCompat mInputMonitor;
     private final boolean mDisableHorizontalSwipe;
     private final PointF mDownPos = new PointF();
     private final float mSquaredTouchSlop;
-    private final Context mContext;
-    private final NavBarPosition mNavBarPosition;
-    private final BaseActivityInterface mActivityInterface;
 
     private boolean mInterceptedTouch;
     private VelocityTracker mVelocityTracker;
 
-    public OverviewWithoutFocusInputConsumer(Context context, GestureState gestureState,
+    public OverviewWithoutFocusInputConsumer(Context context,
+            RecentsAnimationDeviceState deviceState, GestureState gestureState,
             InputMonitorCompat inputMonitor, boolean disableHorizontalSwipe) {
+        mContext = context;
+        mDeviceState = deviceState;
+        mGestureState = gestureState;
         mInputMonitor = inputMonitor;
         mDisableHorizontalSwipe = disableHorizontalSwipe;
-        mContext = context;
-        mActivityInterface = gestureState.getActivityInterface();
         mSquaredTouchSlop = Utilities.squaredTouchSlop(context);
-        mNavBarPosition = new NavBarPosition(context);
-
         mVelocityTracker = VelocityTracker.obtain();
     }
 
@@ -135,8 +133,11 @@
         mVelocityTracker.computeCurrentVelocity(100);
         float velocityX = mVelocityTracker.getXVelocity();
         float velocityY = mVelocityTracker.getYVelocity();
-        float velocity = mNavBarPosition.isRightEdge()
-                ? -velocityX : (mNavBarPosition.isLeftEdge() ? velocityX : -velocityY);
+        float velocity = mDeviceState.getNavBarPosition().isRightEdge()
+                ? -velocityX
+                : mDeviceState.getNavBarPosition().isLeftEdge()
+                        ? velocityX
+                        : -velocityY;
 
         final boolean triggerQuickstep;
         int touch = Touch.FLING;
@@ -150,9 +151,9 @@
         }
 
         if (triggerQuickstep) {
-            mActivityInterface.closeOverlay();
-            ActivityManagerWrapper.getInstance()
-                    .closeSystemWindows(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS);
+            mContext.startActivity(new Intent(Intent.ACTION_MAIN)
+                    .addCategory(Intent.CATEGORY_HOME)
+                    .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
             ActiveGestureLog.INSTANCE.addLog("startQuickstep");
             BaseActivity activity = BaseDraggingActivity.fromContext(mContext);
             int pageIndex = -1; // This number doesn't reflect workspace page index.
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/AssistantUtilities.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/AssistantUtilities.java
new file mode 100644
index 0000000..552db1f
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/AssistantUtilities.java
@@ -0,0 +1,47 @@
+/*
+ * 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.quickstep.util;
+
+import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.ACTIVITY_TYPE_ASSISTANT;
+
+import android.annotation.TargetApi;
+import android.app.TaskInfo;
+import android.content.Intent;
+import android.os.Build;
+
+import com.android.systemui.shared.system.ActivityManagerWrapper;
+import com.android.systemui.shared.system.TaskInfoCompat;
+
+/**
+ * Utility class for interacting with the Assistant.
+ */
+@TargetApi(Build.VERSION_CODES.Q)
+public final class AssistantUtilities {
+
+    /** Returns true if an Assistant activity that is excluded from recents is running. */
+    public static boolean isExcludedAssistantRunning() {
+        return isExcludedAssistant(ActivityManagerWrapper.getInstance().getRunningTask());
+    }
+
+    /** Returns true if the given task holds an Assistant activity that is excluded from recents. */
+    public static boolean isExcludedAssistant(TaskInfo info) {
+        return info != null
+            && TaskInfoCompat.getActivityType(info) == ACTIVITY_TYPE_ASSISTANT
+            && (info.baseIntent.getFlags() & Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) != 0;
+    }
+
+    private AssistantUtilities() {}
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/NavBarPosition.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/NavBarPosition.java
deleted file mode 100644
index e2524b1..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/NavBarPosition.java
+++ /dev/null
@@ -1,54 +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.quickstep.util;
-
-import static com.android.launcher3.uioverrides.QuickstepLauncher.ROTATION_LANDSCAPE;
-import static com.android.launcher3.uioverrides.QuickstepLauncher.ROTATION_SEASCAPE;
-import static com.android.quickstep.SysUINavigationMode.Mode.NO_BUTTON;
-
-import android.content.Context;
-import android.view.Surface;
-
-import com.android.launcher3.graphics.RotationMode;
-import com.android.launcher3.util.DefaultDisplay;
-import com.android.quickstep.SysUINavigationMode;
-
-/**
- * Utility class to check nav bar position
- */
-public class NavBarPosition {
-
-    private final SysUINavigationMode.Mode mMode;
-    private final int mDisplayRotation;
-
-    public NavBarPosition(Context context) {
-        mMode = SysUINavigationMode.getMode(context);
-        mDisplayRotation = DefaultDisplay.INSTANCE.get(context).getInfo().rotation;
-    }
-
-    public boolean isRightEdge() {
-        return mMode != NO_BUTTON && mDisplayRotation == Surface.ROTATION_90;
-    }
-
-    public boolean isLeftEdge() {
-        return mMode != NO_BUTTON && mDisplayRotation == Surface.ROTATION_270;
-    }
-
-    public RotationMode getRotationMode() {
-        return isLeftEdge() ? ROTATION_SEASCAPE
-                : (isRightEdge() ? ROTATION_LANDSCAPE : RotationMode.NORMAL);
-    }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java
index 5d4665d..cf616fe 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java
@@ -60,6 +60,7 @@
 import android.graphics.drawable.Drawable;
 import android.os.Build;
 import android.os.Handler;
+import android.os.UserHandle;
 import android.text.Layout;
 import android.text.StaticLayout;
 import android.text.TextPaint;
@@ -105,7 +106,7 @@
 import com.android.quickstep.RecentsAnimationController;
 import com.android.quickstep.RecentsAnimationTargets;
 import com.android.quickstep.RecentsModel;
-import com.android.quickstep.RecentsModel.TaskThumbnailChangeListener;
+import com.android.quickstep.RecentsModel.TaskVisualsChangeListener;
 import com.android.quickstep.TaskThumbnailCache;
 import com.android.quickstep.TaskUtils;
 import com.android.quickstep.ViewUtils;
@@ -127,7 +128,7 @@
 @TargetApi(Build.VERSION_CODES.P)
 public abstract class RecentsView<T extends BaseActivity> extends PagedView implements Insettable,
         TaskThumbnailCache.HighResLoadingState.HighResLoadingStateChangedCallback,
-        InvariantDeviceProfile.OnIDPChangeListener, TaskThumbnailChangeListener {
+        InvariantDeviceProfile.OnIDPChangeListener, TaskVisualsChangeListener {
 
     private static final String TAG = RecentsView.class.getSimpleName();
 
@@ -383,6 +384,21 @@
         return null;
     }
 
+    @Override
+    public void onTaskIconChanged(String pkg, UserHandle user) {
+        for (int i = 0; i < getTaskViewCount(); i++) {
+            TaskView tv = getTaskViewAt(i);
+            Task task = tv.getTask();
+            if (task != null && task.key != null && pkg.equals(task.key.getPackageName())
+                    && task.key.userId == user.getIdentifier()) {
+                task.icon = null;
+                if (tv.getIconView().getDrawable() != null) {
+                    tv.onTaskListVisibilityChanged(true /* visible */);
+                }
+            }
+        }
+    }
+
     /**
      * Update the thumbnail of the task.
      * @param refreshNow Refresh immediately if it's true.
@@ -1673,8 +1689,8 @@
 
         if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SCROLLED) {
             final int[] visibleTasks = getVisibleChildrenRange();
-            event.setFromIndex(taskViewCount - visibleTasks[1] - 1);
-            event.setToIndex(taskViewCount - visibleTasks[0] - 1);
+            event.setFromIndex(taskViewCount - visibleTasks[1]);
+            event.setToIndex(taskViewCount - visibleTasks[0]);
             event.setItemCount(taskViewCount);
         }
     }
@@ -1726,7 +1742,17 @@
             return;
         }
 
-        mRecentsAnimationController.finish(toRecents, onFinishComplete);
+        mRecentsAnimationController.finish(toRecents, () -> {
+            if (onFinishComplete != null) {
+                onFinishComplete.run();
+                // After we finish the recents animation, the current task id should be correctly
+                // reset so that when the task is launched from Overview later, it goes through the
+                // flow of starting a new task instead of finishing recents animation to app. A
+                // typical example of this is (1) user swipes up from app to Overview (2) user
+                // taps on QSB (3) user goes back to Overview and launch the most recent task.
+                setCurrentTask(-1);
+            }
+        });
     }
 
     public void setDisallowScrollToClearAll(boolean disallowScrollToClearAll) {
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskView.java
index 0dfc39b..a1775f4 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskView.java
@@ -287,11 +287,19 @@
     public void launchTask(boolean animate, boolean freezeTaskList, Consumer<Boolean> resultCallback,
             Handler resultCallbackHandler) {
         if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+            RecentsView recentsView = getRecentsView();
             if (isRunningTask()) {
-                getRecentsView().finishRecentsAnimation(false /* toRecents */,
+                recentsView.finishRecentsAnimation(false /* toRecents */,
                         () -> resultCallbackHandler.post(() -> resultCallback.accept(true)));
             } else {
-                launchTaskInternal(animate, freezeTaskList, resultCallback, resultCallbackHandler);
+                // This is a workaround against the WM issue that app open is not correctly animated
+                // when recents animation is being cleaned up (b/143774568). When that's possible,
+                // we should rely on the framework side to cancel the recents animation, and we will
+                // clean up the screenshot on the launcher side while we launch the next task.
+                recentsView.switchToScreenshot(null,
+                        () -> recentsView.finishRecentsAnimation(true /* toRecents */,
+                                () -> launchTaskInternal(animate, freezeTaskList, resultCallback,
+                                        resultCallbackHandler)));
             }
         } else {
             launchTaskInternal(animate, freezeTaskList, resultCallback, resultCallbackHandler);
@@ -727,8 +735,8 @@
         final RecentsView recentsView = getRecentsView();
         final AccessibilityNodeInfo.CollectionItemInfo itemInfo =
                 AccessibilityNodeInfo.CollectionItemInfo.obtain(
-                        0, 1, recentsView.getChildCount() - recentsView.indexOfChild(this) - 1, 1,
-                        false);
+                        0, 1, recentsView.getTaskViewCount() - recentsView.indexOfChild(this) - 1,
+                        1, false);
         info.setCollectionItemInfo(itemInfo);
     }
 
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/GestureState.java b/quickstep/src/com/android/quickstep/GestureState.java
index 98ff410..ae0886b 100644
--- a/quickstep/src/com/android/quickstep/GestureState.java
+++ b/quickstep/src/com/android/quickstep/GestureState.java
@@ -17,7 +17,10 @@
 
 import static com.android.quickstep.MultiStateCallback.DEBUG_STATES;
 
+import android.app.ActivityManager;
+import android.content.Intent;
 import com.android.launcher3.BaseDraggingActivity;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
 import com.android.systemui.shared.recents.model.ThumbnailData;
 import java.util.ArrayList;
@@ -56,6 +59,8 @@
         public final boolean recentsAttachedToAppWindow;
     }
 
+    private static final String TAG = "GestureState";
+
     private static final ArrayList<String> STATE_NAMES = new ArrayList<>();
     private static int FLAG_COUNT = 0;
     private static int getFlagForIndex(String name) {
@@ -98,25 +103,39 @@
 
 
     // Needed to interact with the current activity
+    private final Intent mHomeIntent;
+    private final Intent mOverviewIntent;
     private final BaseActivityInterface mActivityInterface;
     private final MultiStateCallback mStateCallback;
     private final int mGestureId;
 
+    private ActivityManager.RunningTaskInfo mRunningTask;
     private GestureEndTarget mEndTarget;
     // TODO: This can be removed once we stop finishing the animation when starting a new task
     private int mFinishingRecentsAnimationTaskId = -1;
 
-    public GestureState(BaseActivityInterface activityInterface, int gestureId) {
-        mActivityInterface = activityInterface;
-        mGestureId = gestureId;
+    public GestureState(OverviewComponentObserver componentObserver, int gestureId) {
+        mHomeIntent = componentObserver.getHomeIntent();
+        mOverviewIntent = componentObserver.getOverviewIntent();
+        mActivityInterface = componentObserver.getActivityInterface();
         mStateCallback = new MultiStateCallback(STATE_NAMES.toArray(new String[0]));
+        mGestureId = gestureId;
     }
 
     public GestureState() {
         // Do nothing, only used for initializing the gesture state prior to user unlock
+        mHomeIntent = new Intent();
+        mOverviewIntent = new Intent();
         mActivityInterface = null;
-        mGestureId = -1;
         mStateCallback = new MultiStateCallback(STATE_NAMES.toArray(new String[0]));
+        mGestureId = -1;
+    }
+
+    /**
+     * @return whether the gesture state has the provided {@param stateMask} flags set.
+     */
+    public boolean hasState(int stateMask) {
+        return mStateCallback.hasStates(stateMask);
     }
 
     /**
@@ -134,6 +153,20 @@
     }
 
     /**
+     * @return the intent for the Home component.
+     */
+    public Intent getHomeIntent() {
+        return mHomeIntent;
+    }
+
+    /**
+     * @return the intent for the Overview component.
+     */
+    public Intent getOverviewIntent() {
+        return mOverviewIntent;
+    }
+
+    /**
      * @return the interface to the activity handing the UI updates for this gesture.
      */
     public <T extends BaseDraggingActivity> BaseActivityInterface<T> getActivityInterface() {
@@ -148,6 +181,27 @@
     }
 
     /**
+     * @return the running task for this gesture.
+     */
+    public ActivityManager.RunningTaskInfo getRunningTask() {
+        return mRunningTask;
+    }
+
+    /**
+     * @return the running task id for this gesture.
+     */
+    public int getRunningTaskId() {
+        return mRunningTask != null ? mRunningTask.taskId : -1;
+    }
+
+    /**
+     * Updates the running task for the gesture to be the given {@param runningTask}.
+     */
+    public void updateRunningTask(ActivityManager.RunningTaskInfo runningTask) {
+        mRunningTask = runningTask;
+    }
+
+    /**
      * @return the end target for this gesture (if known).
      */
     public GestureEndTarget getEndTarget() {
@@ -155,14 +209,6 @@
     }
 
     /**
-     * @return whether the current gesture is still running a recents animation to a state in the
-     *         Launcher or Recents activity.
-     */
-    public boolean isRunningAnimationToLauncher() {
-        return isRecentsAnimationRunning() && mEndTarget != null && mEndTarget.isLauncher;
-    }
-
-    /**
      * Sets the end target of this gesture and immediately notifies the state changes.
      */
     public void setEndTarget(GestureEndTarget target) {
@@ -202,6 +248,15 @@
     }
 
     /**
+     * @return whether the current gesture is still running a recents animation to a state in the
+     *         Launcher or Recents activity.
+     * Updates the running task for the gesture to be the given {@param runningTask}.
+     */
+    public boolean isRunningAnimationToLauncher() {
+        return isRecentsAnimationRunning() && mEndTarget != null && mEndTarget.isLauncher;
+    }
+
+    /**
      * @return whether the recents animation is started but not yet ended
      */
     public boolean isRecentsAnimationRunning() {
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationController.java b/quickstep/src/com/android/quickstep/RecentsAnimationController.java
index 9d5120d..333e179 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationController.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationController.java
@@ -48,11 +48,10 @@
     private final Consumer<RecentsAnimationController> mOnFinishedListener;
     private final boolean mShouldMinimizeSplitScreen;
 
-    private boolean mWindowThresholdCrossed = false;
-
     private InputConsumerController mInputConsumerController;
     private Supplier<InputConsumer> mInputProxySupplier;
     private InputConsumer mInputConsumer;
+    private boolean mWindowThresholdCrossed = false;
     private boolean mTouchInProgress;
     private boolean mFinishPending;
 
@@ -62,8 +61,6 @@
         mController = controller;
         mOnFinishedListener = onFinishedListener;
         mShouldMinimizeSplitScreen = shouldMinimizeSplitScreen;
-
-        setWindowThresholdCrossed(mWindowThresholdCrossed);
     }
 
     /**
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
index 1855e64..81f411e 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
@@ -17,8 +17,10 @@
 
 import static android.content.Intent.ACTION_USER_UNLOCKED;
 
+import static android.provider.Settings.System.HAPTIC_FEEDBACK_ENABLED;
 import static com.android.launcher3.ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE;
 import static com.android.launcher3.ResourceUtils.NAVBAR_LANDSCAPE_LEFT_RIGHT_SIZE;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.quickstep.SysUINavigationMode.Mode.NO_BUTTON;
 import static com.android.quickstep.SysUINavigationMode.Mode.THREE_BUTTONS;
 import static com.android.quickstep.SysUINavigationMode.Mode.TWO_BUTTONS;
@@ -35,14 +37,17 @@
 import android.app.ActivityManager;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
+import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.res.Resources;
+import android.database.ContentObserver;
 import android.graphics.Point;
 import android.graphics.RectF;
 import android.graphics.Region;
 import android.os.Process;
+import android.provider.Settings;
 import android.text.TextUtils;
 import android.util.Log;
 import android.view.MotionEvent;
@@ -57,6 +62,7 @@
 import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.util.DefaultDisplay;
 import com.android.quickstep.SysUINavigationMode.NavigationModeChangeListener;
+import com.android.quickstep.util.NavBarPosition;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.QuickStepContract;
 import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags;
@@ -72,16 +78,17 @@
         NavigationModeChangeListener,
         DefaultDisplay.DisplayInfoChangeListener {
 
-    private Context mContext;
-    private UserManagerCompat mUserManager;
-    private SysUINavigationMode mSysUiNavMode;
-    private DefaultDisplay mDefaultDisplay;
-    private int mDisplayId;
+    private final Context mContext;
+    private final UserManagerCompat mUserManager;
+    private final SysUINavigationMode mSysUiNavMode;
+    private final DefaultDisplay mDefaultDisplay;
+    private final int mDisplayId;
 
     private final ArrayList<Runnable> mOnDestroyActions = new ArrayList<>();
 
     private @SystemUiStateFlags int mSystemUiStateFlags;
     private SysUINavigationMode.Mode mMode = THREE_BUTTONS;
+    private NavBarPosition mNavBarPosition;
 
     private final RectF mSwipeUpTouchRegion = new RectF();
     private final Region mDeferredGestureRegion = new Region();
@@ -108,11 +115,13 @@
     private ComponentName mGestureBlockedActivity;
 
     public RecentsAnimationDeviceState(Context context) {
+        final ContentResolver resolver = context.getContentResolver();
         mContext = context;
         mUserManager = UserManagerCompat.getInstance(context);
         mSysUiNavMode = SysUINavigationMode.INSTANCE.get(context);
         mDefaultDisplay = DefaultDisplay.INSTANCE.get(context);
         mDisplayId = mDefaultDisplay.getInfo().id;
+        runOnDestroy(() -> mDefaultDisplay.removeChangeListener(this));
 
         // Register for user unlocked if necessary
         mIsUserUnlocked = mUserManager.isUserUnlocked(Process.myUserHandle());
@@ -155,7 +164,6 @@
         for (Runnable r : mOnDestroyActions) {
             r.run();
         }
-        mDefaultDisplay.removeChangeListener(this);
     }
 
     /**
@@ -183,6 +191,7 @@
             mExclusionListener.unregister();
         }
         mMode = newMode;
+        mNavBarPosition = new NavBarPosition(mMode, mDefaultDisplay.getInfo());
     }
 
     @Override
@@ -191,6 +200,7 @@
             return;
         }
 
+        mNavBarPosition = new NavBarPosition(mMode, info);
         updateGestureTouchRegions();
     }
 
@@ -202,6 +212,13 @@
     }
 
     /**
+     * @return the nav bar position for the current nav bar mode and display rotation.
+     */
+    public NavBarPosition getNavBarPosition() {
+        return mNavBarPosition;
+    }
+
+    /**
      * @return whether the current nav mode is fully gestural.
      */
     public boolean isFullyGesturalNavMode() {
diff --git a/quickstep/src/com/android/quickstep/RecentsModel.java b/quickstep/src/com/android/quickstep/RecentsModel.java
index f248423..517501a 100644
--- a/quickstep/src/com/android/quickstep/RecentsModel.java
+++ b/quickstep/src/com/android/quickstep/RecentsModel.java
@@ -28,7 +28,9 @@
 import android.os.Build;
 import android.os.Looper;
 import android.os.Process;
+import android.os.UserHandle;
 
+import com.android.launcher3.icons.IconProvider;
 import com.android.launcher3.util.MainThreadInitializedObject;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.recents.model.ThumbnailData;
@@ -50,7 +52,7 @@
     public static final MainThreadInitializedObject<RecentsModel> INSTANCE =
             new MainThreadInitializedObject<>(RecentsModel::new);
 
-    private final List<TaskThumbnailChangeListener> mThumbnailChangeListeners = new ArrayList<>();
+    private final List<TaskVisualsChangeListener> mThumbnailChangeListeners = new ArrayList<>();
     private final Context mContext;
 
     private final RecentTasksList mTaskList;
@@ -65,7 +67,10 @@
                 new KeyguardManagerCompat(context), ActivityManagerWrapper.getInstance());
         mIconCache = new TaskIconCache(context, looper);
         mThumbnailCache = new TaskThumbnailCache(context, looper);
+
         ActivityManagerWrapper.getInstance().registerTaskStackListener(this);
+        IconProvider.registerIconChangeListener(context,
+                this::onPackageIconChanged, MAIN_EXECUTOR.getHandler());
     }
 
     public TaskIconCache getIconCache() {
@@ -178,16 +183,40 @@
         }
     }
 
-    public void addThumbnailChangeListener(TaskThumbnailChangeListener listener) {
+    private void onPackageIconChanged(String pkg, UserHandle user) {
+        mIconCache.invalidateCacheEntries(pkg, user);
+        for (int i = mThumbnailChangeListeners.size() - 1; i >= 0; i--) {
+            mThumbnailChangeListeners.get(i).onTaskIconChanged(pkg, user);
+        }
+    }
+
+    /**
+     * Adds a listener for visuals changes
+     */
+    public void addThumbnailChangeListener(TaskVisualsChangeListener listener) {
         mThumbnailChangeListeners.add(listener);
     }
 
-    public void removeThumbnailChangeListener(TaskThumbnailChangeListener listener) {
+    /**
+     * Removes a previously added listener
+     */
+    public void removeThumbnailChangeListener(TaskVisualsChangeListener listener) {
         mThumbnailChangeListeners.remove(listener);
     }
 
-    public interface TaskThumbnailChangeListener {
+    /**
+     * Listener for receiving various task properties changes
+     */
+    public interface TaskVisualsChangeListener {
 
+        /**
+         * Called whn the task thumbnail changes
+         */
         Task onTaskThumbnailChanged(int taskId, ThumbnailData thumbnailData);
+
+        /**
+         * Called when the icon for a task changes
+         */
+        void onTaskIconChanged(String pkg, UserHandle user);
     }
 }
diff --git a/quickstep/src/com/android/quickstep/TaskIconCache.java b/quickstep/src/com/android/quickstep/TaskIconCache.java
index 873f29c..e590aea 100644
--- a/quickstep/src/com/android/quickstep/TaskIconCache.java
+++ b/quickstep/src/com/android/quickstep/TaskIconCache.java
@@ -15,6 +15,7 @@
  */
 package com.android.quickstep;
 
+import static com.android.launcher3.FastBitmapDrawable.newIcon;
 import static com.android.launcher3.uioverrides.QuickstepLauncher.GO_LOW_RAM_RECENTS_ENABLED;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 
@@ -37,14 +38,14 @@
 import com.android.launcher3.FastBitmapDrawable;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
-import com.android.launcher3.graphics.DrawableFactory;
 import com.android.launcher3.icons.BitmapInfo;
+import com.android.launcher3.icons.IconProvider;
 import com.android.launcher3.icons.LauncherIcons;
 import com.android.launcher3.icons.cache.HandlerRunnable;
 import com.android.launcher3.util.Preconditions;
+import com.android.quickstep.util.TaskKeyLruCache;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.recents.model.Task.TaskKey;
-import com.android.systemui.shared.recents.model.TaskKeyLruCache;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.PackageManagerWrapper;
 
@@ -61,6 +62,7 @@
     private final Context mContext;
     private final TaskKeyLruCache<TaskCacheEntry> mIconCache;
     private final SparseArray<BitmapInfo> mDefaultIcons = new SparseArray<>();
+    private final IconProvider mIconProvider;
 
     public TaskIconCache(Context context, Looper backgroundLooper) {
         mContext = context;
@@ -70,6 +72,7 @@
         Resources res = context.getResources();
         int cacheSize = res.getInteger(R.integer.recentsIconCacheSize);
         mIconCache = new TaskKeyLruCache<>(cacheSize);
+        mIconProvider = new IconProvider(context);
     }
 
     /**
@@ -115,6 +118,12 @@
         mIconCache.remove(taskKey);
     }
 
+    void invalidateCacheEntries(String pkg, UserHandle handle) {
+        Utilities.postAsyncCallback(mBackgroundHandler,
+                () -> mIconCache.removeAll(key ->
+                        pkg.equals(key.getPackageName()) && handle.getIdentifier() == key.userId));
+    }
+
     @WorkerThread
     private TaskCacheEntry getCacheEntry(Task task) {
         TaskCacheEntry entry = mIconCache.getAndInvalidateIfModified(task.key);
@@ -143,12 +152,11 @@
                     key.getComponent(), key.userId);
             if (activityInfo != null) {
                 BitmapInfo bitmapInfo = getBitmapInfo(
-                        activityInfo.loadUnbadgedIcon(mContext.getPackageManager()),
+                        mIconProvider.getIcon(activityInfo, UserHandle.of(key.userId)),
                         key.userId,
                         desc.getPrimaryColor(),
                         activityInfo.applicationInfo.isInstantApp());
-                entry.icon = DrawableFactory.INSTANCE.get(mContext).newIcon(
-                        mContext, bitmapInfo, activityInfo);
+                entry.icon = newIcon(mContext, bitmapInfo);
             } else {
                 entry.icon = getDefaultIcon(key.userId);
             }
diff --git a/quickstep/src/com/android/quickstep/TaskThumbnailCache.java b/quickstep/src/com/android/quickstep/TaskThumbnailCache.java
index 3b50c26..e47df6c 100644
--- a/quickstep/src/com/android/quickstep/TaskThumbnailCache.java
+++ b/quickstep/src/com/android/quickstep/TaskThumbnailCache.java
@@ -27,9 +27,9 @@
 import com.android.launcher3.Utilities;
 import com.android.launcher3.icons.cache.HandlerRunnable;
 import com.android.launcher3.util.Preconditions;
+import com.android.quickstep.util.TaskKeyLruCache;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.recents.model.Task.TaskKey;
-import com.android.systemui.shared.recents.model.TaskKeyLruCache;
 import com.android.systemui.shared.recents.model.ThumbnailData;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 
@@ -41,7 +41,7 @@
     private final Handler mBackgroundHandler;
 
     private final int mCacheSize;
-    private final ThumbnailCache mCache;
+    private final TaskKeyLruCache<ThumbnailData> mCache;
     private final HighResLoadingState mHighResLoadingState;
 
     public static class HighResLoadingState {
@@ -100,7 +100,7 @@
 
         Resources res = context.getResources();
         mCacheSize = res.getInteger(R.integer.recentsThumbnailCacheSize);
-        mCache = new ThumbnailCache(mCacheSize);
+        mCache = new TaskKeyLruCache<>(mCacheSize);
     }
 
     /**
@@ -223,21 +223,4 @@
             this.reducedResolution = reducedResolution;
         }
     }
-
-    private static class ThumbnailCache extends TaskKeyLruCache<ThumbnailData> {
-
-        public ThumbnailCache(int cacheSize) {
-            super(cacheSize);
-        }
-
-        /**
-         * Updates the cache entry if it is already present in the cache
-         */
-        public void updateIfAlreadyInCache(int taskId, ThumbnailData thumbnailData) {
-            ThumbnailData oldData = getCacheEntry(taskId);
-            if (oldData != null) {
-                putCacheEntry(taskId, thumbnailData);
-            }
-        }
-    }
 }
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/src/com/android/quickstep/util/NavBarPosition.java b/quickstep/src/com/android/quickstep/util/NavBarPosition.java
new file mode 100644
index 0000000..a4614de
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/NavBarPosition.java
@@ -0,0 +1,127 @@
+/*
+ * 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.quickstep.util;
+
+import static com.android.quickstep.SysUINavigationMode.Mode.NO_BUTTON;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.view.Gravity;
+import android.view.Surface;
+
+import com.android.launcher3.graphics.RotationMode;
+import com.android.launcher3.util.DefaultDisplay;
+import com.android.quickstep.SysUINavigationMode;
+
+/**
+ * Utility class to check nav bar position.
+ */
+public class NavBarPosition {
+
+    public static RotationMode ROTATION_LANDSCAPE = new RotationMode(-90) {
+        @Override
+        public void mapRect(int left, int top, int right, int bottom, Rect out) {
+            out.left = top;
+            out.top = right;
+            out.right = bottom;
+            out.bottom = left;
+        }
+
+        @Override
+        public void mapInsets(Context context, Rect insets, Rect out) {
+            // If there is a display cutout, the top insets in portrait would also include the
+            // cutout, which we will get as the left inset in landscape. Using the max of left and
+            // top allows us to cover both cases (with or without cutout).
+            if (SysUINavigationMode.getMode(context) == NO_BUTTON) {
+                out.top = Math.max(insets.top, insets.left);
+                out.bottom = Math.max(insets.right, insets.bottom);
+                out.left = out.right = 0;
+            } else {
+                out.top = Math.max(insets.top, insets.left);
+                out.bottom = insets.right;
+                out.left = insets.bottom;
+                out.right = 0;
+            }
+        }
+    };
+
+    public static RotationMode ROTATION_SEASCAPE = new RotationMode(90) {
+        @Override
+        public void mapRect(int left, int top, int right, int bottom, Rect out) {
+            out.left = bottom;
+            out.top = left;
+            out.right = top;
+            out.bottom = right;
+        }
+
+        @Override
+        public void mapInsets(Context context, Rect insets, Rect out) {
+            if (SysUINavigationMode.getMode(context) == NO_BUTTON) {
+                out.top = Math.max(insets.top, insets.right);
+                out.bottom = Math.max(insets.left, insets.bottom);
+                out.left = out.right = 0;
+            } else {
+                out.top = Math.max(insets.top, insets.right);
+                out.bottom = insets.left;
+                out.right = insets.bottom;
+                out.left = 0;
+            }
+        }
+
+        @Override
+        public int toNaturalGravity(int absoluteGravity) {
+            int horizontalGravity = absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK;
+            int verticalGravity = absoluteGravity & Gravity.VERTICAL_GRAVITY_MASK;
+
+            if (horizontalGravity == Gravity.RIGHT) {
+                horizontalGravity = Gravity.LEFT;
+            } else if (horizontalGravity == Gravity.LEFT) {
+                horizontalGravity = Gravity.RIGHT;
+            }
+
+            if (verticalGravity == Gravity.TOP) {
+                verticalGravity = Gravity.BOTTOM;
+            } else if (verticalGravity == Gravity.BOTTOM) {
+                verticalGravity = Gravity.TOP;
+            }
+
+            return ((absoluteGravity & ~Gravity.HORIZONTAL_GRAVITY_MASK)
+                    & ~Gravity.VERTICAL_GRAVITY_MASK)
+                    | horizontalGravity | verticalGravity;
+        }
+    };
+
+    private final SysUINavigationMode.Mode mMode;
+    private final int mDisplayRotation;
+
+    public NavBarPosition(SysUINavigationMode.Mode mode, DefaultDisplay.Info info) {
+        mMode = mode;
+        mDisplayRotation = info.rotation;
+    }
+
+    public boolean isRightEdge() {
+        return mMode != NO_BUTTON && mDisplayRotation == Surface.ROTATION_90;
+    }
+
+    public boolean isLeftEdge() {
+        return mMode != NO_BUTTON && mDisplayRotation == Surface.ROTATION_270;
+    }
+
+    public RotationMode getRotationMode() {
+        return isLeftEdge() ? ROTATION_SEASCAPE
+                : (isRightEdge() ? ROTATION_LANDSCAPE : RotationMode.NORMAL);
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/util/TaskKeyLruCache.java b/quickstep/src/com/android/quickstep/util/TaskKeyLruCache.java
new file mode 100644
index 0000000..d87feec
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/TaskKeyLruCache.java
@@ -0,0 +1,124 @@
+/*
+ * 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.quickstep.util;
+
+import android.util.Log;
+
+import com.android.systemui.shared.recents.model.Task.TaskKey;
+
+import java.util.LinkedHashMap;
+import java.util.function.Predicate;
+
+/**
+ * A simple LRU cache for task key entries
+ * @param <V> The type of the value
+ */
+public class TaskKeyLruCache<V> {
+
+    private final MyLinkedHashMap<V> mMap;
+
+    public TaskKeyLruCache(int maxSize) {
+        mMap = new MyLinkedHashMap<>(maxSize);
+    }
+
+    /**
+     * Removes all entries from the cache
+     */
+    public synchronized void evictAll() {
+        mMap.clear();
+    }
+
+    /**
+     * Removes a particular entry from the cache
+     */
+    public synchronized void remove(TaskKey key) {
+        mMap.remove(key.id);
+    }
+
+    /**
+     * Removes all entries matching keyCheck
+     */
+    public synchronized void removeAll(Predicate<TaskKey> keyCheck) {
+        mMap.entrySet().removeIf(e -> keyCheck.test(e.getValue().mKey));
+    }
+
+    /**
+     * Gets the entry if it is still valid
+     */
+    public synchronized V getAndInvalidateIfModified(TaskKey key) {
+        Entry<V> entry = mMap.get(key.id);
+
+        if (entry != null && entry.mKey.windowingMode == key.windowingMode
+                && entry.mKey.lastActiveTime == key.lastActiveTime) {
+            return entry.mValue;
+        } else {
+            remove(key);
+            return null;
+        }
+    }
+
+    /**
+     * Adds an entry to the cache, optionally evicting the last accessed entry
+     */
+    public final synchronized void put(TaskKey key, V value) {
+        if (key != null && value != null) {
+            mMap.put(key.id, new Entry<>(key, value));
+        } else {
+            Log.e("TaskKeyCache", "Unexpected null key or value: " + key + ", " + value);
+        }
+    }
+
+    /**
+     * Updates the cache entry if it is already present in the cache
+     */
+    public synchronized void updateIfAlreadyInCache(int taskId, V data) {
+        Entry<V> entry = mMap.get(taskId);
+        if (entry != null) {
+            entry.mValue = data;
+        }
+    }
+
+    private static class Entry<V> {
+
+        final TaskKey mKey;
+        V mValue;
+
+        Entry(TaskKey key, V value) {
+            mKey = key;
+            mValue = value;
+        }
+
+        @Override
+        public int hashCode() {
+            return mKey.id;
+        }
+    }
+
+    private static class MyLinkedHashMap<V> extends LinkedHashMap<Integer, Entry<V>> {
+
+        private final int mMaxSize;
+
+        MyLinkedHashMap(int maxSize) {
+            super(0, 0.75f, true /* accessOrder */);
+            mMaxSize = maxSize;
+        }
+
+        @Override
+        protected boolean removeEldestEntry(Entry<Integer, TaskKeyLruCache.Entry<V>> eldest) {
+            return size() > mMaxSize;
+        }
+    }
+}
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/res/values/config.xml b/res/values/config.xml
index 10671c5..2a1f6f7 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -1,5 +1,5 @@
 <resources>
-<!-- Miscellaneous -->
+    <!-- Miscellaneous -->
     <bool name="config_largeHeap">false</bool>
     <bool name="is_tablet">false</bool>
     <bool name="is_large_tablet">false</bool>
@@ -21,10 +21,10 @@
     <!-- String representing the fragment class for settings activity.-->
     <string name="settings_fragment_name" translatable="false">com.android.launcher3.settings.SettingsActivity$LauncherSettingsFragment</string>
 
-<!-- DragController -->
+    <!-- DragController -->
     <item type="id" name="drag_event_parity" />
 
-<!-- AllApps & Launcher transitions -->
+    <!-- AllApps & Launcher transitions -->
     <!-- Out of 100, the percent to shrink the workspace during spring loaded mode. -->
     <integer name="config_workspaceSpringLoadShrinkPercentage">90</integer>
 
@@ -34,7 +34,7 @@
     <!-- View tag key used to store SpringAnimation data. -->
     <item type="id" name="spring_animation_tag" />
 
-<!-- Workspace -->
+    <!-- Workspace -->
     <!-- The duration (in ms) of the fade animation on the object outlines, used when
          we are dragging objects around on the home screen. -->
     <integer name="config_dragOutlineFadeTime">900</integer>
@@ -57,13 +57,11 @@
     <!-- The duration of the caret animation -->
     <integer name="config_caretAnimationDuration">200</integer>
 
-<!-- Hotseat -->
+    <!-- Hotseat -->
     <bool name="hotseat_transpose_layout_with_orientation">true</bool>
 
     <!-- Various classes overriden by projects/build flavors. -->
     <string name="app_filter_class" translatable="false"></string>
-    <string name="icon_provider_class" translatable="false"></string>
-    <string name="drawable_factory_class" translatable="false"></string>
     <string name="user_event_dispatcher_class" translatable="false"></string>
     <string name="stats_log_manager_class" translatable="false"></string>
     <string name="app_transition_manager_class" translatable="false"></string>
@@ -73,12 +71,6 @@
     <string name="test_information_handler_class" translatable="false"></string>
     <string name="launcher_activity_logic_class" translatable="false"></string>
 
-    <!-- Package name of the default wallpaper picker. -->
-    <string name="wallpaper_picker_package" translatable="false"></string>
-
-    <!-- Whitelisted package to retrieve packagename for badge. Can be empty. -->
-    <string name="shortcutinfo_badgepkg_whitelist" translatable="false"></string>
-
     <!-- View ID to use for QSB widget -->
     <item type="id" name="qsb_widget" />
 
@@ -96,7 +88,12 @@
     <integer name="config_popupArrowOpenCloseDuration">40</integer>
     <integer name="config_removeNotificationViewDuration">300</integer>
 
-<!-- Accessibility actions -->
+    <!-- Default packages -->
+    <string name="wallpaper_picker_package" translatable="false"></string>
+    <string name="calendar_component_name" translatable="false"></string>
+    <string name="clock_component_name" translatable="false"></string>
+
+    <!-- Accessibility actions -->
     <item type="id" name="action_remove" />
     <item type="id" name="action_uninstall" />
     <item type="id" name="action_reconfigure" />
@@ -111,10 +108,10 @@
     <item type="id" name="action_dismiss_notification" />
     <item type="id" name="action_remote_action_shortcut" />
 
-<!-- QSB IDs. DO not change -->
+    <!-- QSB IDs. DO not change -->
     <item type="id" name="search_container_workspace" />
     <item type="id" name="search_container_all_apps" />
 
-<!-- Recents -->
+    <!-- Recents -->
     <item type="id" name="overview_panel"/>
 </resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 9d9c2e8..dec8939 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -102,9 +102,11 @@
     <string name="app_info_drop_target_label">App info</string>
     <!-- Label for install drop target. [CHAR_LIMIT=20] -->
     <string name="install_drop_target_label">Install</string>
-
     <!-- Label for install dismiss prediction. -->
     <string translatable="false" name="dismiss_prediction_label">Dismiss prediction</string>
+    <!-- Label for pinning predicted app. -->
+    <string name="pin_prediction" translatable="false">Pin Prediction</string>
+
 
     <!-- Permissions: -->
     <skip />
diff --git a/robolectric_tests/src/com/android/launcher3/model/BaseModelUpdateTaskTestCase.java b/robolectric_tests/src/com/android/launcher3/model/BaseModelUpdateTaskTestCase.java
index 32eb2ec..5b6d94d 100644
--- a/robolectric_tests/src/com/android/launcher3/model/BaseModelUpdateTaskTestCase.java
+++ b/robolectric_tests/src/com/android/launcher3/model/BaseModelUpdateTaskTestCase.java
@@ -202,15 +202,14 @@
             CacheEntry entry = mCache.get(new ComponentKey(componentName, user));
             if (entry == null) {
                 entry = new CacheEntry();
-                getDefaultIcon(user).applyTo(entry);
+                entry.bitmap = getDefaultIcon(user);
             }
             return entry;
         }
 
         public void addCache(ComponentName key, String title) {
             CacheEntry entry = new CacheEntry();
-            entry.icon = newIcon();
-            entry.color = Color.RED;
+            entry.bitmap = BitmapInfo.of(newIcon(), Color.RED);
             entry.title = title;
             mCache.put(new ComponentKey(key, Process.myUserHandle()), entry);
         }
diff --git a/robolectric_tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java b/robolectric_tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java
index 81b9043..69c5b00 100644
--- a/robolectric_tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java
+++ b/robolectric_tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java
@@ -2,13 +2,13 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNotSame;
-import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
 
 import com.android.launcher3.AppInfo;
 import com.android.launcher3.ItemInfo;
 import com.android.launcher3.WorkspaceItemInfo;
+import com.android.launcher3.icons.BitmapInfo;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -43,7 +43,7 @@
     public void testCacheUpdate_update_apps() throws Exception {
         // Clear all icons from apps list so that its easy to check what was updated
         for (AppInfo info : allAppsList.data) {
-            info.iconBitmap = null;
+            info.bitmap = BitmapInfo.LOW_RES_INFO;
         }
 
         executeTaskForTest(newTask(CacheDataUpdatedTask.OP_CACHE_UPDATE, "app1"));
@@ -56,9 +56,9 @@
         assertFalse(allAppsList.data.isEmpty());
         for (AppInfo info : allAppsList.data) {
             if (info.componentName.getPackageName().equals("app1")) {
-                assertNotNull(info.iconBitmap);
+                assertFalse(info.bitmap.isNullOrLowRes());
             } else {
-                assertNull(info.iconBitmap);
+                assertTrue(info.bitmap.isNullOrLowRes());
             }
         }
     }
@@ -85,10 +85,10 @@
         for (ItemInfo info : bgDataModel.itemsIdMap) {
             if (updates.contains(info.id)) {
                 assertEquals(NEW_LABEL_PREFIX + info.id, info.title);
-                assertNotNull(((WorkspaceItemInfo) info).iconBitmap);
+                assertFalse(((WorkspaceItemInfo) info).bitmap.isNullOrLowRes());
             } else {
                 assertNotSame(NEW_LABEL_PREFIX + info.id, info.title);
-                assertNull(((WorkspaceItemInfo) info).iconBitmap);
+                assertTrue(((WorkspaceItemInfo) info).bitmap.isNullOrLowRes());
             }
         }
     }
diff --git a/src/com/android/launcher3/BaseDraggingActivity.java b/src/com/android/launcher3/BaseDraggingActivity.java
index d24de8e..772eb00 100644
--- a/src/com/android/launcher3/BaseDraggingActivity.java
+++ b/src/com/android/launcher3/BaseDraggingActivity.java
@@ -57,7 +57,7 @@
     private ActionMode mCurrentActionMode;
     protected boolean mIsSafeModeEnabled;
 
-    private OnStartCallback mOnStartCallback;
+    private Runnable mOnStartCallback;
 
     private int mThemeRes = R.style.AppTheme;
 
@@ -226,7 +226,7 @@
         super.onStart();
 
         if (mOnStartCallback != null) {
-            mOnStartCallback.onActivityStart(this);
+            mOnStartCallback.run();
             mOnStartCallback = null;
         }
     }
@@ -238,8 +238,12 @@
         mRotationListener.disable();
     }
 
-    public <T extends BaseDraggingActivity> void setOnStartCallback(OnStartCallback<T> callback) {
-        mOnStartCallback = callback;
+    public void runOnceOnStart(Runnable action) {
+        mOnStartCallback = action;
+    }
+
+    public void clearRunOnceOnStartCallback() {
+        mOnStartCallback = null;
     }
 
     protected void onDeviceProfileInitiated() {
@@ -258,12 +262,4 @@
     }
 
     protected abstract void reapplyUi();
-
-    /**
-     * Callback for listening for onStart
-     */
-    public interface OnStartCallback<T extends BaseDraggingActivity> {
-
-        void onActivityStart(T activity);
-    }
 }
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index 7adb6a4..01e9b6e 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -16,6 +16,8 @@
 
 package com.android.launcher3;
 
+import static com.android.launcher3.FastBitmapDrawable.newIcon;
+import static com.android.launcher3.graphics.PreloadIconDrawable.newPendingIcon;
 import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound;
 
 import android.animation.Animator;
@@ -45,7 +47,6 @@
 import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
 import com.android.launcher3.dot.DotInfo;
 import com.android.launcher3.folder.FolderIcon;
-import com.android.launcher3.graphics.DrawableFactory;
 import com.android.launcher3.graphics.IconPalette;
 import com.android.launcher3.graphics.IconShape;
 import com.android.launcher3.graphics.PreloadIconDrawable;
@@ -287,9 +288,8 @@
     }
 
     private void applyIconAndLabel(ItemInfoWithIcon info) {
-        FastBitmapDrawable iconDrawable = DrawableFactory.INSTANCE.get(getContext())
-                .newIcon(getContext(), info);
-        mDotParams.color = IconPalette.getMutedColor(info.iconColor, 0.54f);
+        FastBitmapDrawable iconDrawable = newIcon(getContext(), info);
+        mDotParams.color = IconPalette.getMutedColor(info.bitmap.color, 0.54f);
 
         setIcon(iconDrawable);
         setText(info.title);
@@ -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) {
@@ -567,8 +568,7 @@
                     preloadDrawable = (PreloadIconDrawable) mIcon;
                     preloadDrawable.setLevel(progressLevel);
                 } else {
-                    preloadDrawable = DrawableFactory.INSTANCE.get(getContext())
-                            .newPendingIcon(getContext(), info);
+                    preloadDrawable = newPendingIcon(getContext(), info);
                     preloadDrawable.setLevel(progressLevel);
                     setIcon(preloadDrawable);
                 }
@@ -665,7 +665,7 @@
             mDisableRelayout = true;
 
             // Optimization: Starting in N, pre-uploads the bitmap to RenderThread.
-            info.iconBitmap.prepareToDraw();
+            info.bitmap.icon.prepareToDraw();
 
             if (info instanceof AppInfo) {
                 applyFromApplicationInfo((AppInfo) info);
diff --git a/src/com/android/launcher3/FastBitmapDrawable.java b/src/com/android/launcher3/FastBitmapDrawable.java
index a90025e..5091684 100644
--- a/src/com/android/launcher3/FastBitmapDrawable.java
+++ b/src/com/android/launcher3/FastBitmapDrawable.java
@@ -20,6 +20,7 @@
 import static com.android.launcher3.anim.Interpolators.DEACCEL;
 
 import android.animation.ObjectAnimator;
+import android.content.Context;
 import android.graphics.Bitmap;
 import android.graphics.Canvas;
 import android.graphics.Color;
@@ -35,6 +36,7 @@
 import android.util.Property;
 import android.util.SparseArray;
 
+import com.android.launcher3.graphics.PlaceHolderIconDrawable;
 import com.android.launcher3.icons.BitmapInfo;
 
 public class FastBitmapDrawable extends Drawable {
@@ -98,10 +100,6 @@
         this(info.icon, info.color);
     }
 
-    public FastBitmapDrawable(ItemInfoWithIcon info) {
-        this(info.iconBitmap, info.iconColor);
-    }
-
     protected FastBitmapDrawable(Bitmap b, int iconColor) {
         this(b, iconColor, false);
     }
@@ -365,7 +363,7 @@
         }
 
         @Override
-        public Drawable newDrawable() {
+        public FastBitmapDrawable newDrawable() {
             return new FastBitmapDrawable(mBitmap, mIconColor, mIsDisabled);
         }
 
@@ -374,4 +372,37 @@
             return 0;
         }
     }
+
+    /**
+     * Interface to be implemented by custom {@link BitmapInfo} to handle drawable construction
+     */
+    public interface Factory {
+
+        /**
+         * Called to create a new drawable
+         */
+        FastBitmapDrawable newDrawable();
+    }
+
+    /**
+     * Returns a FastBitmapDrawable with the icon.
+     */
+    public static FastBitmapDrawable newIcon(Context context, ItemInfoWithIcon info) {
+        FastBitmapDrawable drawable = newIcon(context, info.bitmap);
+        drawable.setIsDisabled(info.isDisabled());
+        return drawable;
+    }
+
+    /**
+     * Creates a drawable for the provided BitmapInfo
+     */
+    public static FastBitmapDrawable newIcon(Context context, BitmapInfo info) {
+        if (info instanceof Factory) {
+            return ((Factory) info).newDrawable();
+        } else if (info.isLowRes()) {
+            return new PlaceHolderIconDrawable(info, context);
+        } else {
+            return new FastBitmapDrawable(info);
+        }
+    }
 }
diff --git a/src/com/android/launcher3/IconProvider.java b/src/com/android/launcher3/IconProvider.java
deleted file mode 100644
index 0f006f7..0000000
--- a/src/com/android/launcher3/IconProvider.java
+++ /dev/null
@@ -1,29 +0,0 @@
-package com.android.launcher3;
-
-import static com.android.launcher3.util.MainThreadInitializedObject.forOverride;
-
-import android.content.pm.LauncherActivityInfo;
-import android.graphics.drawable.Drawable;
-
-import com.android.launcher3.util.MainThreadInitializedObject;
-import com.android.launcher3.util.ResourceBasedOverride;
-
-public class IconProvider implements ResourceBasedOverride {
-
-    public static MainThreadInitializedObject<IconProvider> INSTANCE =
-            forOverride(IconProvider.class, R.string.icon_provider_class);
-
-    public IconProvider() { }
-
-    public String getSystemStateForPackage(String systemState, String packageName) {
-        return systemState;
-    }
-
-    /**
-     * @param flattenDrawable true if the caller does not care about the specification of the
-     *                        original icon as long as the flattened version looks the same.
-     */
-    public Drawable getIcon(LauncherActivityInfo info, int iconDpi, boolean flattenDrawable) {
-        return info.getIcon(iconDpi);
-    }
-}
diff --git a/src/com/android/launcher3/InstallShortcutReceiver.java b/src/com/android/launcher3/InstallShortcutReceiver.java
index 93def50..21359f1 100644
--- a/src/com/android/launcher3/InstallShortcutReceiver.java
+++ b/src/com/android/launcher3/InstallShortcutReceiver.java
@@ -17,10 +17,12 @@
 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;
 import android.content.BroadcastReceiver;
+import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.SharedPreferences;
@@ -39,9 +41,11 @@
 import android.util.Log;
 import android.util.Pair;
 
+import androidx.annotation.Nullable;
 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;
@@ -238,11 +242,6 @@
         return info == null ? null : (WorkspaceItemInfo) info.getItemInfo().first;
     }
 
-    public static WorkspaceItemInfo fromActivityInfo(LauncherActivityInfo info, Context context) {
-        return (WorkspaceItemInfo)
-                new PendingInstallShortcutInfo(info, context).getItemInfo().first;
-    }
-
     public static void queueShortcut(ShortcutInfo info, Context context) {
         queuePendingShortcutInfo(new PendingInstallShortcutInfo(info, context), context);
     }
@@ -318,10 +317,10 @@
     private static class PendingInstallShortcutInfo {
 
         final boolean isActivity;
-        final ShortcutInfo shortcutInfo;
-        final AppWidgetProviderInfo providerInfo;
+        @Nullable final ShortcutInfo shortcutInfo;
+        @Nullable final AppWidgetProviderInfo providerInfo;
 
-        final Intent data;
+        @Nullable final Intent data;
         final Context mContext;
         final Intent launchIntent;
         final String label;
@@ -351,7 +350,12 @@
             shortcutInfo = null;
             providerInfo = null;
 
-            data = null;
+            String packageName = info.getComponentName().getPackageName();
+            data = new Intent();
+            data.putExtra(Intent.EXTRA_SHORTCUT_INTENT, new Intent().setComponent(
+                    new ComponentName(packageName, "")).setPackage(packageName));
+            data.putExtra(Intent.EXTRA_SHORTCUT_NAME, info.getLabel());
+
             user = info.getUser();
             mContext = context;
 
@@ -445,9 +449,10 @@
                 // This name is only used for comparisons and notifications, so fall back to activity
                 // name if not supplied
                 String name = ensureValidName(mContext, launchIntent, label).toString();
-                Bitmap icon = data.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON);
-                Intent.ShortcutIconResource iconResource =
-                    data.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE);
+                Bitmap icon = data == null ? null
+                        : data.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON);
+                Intent.ShortcutIconResource iconResource = data == null ? null
+                    : data.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE);
 
                 // Only encode the parameters which are supported by the API.
                 JSONStringer json = new JSONStringer()
@@ -459,9 +464,11 @@
                     .key(APP_SHORTCUT_TYPE_KEY).value(isActivity);
                 if (icon != null) {
                     byte[] iconByteArray = GraphicsUtils.flattenBitmap(icon);
-                    json = json.key(ICON_KEY).value(
-                            Base64.encodeToString(
-                                    iconByteArray, 0, iconByteArray.length, Base64.DEFAULT));
+                    if (iconByteArray != null) {
+                        json = json.key(ICON_KEY).value(
+                                Base64.encodeToString(
+                                        iconByteArray, 0, iconByteArray.length, Base64.DEFAULT));
+                    }
                 }
                 if (iconResource != null) {
                     json = json.key(ICON_RESOURCE_NAME_KEY).value(iconResource.resourceName);
@@ -484,9 +491,13 @@
                 return Pair.create(si, null);
             } else if (shortcutInfo != null) {
                 WorkspaceItemInfo itemInfo = new WorkspaceItemInfo(shortcutInfo, mContext);
-                LauncherIcons li = LauncherIcons.obtain(mContext);
-                itemInfo.applyFrom(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
@@ -656,7 +667,7 @@
         if (iconInfo == null) {
             iconInfo = app.getIconCache().getDefaultIcon(info.user);
         }
-        info.applyFrom(iconInfo);
+        info.bitmap = iconInfo;
 
         info.title = Utilities.trim(name);
         info.contentDescription = app.getContext().getPackageManager()
diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java
index d66ba73..9d87152 100644
--- a/src/com/android/launcher3/InvariantDeviceProfile.java
+++ b/src/com/android/launcher3/InvariantDeviceProfile.java
@@ -104,8 +104,6 @@
     public int iconBitmapSize;
     public int fillResIconDpi;
     public float iconTextSize;
-    public float allAppsIconSize;
-    public float allAppsIconTextSize;
 
     private SparseArray<TypedValue> mExtraAttrs;
 
@@ -146,8 +144,6 @@
         iconTextSize = p.iconTextSize;
         numHotseatIcons = p.numHotseatIcons;
         numAllAppsColumns = p.numAllAppsColumns;
-        allAppsIconSize = p.allAppsIconSize;
-        allAppsIconTextSize = p.allAppsIconTextSize;
         defaultLayoutId = p.defaultLayoutId;
         demoModeLayoutId = p.demoModeLayoutId;
         mExtraAttrs = p.mExtraAttrs;
diff --git a/src/com/android/launcher3/ItemInfoWithIcon.java b/src/com/android/launcher3/ItemInfoWithIcon.java
index 1550bb0..1941455 100644
--- a/src/com/android/launcher3/ItemInfoWithIcon.java
+++ b/src/com/android/launcher3/ItemInfoWithIcon.java
@@ -16,10 +16,6 @@
 
 package com.android.launcher3;
 
-import static com.android.launcher3.icons.BitmapInfo.LOW_RES_ICON;
-
-import android.graphics.Bitmap;
-
 import com.android.launcher3.icons.BitmapInfo;
 
 /**
@@ -30,14 +26,9 @@
     public static final String TAG = "ItemInfoDebug";
 
     /**
-     * A bitmap version of the application icon.
+     * The bitmap for the application icon
      */
-    public Bitmap iconBitmap;
-
-    /**
-     * Dominant color in the {@link #iconBitmap}.
-     */
-    public int iconColor;
+    public BitmapInfo bitmap = BitmapInfo.LOW_RES_INFO;
 
     /**
      * Indicates that the icon is disabled due to safe mode restrictions.
@@ -106,8 +97,7 @@
 
     protected ItemInfoWithIcon(ItemInfoWithIcon info) {
         super(info);
-        iconBitmap = info.iconBitmap;
-        iconColor = info.iconColor;
+        bitmap = info.bitmap;
         runtimeStatusFlags = info.runtimeStatusFlags;
     }
 
@@ -120,12 +110,7 @@
      * Indicates whether we're using a low res icon
      */
     public boolean usingLowResIcon() {
-        return iconBitmap == LOW_RES_ICON;
-    }
-
-    public void applyFrom(BitmapInfo info) {
-        iconBitmap = info.icon;
-        iconColor = info.color;
+        return bitmap.isLowRes();
     }
 
     /**
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 3dce9fc..8714032 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -36,7 +36,6 @@
 import static com.android.launcher3.popup.SystemShortcut.INSTALL;
 import static com.android.launcher3.popup.SystemShortcut.WIDGETS;
 import static com.android.launcher3.states.RotationHelper.REQUEST_NONE;
-import static com.android.launcher3.testing.TestProtocol.CRASH_ADD_CUSTOM_SHORTCUT;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -121,7 +120,6 @@
 import com.android.launcher3.popup.SystemShortcut;
 import com.android.launcher3.qsb.QsbContainerView;
 import com.android.launcher3.states.RotationHelper;
-import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.touch.AllAppsSwipeController;
 import com.android.launcher3.touch.ItemClickHandler;
 import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
@@ -1210,13 +1208,8 @@
      */
     private void completeAddShortcut(Intent data, int container, int screenId, int cellX,
             int cellY, PendingRequestArgs args) {
-        if (data == null
-                || args.getRequestCode() != REQUEST_CREATE_SHORTCUT
+        if (args.getRequestCode() != REQUEST_CREATE_SHORTCUT
                 || args.getPendingIntent().getComponent() == null) {
-            if (data == null && TestProtocol.sDebugTracing) {
-                Log.d(CRASH_ADD_CUSTOM_SHORTCUT,
-                        "Failed to add custom shortcut: Intent is null, args = " + args);
-            }
             return;
         }
 
diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java
index c717d1a..79f4821 100644
--- a/src/com/android/launcher3/LauncherAppState.java
+++ b/src/com/android/launcher3/LauncherAppState.java
@@ -30,12 +30,14 @@
 import com.android.launcher3.compat.UserManagerCompat;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.icons.IconProvider;
 import com.android.launcher3.icons.LauncherIcons;
 import com.android.launcher3.notification.NotificationListener;
 import com.android.launcher3.pm.InstallSessionTracker;
 import com.android.launcher3.pm.PackageInstallerCompat;
 import com.android.launcher3.util.MainThreadInitializedObject;
 import com.android.launcher3.util.Preconditions;
+import com.android.launcher3.util.SafeCloseable;
 import com.android.launcher3.util.SecureSettingsObserver;
 import com.android.launcher3.util.SimpleBroadcastReceiver;
 import com.android.launcher3.widget.custom.CustomWidgetManager;
@@ -57,6 +59,7 @@
 
     private final InstallSessionTracker mInstallSessionTracker;
     private final SimpleBroadcastReceiver mModelChangeReceiver;
+    private final SafeCloseable mCalendarChangeTracker;
 
     public static LauncherAppState getInstance(final Context context) {
         return INSTANCE.get(context);
@@ -92,6 +95,10 @@
         if (FeatureFlags.IS_DOGFOOD_BUILD) {
             mModelChangeReceiver.register(mContext, ACTION_FORCE_ROLOAD);
         }
+
+        mCalendarChangeTracker = IconProvider.registerIconChangeListener(mContext,
+                mModel::onAppIconChanged, MODEL_EXECUTOR.getHandler());
+
         // TODO: remove listener on terminate
         FeatureFlags.APP_SEARCH_IMPROVEMENTS.addChangeListener(context, mModel::forceReload);
         CustomWidgetManager.INSTANCE.get(mContext)
@@ -143,6 +150,7 @@
         mContext.unregisterReceiver(mModelChangeReceiver);
         mContext.getSystemService(LauncherApps.class).unregisterCallback(mModel);
         mInstallSessionTracker.unregister();
+        mCalendarChangeTracker.close();
         CustomWidgetManager.INSTANCE.get(mContext).setWidgetRefreshCallback(null);
 
         if (mNotificationDotsObserver != null) {
diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java
index b0b213c..1e25c0c 100644
--- a/src/com/android/launcher3/LauncherModel.java
+++ b/src/com/android/launcher3/LauncherModel.java
@@ -21,6 +21,7 @@
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
 
+import android.content.Context;
 import android.content.Intent;
 import android.content.pm.LauncherApps;
 import android.content.pm.PackageInstaller;
@@ -31,6 +32,7 @@
 import android.util.Pair;
 
 import androidx.annotation.Nullable;
+import androidx.annotation.WorkerThread;
 
 import com.android.launcher3.compat.UserManagerCompat;
 import com.android.launcher3.config.FeatureFlags;
@@ -53,7 +55,6 @@
 import com.android.launcher3.pm.InstallSessionTracker;
 import com.android.launcher3.pm.PackageInstallInfo;
 import com.android.launcher3.shortcuts.DeepShortcutManager;
-import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.util.IntSparseArrayMap;
 import com.android.launcher3.util.ItemInfoMatcher;
 import com.android.launcher3.util.PackageUserKey;
@@ -210,9 +211,21 @@
         enqueueModelUpdateTask(new ShortcutsChangedTask(packageName, shortcuts, user, true));
     }
 
-    public void updatePinnedShortcuts(String packageName, List<ShortcutInfo> shortcuts,
-            UserHandle user) {
-        enqueueModelUpdateTask(new ShortcutsChangedTask(packageName, shortcuts, user, false));
+    /**
+     * Called when the icon for an app changes, outside of package event
+     */
+    @WorkerThread
+    public void onAppIconChanged(String packageName, UserHandle user) {
+        // Update the icon for the calendar package
+        Context context = mApp.getContext();
+        onPackageChanged(packageName, user);
+
+        List<ShortcutInfo> pinnedShortcuts = DeepShortcutManager.getInstance(context)
+                .queryForPinnedShortcuts(packageName, user);
+        if (!pinnedShortcuts.isEmpty()) {
+            enqueueModelUpdateTask(new ShortcutsChangedTask(packageName, pinnedShortcuts, user,
+                    false));
+        }
     }
 
     public void onBroadcastIntent(Intent intent) {
@@ -510,7 +523,7 @@
         updateAndBindWorkspaceItem(() -> {
             si.updateFromDeepShortcutInfo(info, mApp.getContext());
             LauncherIcons li = LauncherIcons.obtain(mApp.getContext());
-            si.applyFrom(li.createShortcutIcon(info));
+            si.bitmap = li.createShortcutIcon(info);
             li.recycle();
             return si;
         });
@@ -546,7 +559,8 @@
         if (args.length > 0 && TextUtils.equals(args[0], "--all")) {
             writer.println(prefix + "All apps list: size=" + mBgAllAppsList.data.size());
             for (AppInfo info : mBgAllAppsList.data) {
-                writer.println(prefix + "   title=\"" + info.title + "\" iconBitmap=" + info.iconBitmap
+                writer.println(prefix + "   title=\"" + info.title
+                        + "\" bitmapIcon=" + info.bitmap.icon
                         + " componentName=" + info.componentName.getPackageName());
             }
         }
diff --git a/src/com/android/launcher3/LauncherSettings.java b/src/com/android/launcher3/LauncherSettings.java
index c509680..ec307db 100644
--- a/src/com/android/launcher3/LauncherSettings.java
+++ b/src/com/android/launcher3/LauncherSettings.java
@@ -127,6 +127,7 @@
         public static final int CONTAINER_DESKTOP = -100;
         public static final int CONTAINER_HOTSEAT = -101;
         public static final int CONTAINER_PREDICTION = -102;
+        public static final int CONTAINER_HOTSEAT_PREDICTION = -103;
 
         static final String containerToString(int container) {
             switch (container) {
diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java
index 92f8069..2032845 100644
--- a/src/com/android/launcher3/Utilities.java
+++ b/src/com/android/launcher3/Utilities.java
@@ -63,6 +63,7 @@
 import com.android.launcher3.dragndrop.FolderAdaptiveIcon;
 import com.android.launcher3.graphics.RotationMode;
 import com.android.launcher3.graphics.TintedDrawableSpan;
+import com.android.launcher3.icons.IconProvider;
 import com.android.launcher3.icons.LauncherIcons;
 import com.android.launcher3.pm.ShortcutConfigActivityInfo;
 import com.android.launcher3.shortcuts.DeepShortcutManager;
@@ -518,19 +519,20 @@
     }
 
     /**
-     * Returns the full drawable for {@param info}.
+     * Returns the full drawable for info without any flattening or pre-processing.
+     *
      * @param outObj this is set to the internal data associated with {@param info},
      *               eg {@link LauncherActivityInfo} or {@link ShortcutInfo}.
      */
     public static Drawable getFullDrawable(Launcher launcher, ItemInfo info, int width, int height,
-            boolean flattenDrawable, Object[] outObj) {
+            Object[] outObj) {
         LauncherAppState appState = LauncherAppState.getInstance(launcher);
         if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION) {
             LauncherActivityInfo activityInfo = launcher.getSystemService(LauncherApps.class)
                     .resolveActivity(info.getIntent(), info.user);
             outObj[0] = activityInfo;
-            return (activityInfo != null) ? appState.getIconCache()
-                    .getFullResIcon(activityInfo, flattenDrawable) : null;
+            return activityInfo == null ? null : new IconProvider(launcher).getIconForUI(
+                    activityInfo, launcher.getDeviceProfile().inv.fillResIconDpi);
         } else if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
             if (info instanceof PendingAddShortcutInfo) {
                 ShortcutConfigActivityInfo activityInfo =
@@ -582,7 +584,7 @@
             }
             ShortcutInfo si = (ShortcutInfo) obj;
             LauncherIcons li = LauncherIcons.obtain(appState.getContext());
-            Bitmap badge = li.getShortcutInfoBadge(si, appState.getIconCache()).iconBitmap;
+            Bitmap badge = li.getShortcutInfoBadge(si, appState.getIconCache()).bitmap.icon;
             li.recycle();
             float badgeSize = iconSize * LauncherIcons.getBadgeSizeForIconSize(iconSize);
             float insetFraction = (iconSize - badgeSize) / iconSize;
diff --git a/src/com/android/launcher3/WidgetPreviewLoader.java b/src/com/android/launcher3/WidgetPreviewLoader.java
index c5e74ef..37b58d3 100644
--- a/src/com/android/launcher3/WidgetPreviewLoader.java
+++ b/src/com/android/launcher3/WidgetPreviewLoader.java
@@ -23,16 +23,19 @@
 import android.graphics.PorterDuffXfermode;
 import android.graphics.Rect;
 import android.graphics.RectF;
+import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.Drawable;
 import android.os.AsyncTask;
 import android.os.CancellationSignal;
 import android.os.Process;
 import android.os.UserHandle;
+import android.util.ArrayMap;
 import android.util.Log;
 import android.util.LongSparseArray;
 import android.util.Pair;
 
 import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
 
 import com.android.launcher3.compat.AppWidgetManagerCompat;
 import com.android.launcher3.compat.UserManagerCompat;
@@ -78,6 +81,9 @@
     private final UserManagerCompat mUserManager;
     private final CacheDb mDb;
 
+    private final UserHandle mMyUser = Process.myUserHandle();
+    private final ArrayMap<UserHandle, Bitmap> mUserBadges = new ArrayMap<>();
+
     public WidgetPreviewLoader(Context context, IconCache iconCache) {
         mContext = context;
         mIconCache = iconCache;
@@ -86,6 +92,51 @@
     }
 
     /**
+     * Returns a drawable that can be used as a badge for the user or null.
+     */
+    @UiThread
+    public Drawable getBadgeForUser(UserHandle user, int badgeSize) {
+        if (mMyUser.equals(user)) {
+            return null;
+        }
+
+        Bitmap badgeBitmap = getUserBadge(user, badgeSize);
+        FastBitmapDrawable d = new FastBitmapDrawable(badgeBitmap);
+        d.setFilterBitmap(true);
+        d.setBounds(0, 0, badgeBitmap.getWidth(), badgeBitmap.getHeight());
+        return d;
+    }
+
+    private Bitmap getUserBadge(UserHandle user, int badgeSize) {
+        synchronized (mUserBadges) {
+            Bitmap badgeBitmap = mUserBadges.get(user);
+            if (badgeBitmap != null) {
+                return badgeBitmap;
+            }
+
+            final Resources res = mContext.getResources();
+            badgeBitmap = Bitmap.createBitmap(badgeSize, badgeSize, Bitmap.Config.ARGB_8888);
+
+            Drawable drawable = mContext.getPackageManager().getUserBadgedDrawableForDensity(
+                    new BitmapDrawable(res, badgeBitmap), user,
+                    new Rect(0, 0, badgeSize, badgeSize),
+                    0);
+            if (drawable instanceof BitmapDrawable) {
+                badgeBitmap = ((BitmapDrawable) drawable).getBitmap();
+            } else {
+                badgeBitmap.eraseColor(Color.TRANSPARENT);
+                Canvas c = new Canvas(badgeBitmap);
+                drawable.setBounds(0, 0, badgeSize, badgeSize);
+                drawable.draw(c);
+                c.setBitmap(null);
+            }
+
+            mUserBadges.put(user, badgeBitmap);
+            return badgeBitmap;
+        }
+    }
+
+    /**
      * Generates the widget preview on {@link AsyncTask#THREAD_POOL_EXECUTOR}. Must be
      * called on UI thread
      *
@@ -106,8 +157,8 @@
 
     public void refresh() {
         mDb.clear();
-
     }
+
     /**
      * The DB holds the generated previews for various components. Previews can also have different
      * sizes (landscape vs portrait).
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index eca5d12..431a149 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -1471,9 +1471,6 @@
 
     public DragView beginDragShared(View child, DragSource source, ItemInfo dragObject,
             DragPreviewProvider previewProvider, DragOptions dragOptions) {
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.NO_CONTEXT_MENU, "beginDragShared");
-        }
         float iconScale = 1f;
         if (child instanceof BubbleTextView) {
             Drawable icon = ((BubbleTextView) child).getIcon();
diff --git a/src/com/android/launcher3/WorkspaceItemInfo.java b/src/com/android/launcher3/WorkspaceItemInfo.java
index 71bd92f..be907e5 100644
--- a/src/com/android/launcher3/WorkspaceItemInfo.java
+++ b/src/com/android/launcher3/WorkspaceItemInfo.java
@@ -140,7 +140,7 @@
                 .put(Favorites.RESTORED, status);
 
         if (!usingLowResIcon()) {
-            writer.putIcon(iconBitmap, user);
+            writer.putIcon(bitmap, user);
         }
         if (iconResource != null) {
             writer.put(Favorites.ICON_PACKAGE, iconResource.packageName)
diff --git a/src/com/android/launcher3/WorkspaceLayoutManager.java b/src/com/android/launcher3/WorkspaceLayoutManager.java
index ea2d4d0..0b9d602 100644
--- a/src/com/android/launcher3/WorkspaceLayoutManager.java
+++ b/src/com/android/launcher3/WorkspaceLayoutManager.java
@@ -39,7 +39,8 @@
     default void addInScreenFromBind(View child, ItemInfo info) {
         int x = info.cellX;
         int y = info.cellY;
-        if (info.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
+        if (info.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT
+                || info.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION) {
             int screenId = info.screenId;
             x = getHotseat().getCellXFromOrder(screenId);
             y = getHotseat().getCellYFromOrder(screenId);
@@ -83,7 +84,8 @@
         }
 
         final CellLayout layout;
-        if (container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
+        if (container == LauncherSettings.Favorites.CONTAINER_HOTSEAT
+                || container == LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION) {
             layout = getHotseat();
 
             // Hide folder title in the hotseat
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index 4abdbef..d622037 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -120,6 +120,16 @@
     public static final TogglableFlag ENABLE_QUICK_CAPTURE_GESTURE = new TogglableFlag(
             "ENABLE_QUICK_CAPTURE_GESTURE", false, "Swipe from right to left to quick capture");
 
+    public static final TogglableFlag ASSISTANT_GIVES_LAUNCHER_FOCUS = new TogglableFlag(
+            "ASSISTANT_GIVES_LAUNCHER_FOCUS", false,
+            "Allow Launcher to handle nav bar gestures while Assistant is running over it");
+
+    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/dragndrop/DragView.java b/src/com/android/launcher3/dragndrop/DragView.java
index f66d07e..145885a 100644
--- a/src/com/android/launcher3/dragndrop/DragView.java
+++ b/src/com/android/launcher3/dragndrop/DragView.java
@@ -216,8 +216,7 @@
                 Object[] outObj = new Object[1];
                 int w = mBitmap.getWidth();
                 int h = mBitmap.getHeight();
-                Drawable dr = Utilities.getFullDrawable(mLauncher, info, w, h,
-                        false /* flattenDrawable */, outObj);
+                Drawable dr = Utilities.getFullDrawable(mLauncher, info, w, h, outObj);
 
                 if (dr instanceof AdaptiveIconDrawable) {
                     int blurMargin = (int) mLauncher.getResources()
diff --git a/src/com/android/launcher3/folder/PreviewItemManager.java b/src/com/android/launcher3/folder/PreviewItemManager.java
index 2d817e6..5b3a05e 100644
--- a/src/com/android/launcher3/folder/PreviewItemManager.java
+++ b/src/com/android/launcher3/folder/PreviewItemManager.java
@@ -16,10 +16,12 @@
 
 package com.android.launcher3.folder;
 
+import static com.android.launcher3.FastBitmapDrawable.newIcon;
 import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.ENTER_INDEX;
 import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.EXIT_INDEX;
 import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW;
 import static com.android.launcher3.folder.FolderIcon.DROP_IN_ANIMATION_DURATION;
+import static com.android.launcher3.graphics.PreloadIconDrawable.newPendingIcon;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -38,7 +40,6 @@
 import com.android.launcher3.Launcher;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.WorkspaceItemInfo;
-import com.android.launcher3.graphics.DrawableFactory;
 import com.android.launcher3.graphics.PreloadIconDrawable;
 
 import java.util.ArrayList;
@@ -66,7 +67,6 @@
 
     private final Context mContext;
     private final FolderIcon mIcon;
-    private final DrawableFactory mDrawableFactory;
     private final int mIconSize;
 
     // These variables are all associated with the drawing of the preview; they are stored
@@ -94,7 +94,6 @@
     public PreviewItemManager(FolderIcon icon) {
         mContext = icon.getContext();
         mIcon = icon;
-        mDrawableFactory = DrawableFactory.INSTANCE.get(mContext);
         mIconSize = Launcher.getLauncher(mContext).getDeviceProfile().folderChildIconSizePx;
     }
 
@@ -395,11 +394,11 @@
 
     private void setDrawable(PreviewItemDrawingParams p, WorkspaceItemInfo item) {
         if (item.hasPromiseIconUi()) {
-            PreloadIconDrawable drawable = mDrawableFactory.newPendingIcon(mContext, item);
+            PreloadIconDrawable drawable = newPendingIcon(mContext, item);
             drawable.setLevel(item.getInstallProgress());
             p.drawable = drawable;
         } else {
-            p.drawable = mDrawableFactory.newIcon(mContext, item);
+            p.drawable = newIcon(mContext, item);
         }
         p.drawable.setBounds(0, 0, mIconSize, mIconSize);
         p.item = item;
diff --git a/src/com/android/launcher3/graphics/DrawableFactory.java b/src/com/android/launcher3/graphics/DrawableFactory.java
deleted file mode 100644
index 837301f..0000000
--- a/src/com/android/launcher3/graphics/DrawableFactory.java
+++ /dev/null
@@ -1,120 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.graphics;
-
-import static com.android.launcher3.graphics.IconShape.getShapePath;
-import static com.android.launcher3.util.MainThreadInitializedObject.forOverride;
-
-import android.content.Context;
-import android.content.pm.ActivityInfo;
-import android.content.res.Resources;
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Rect;
-import android.graphics.drawable.BitmapDrawable;
-import android.graphics.drawable.Drawable;
-import android.os.Process;
-import android.os.UserHandle;
-import android.util.ArrayMap;
-
-import androidx.annotation.UiThread;
-
-import com.android.launcher3.FastBitmapDrawable;
-import com.android.launcher3.ItemInfoWithIcon;
-import com.android.launcher3.R;
-import com.android.launcher3.icons.BitmapInfo;
-import com.android.launcher3.util.MainThreadInitializedObject;
-import com.android.launcher3.util.ResourceBasedOverride;
-
-/**
- * Factory for creating new drawables.
- */
-public class DrawableFactory implements ResourceBasedOverride {
-
-    public static final MainThreadInitializedObject<DrawableFactory> INSTANCE =
-            forOverride(DrawableFactory.class, R.string.drawable_factory_class);
-
-    protected final UserHandle mMyUser = Process.myUserHandle();
-    protected final ArrayMap<UserHandle, Bitmap> mUserBadges = new ArrayMap<>();
-
-    /**
-     * Returns a FastBitmapDrawable with the icon.
-     */
-    public FastBitmapDrawable newIcon(Context context, ItemInfoWithIcon info) {
-        FastBitmapDrawable drawable = info.usingLowResIcon()
-                ? new PlaceHolderIconDrawable(info, getShapePath(), context)
-                : new FastBitmapDrawable(info);
-        drawable.setIsDisabled(info.isDisabled());
-        return drawable;
-    }
-
-    public FastBitmapDrawable newIcon(Context context, BitmapInfo info, ActivityInfo target) {
-        return info.isLowRes()
-                ? new PlaceHolderIconDrawable(info, getShapePath(), context)
-                : new FastBitmapDrawable(info);
-    }
-
-    /**
-     * Returns a FastBitmapDrawable with the icon.
-     */
-    public PreloadIconDrawable newPendingIcon(Context context, ItemInfoWithIcon info) {
-        return new PreloadIconDrawable(info, getShapePath(), context);
-    }
-
-    /**
-     * Returns a drawable that can be used as a badge for the user or null.
-     */
-    @UiThread
-    public Drawable getBadgeForUser(UserHandle user, Context context, int badgeSize) {
-        if (mMyUser.equals(user)) {
-            return null;
-        }
-
-        Bitmap badgeBitmap = getUserBadge(user, context, badgeSize);
-        FastBitmapDrawable d = new FastBitmapDrawable(badgeBitmap);
-        d.setFilterBitmap(true);
-        d.setBounds(0, 0, badgeBitmap.getWidth(), badgeBitmap.getHeight());
-        return d;
-    }
-
-    protected synchronized Bitmap getUserBadge(UserHandle user, Context context, int badgeSize) {
-        Bitmap badgeBitmap = mUserBadges.get(user);
-        if (badgeBitmap != null) {
-            return badgeBitmap;
-        }
-
-        final Resources res = context.getApplicationContext().getResources();
-        badgeBitmap = Bitmap.createBitmap(badgeSize, badgeSize, Bitmap.Config.ARGB_8888);
-
-        Drawable drawable = context.getPackageManager().getUserBadgedDrawableForDensity(
-                new BitmapDrawable(res, badgeBitmap), user, new Rect(0, 0, badgeSize, badgeSize),
-                0);
-        if (drawable instanceof BitmapDrawable) {
-            badgeBitmap = ((BitmapDrawable) drawable).getBitmap();
-        } else {
-            badgeBitmap.eraseColor(Color.TRANSPARENT);
-            Canvas c = new Canvas(badgeBitmap);
-            drawable.setBounds(0, 0, badgeSize, badgeSize);
-            drawable.draw(c);
-            c.setBitmap(null);
-        }
-
-        mUserBadges.put(user, badgeBitmap);
-        return badgeBitmap;
-    }
-}
diff --git a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
index d7b845b..2badb6e 100644
--- a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
+++ b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
@@ -50,8 +50,8 @@
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.R;
-import com.android.launcher3.WorkspaceItemInfo;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.WorkspaceItemInfo;
 import com.android.launcher3.WorkspaceLayoutManager;
 import com.android.launcher3.allapps.SearchUiManager;
 import com.android.launcher3.config.FeatureFlags;
@@ -105,7 +105,7 @@
                 Build.VERSION.SDK_INT);
 
         mWorkspaceItemInfo = new WorkspaceItemInfo();
-        mWorkspaceItemInfo.applyFrom(iconInfo);
+        mWorkspaceItemInfo.bitmap = iconInfo;
         mWorkspaceItemInfo.intent = new Intent();
         mWorkspaceItemInfo.contentDescription = mWorkspaceItemInfo.title =
                 context.getString(R.string.label_application);
diff --git a/src/com/android/launcher3/graphics/PlaceHolderIconDrawable.java b/src/com/android/launcher3/graphics/PlaceHolderIconDrawable.java
index 23745cb..d347e8f 100644
--- a/src/com/android/launcher3/graphics/PlaceHolderIconDrawable.java
+++ b/src/com/android/launcher3/graphics/PlaceHolderIconDrawable.java
@@ -17,14 +17,14 @@
 
 import static androidx.core.graphics.ColorUtils.compositeColors;
 
+import static com.android.launcher3.graphics.IconShape.getShapePath;
+
 import android.content.Context;
-import android.graphics.Bitmap;
 import android.graphics.Canvas;
 import android.graphics.Path;
 import android.graphics.Rect;
 
 import com.android.launcher3.FastBitmapDrawable;
-import com.android.launcher3.ItemInfoWithIcon;
 import com.android.launcher3.R;
 import com.android.launcher3.icons.BitmapInfo;
 import com.android.launcher3.util.Themes;
@@ -37,20 +37,12 @@
     // Path in [0, 100] bounds.
     private final Path mProgressPath;
 
-    public PlaceHolderIconDrawable(BitmapInfo info, Path progressPath, Context context) {
-        this(info.icon, info.color, progressPath, context);
-    }
+    public PlaceHolderIconDrawable(BitmapInfo info, Context context) {
+        super(info);
 
-    public PlaceHolderIconDrawable(ItemInfoWithIcon info, Path progressPath, Context context) {
-        this(info.iconBitmap, info.iconColor, progressPath, context);
-    }
-
-    protected PlaceHolderIconDrawable(Bitmap b, int iconColor, Path progressPath, Context context) {
-        super(b, iconColor);
-
-        mProgressPath = progressPath;
+        mProgressPath = getShapePath();
         mPaint.setColor(compositeColors(
-                Themes.getAttrColor(context, R.attr.loadingIconColor), iconColor));
+                Themes.getAttrColor(context, R.attr.loadingIconColor), info.color));
     }
 
     @Override
diff --git a/src/com/android/launcher3/graphics/PreloadIconDrawable.java b/src/com/android/launcher3/graphics/PreloadIconDrawable.java
index cc4c2ef..b0e1db1 100644
--- a/src/com/android/launcher3/graphics/PreloadIconDrawable.java
+++ b/src/com/android/launcher3/graphics/PreloadIconDrawable.java
@@ -18,6 +18,7 @@
 package com.android.launcher3.graphics;
 
 import static com.android.launcher3.graphics.IconShape.DEFAULT_PATH_SIZE;
+import static com.android.launcher3.graphics.IconShape.getShapePath;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -101,13 +102,10 @@
 
     private ObjectAnimator mCurrentAnim;
 
-    /**
-     * @param progressPath fixed path in the bounds [0, 0, 100, 100] representing a progress bar.
-     */
-    public PreloadIconDrawable(ItemInfoWithIcon info, Path progressPath, Context context) {
-        super(info);
+    public PreloadIconDrawable(ItemInfoWithIcon info, Context context) {
+        super(info.bitmap);
         mItem = info;
-        mProgressPath = progressPath;
+        mProgressPath = getShapePath();
         mScaledTrackPath = new Path();
         mScaledProgressPath = new Path();
 
@@ -289,4 +287,11 @@
         }
         invalidateSelf();
     }
+
+    /**
+     * Returns a FastBitmapDrawable with the icon.
+     */
+    public static PreloadIconDrawable newPendingIcon(Context context, ItemInfoWithIcon info) {
+        return new PreloadIconDrawable(info, context);
+    }
 }
diff --git a/src/com/android/launcher3/icons/ClockDrawableWrapper.java b/src/com/android/launcher3/icons/ClockDrawableWrapper.java
new file mode 100644
index 0000000..b7dd092
--- /dev/null
+++ b/src/com/android/launcher3/icons/ClockDrawableWrapper.java
@@ -0,0 +1,328 @@
+/*
+ * 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.annotation.TargetApi;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.AdaptiveIconDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.LayerDrawable;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Process;
+import android.os.SystemClock;
+import android.util.Log;
+
+import com.android.launcher3.FastBitmapDrawable;
+
+import java.util.Calendar;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Wrapper over {@link AdaptiveIconDrawable} to intercept icon flattening logic for dynamic
+ * clock icons
+ */
+@TargetApi(Build.VERSION_CODES.O)
+public class ClockDrawableWrapper extends AdaptiveIconDrawable implements BitmapInfo.Extender {
+
+    private static final String TAG = "ClockDrawableWrapper";
+
+    private static final boolean DISABLE_SECONDS = true;
+
+    // Time after which the clock icon should check for an update. The actual invalidate
+    // will only happen in case of any change.
+    public static final long TICK_MS = DISABLE_SECONDS ? TimeUnit.MINUTES.toMillis(1) : 200L;
+
+    private static final String LAUNCHER_PACKAGE = "com.android.launcher3";
+    private static final String ROUND_ICON_METADATA_KEY = LAUNCHER_PACKAGE
+            + ".LEVEL_PER_TICK_ICON_ROUND";
+    private static final String HOUR_INDEX_METADATA_KEY = LAUNCHER_PACKAGE + ".HOUR_LAYER_INDEX";
+    private static final String MINUTE_INDEX_METADATA_KEY = LAUNCHER_PACKAGE
+            + ".MINUTE_LAYER_INDEX";
+    private static final String SECOND_INDEX_METADATA_KEY = LAUNCHER_PACKAGE
+            + ".SECOND_LAYER_INDEX";
+    private static final String DEFAULT_HOUR_METADATA_KEY = LAUNCHER_PACKAGE
+            + ".DEFAULT_HOUR";
+    private static final String DEFAULT_MINUTE_METADATA_KEY = LAUNCHER_PACKAGE
+            + ".DEFAULT_MINUTE";
+    private static final String DEFAULT_SECOND_METADATA_KEY = LAUNCHER_PACKAGE
+            + ".DEFAULT_SECOND";
+
+    /* Number of levels to jump per second for the second hand */
+    private static final int LEVELS_PER_SECOND = 10;
+
+    public static final int INVALID_VALUE = -1;
+
+    private final AnimationInfo mAnimationInfo = new AnimationInfo();
+    private int mTargetSdkVersion;
+
+    public ClockDrawableWrapper(AdaptiveIconDrawable base) {
+        super(base.getBackground(), base.getForeground());
+    }
+
+    /**
+     * Loads and returns the wrapper from the provided package, or returns null
+     * if it is unable to load.
+     */
+    public static ClockDrawableWrapper forPackage(Context context, String pkg, int iconDpi) {
+        try {
+            PackageManager pm = context.getPackageManager();
+            ApplicationInfo appInfo =  pm.getApplicationInfo(pkg,
+                    PackageManager.MATCH_UNINSTALLED_PACKAGES | PackageManager.GET_META_DATA);
+            final Bundle metadata = appInfo.metaData;
+            if (metadata == null) {
+                return null;
+            }
+            int drawableId = metadata.getInt(ROUND_ICON_METADATA_KEY, 0);
+            if (drawableId == 0) {
+                return null;
+            }
+
+            Drawable drawable = pm.getResourcesForApplication(appInfo).getDrawableForDensity(
+                    drawableId, iconDpi).mutate();
+            if (!(drawable instanceof AdaptiveIconDrawable)) {
+                return null;
+            }
+
+            ClockDrawableWrapper wrapper =
+                    new ClockDrawableWrapper((AdaptiveIconDrawable) drawable);
+            wrapper.mTargetSdkVersion = appInfo.targetSdkVersion;
+            AnimationInfo info = wrapper.mAnimationInfo;
+
+            info.baseDrawableState = drawable.getConstantState();
+
+            info.hourLayerIndex = metadata.getInt(HOUR_INDEX_METADATA_KEY, INVALID_VALUE);
+            info.minuteLayerIndex = metadata.getInt(MINUTE_INDEX_METADATA_KEY, INVALID_VALUE);
+            info.secondLayerIndex = metadata.getInt(SECOND_INDEX_METADATA_KEY, INVALID_VALUE);
+
+            info.defaultHour = metadata.getInt(DEFAULT_HOUR_METADATA_KEY, 0);
+            info.defaultMinute = metadata.getInt(DEFAULT_MINUTE_METADATA_KEY, 0);
+            info.defaultSecond = metadata.getInt(DEFAULT_SECOND_METADATA_KEY, 0);
+
+            LayerDrawable foreground = (LayerDrawable) wrapper.getForeground();
+            int layerCount = foreground.getNumberOfLayers();
+            if (info.hourLayerIndex < 0 || info.hourLayerIndex >= layerCount) {
+                info.hourLayerIndex = INVALID_VALUE;
+            }
+            if (info.minuteLayerIndex < 0 || info.minuteLayerIndex >= layerCount) {
+                info.minuteLayerIndex = INVALID_VALUE;
+            }
+            if (info.secondLayerIndex < 0 || info.secondLayerIndex >= layerCount) {
+                info.secondLayerIndex = INVALID_VALUE;
+            } else if (DISABLE_SECONDS) {
+                foreground.setDrawable(info.secondLayerIndex, null);
+                info.secondLayerIndex = INVALID_VALUE;
+            }
+            return wrapper;
+        } catch (Exception e) {
+            Log.d(TAG, "Unable to load clock drawable info", e);
+        }
+        return null;
+    }
+
+    @Override
+    public BitmapInfo getExtendedInfo(Bitmap bitmap, int color, BaseIconFactory iconFactory) {
+        iconFactory.disableColorExtraction();
+        float [] scale = new float[1];
+        AdaptiveIconDrawable background = new AdaptiveIconDrawable(
+                getBackground().getConstantState().newDrawable(), null);
+        BitmapInfo bitmapInfo = iconFactory.createBadgedIconBitmap(background,
+                Process.myUserHandle(), mTargetSdkVersion, false, scale);
+
+        return new ClockBitmapInfo(bitmap, color, scale[0], mAnimationInfo, bitmapInfo.icon);
+    }
+
+    @Override
+    public void prepareToDrawOnUi() {
+        mAnimationInfo.applyTime(Calendar.getInstance(), (LayerDrawable) getForeground());
+    }
+
+    private static class AnimationInfo {
+
+        public ConstantState baseDrawableState;
+
+        public int hourLayerIndex;
+        public int minuteLayerIndex;
+        public int secondLayerIndex;
+        public int defaultHour;
+        public int defaultMinute;
+        public int defaultSecond;
+
+        boolean applyTime(Calendar time, LayerDrawable foregroundDrawable) {
+            time.setTimeInMillis(System.currentTimeMillis());
+
+            // We need to rotate by the difference from the default time if one is specified.
+            int convertedHour = (time.get(Calendar.HOUR) + (12 - defaultHour)) % 12;
+            int convertedMinute = (time.get(Calendar.MINUTE) + (60 - defaultMinute)) % 60;
+            int convertedSecond = (time.get(Calendar.SECOND) + (60 - defaultSecond)) % 60;
+
+            boolean invalidate = false;
+            if (hourLayerIndex != INVALID_VALUE) {
+                final Drawable hour = foregroundDrawable.getDrawable(hourLayerIndex);
+                if (hour.setLevel(convertedHour * 60 + time.get(Calendar.MINUTE))) {
+                    invalidate = true;
+                }
+            }
+
+            if (minuteLayerIndex != INVALID_VALUE) {
+                final Drawable minute = foregroundDrawable.getDrawable(minuteLayerIndex);
+                if (minute.setLevel(time.get(Calendar.HOUR) * 60 + convertedMinute)) {
+                    invalidate = true;
+                }
+            }
+
+            if (secondLayerIndex != INVALID_VALUE) {
+                final Drawable second = foregroundDrawable.getDrawable(secondLayerIndex);
+                if (second.setLevel(convertedSecond * LEVELS_PER_SECOND)) {
+                    invalidate = true;
+                }
+            }
+
+            return invalidate;
+        }
+    }
+
+    private static class ClockBitmapInfo extends BitmapInfo implements FastBitmapDrawable.Factory {
+
+        public final float scale;
+        public final int offset;
+        public final AnimationInfo animInfo;
+        public final Bitmap mFlattenedBackground;
+
+        ClockBitmapInfo(Bitmap icon, int color, float scale, AnimationInfo animInfo,
+                Bitmap background) {
+            super(icon, color);
+            this.scale = scale;
+            this.animInfo = animInfo;
+            this.offset = (int) Math.ceil(ShadowGenerator.BLUR_FACTOR * icon.getWidth());
+            this.mFlattenedBackground = background;
+        }
+
+        @Override
+        public FastBitmapDrawable newDrawable() {
+            return new ClockIconDrawable(this);
+        }
+    }
+
+    private static class ClockIconDrawable extends FastBitmapDrawable implements Runnable {
+
+        private final Calendar mTime = Calendar.getInstance();
+
+        private final ClockBitmapInfo mInfo;
+
+        private final AdaptiveIconDrawable mFullDrawable;
+        private final LayerDrawable mForeground;
+
+        ClockIconDrawable(ClockBitmapInfo clockInfo) {
+            super(clockInfo);
+
+            mInfo = clockInfo;
+
+            mFullDrawable = (AdaptiveIconDrawable) mInfo.animInfo.baseDrawableState.newDrawable();
+            mForeground = (LayerDrawable) mFullDrawable.getForeground();
+        }
+
+        @Override
+        protected void onBoundsChange(Rect bounds) {
+            super.onBoundsChange(bounds);
+            mFullDrawable.setBounds(bounds);
+        }
+
+        @Override
+        public void drawInternal(Canvas canvas, Rect bounds) {
+            if (mInfo == null) {
+                super.drawInternal(canvas, bounds);
+                return;
+            }
+            // draw the background that is already flattened to a bitmap
+            canvas.drawBitmap(mInfo.mFlattenedBackground, null, bounds, mPaint);
+
+            // prepare and draw the foreground
+            mInfo.animInfo.applyTime(mTime, mForeground);
+
+            canvas.scale(mInfo.scale, mInfo.scale,
+                    bounds.exactCenterX() + mInfo.offset, bounds.exactCenterY() + mInfo.offset);
+            canvas.clipPath(mFullDrawable.getIconMask());
+            mForeground.draw(canvas);
+
+            reschedule();
+        }
+
+        @Override
+        protected void updateFilter() {
+            super.updateFilter();
+            mFullDrawable.setColorFilter(mPaint.getColorFilter());
+        }
+
+        @Override
+        public void run() {
+            if (mInfo.animInfo.applyTime(mTime, mForeground)) {
+                invalidateSelf();
+            } else {
+                reschedule();
+            }
+        }
+
+        @Override
+        public boolean setVisible(boolean visible, boolean restart) {
+            boolean result = super.setVisible(visible, restart);
+            if (visible) {
+                reschedule();
+            } else {
+                unscheduleSelf(this);
+            }
+            return result;
+        }
+
+        private void reschedule() {
+            if (!isVisible()) {
+                return;
+            }
+
+            unscheduleSelf(this);
+            final long upTime = SystemClock.uptimeMillis();
+            final long step = TICK_MS; /* tick every 200 ms */
+            scheduleSelf(this, upTime - ((upTime % step)) + step);
+        }
+
+        @Override
+        public ConstantState getConstantState() {
+            return new ClockConstantState(mInfo, isDisabled());
+        }
+
+        private static class ClockConstantState extends MyConstantState {
+
+            private final ClockBitmapInfo mInfo;
+
+            ClockConstantState(ClockBitmapInfo info, boolean isDisabled) {
+                super(info.icon, info.color, isDisabled);
+                mInfo = info;
+            }
+
+            @Override
+            public FastBitmapDrawable newDrawable() {
+                ClockIconDrawable drawable = new ClockIconDrawable(mInfo);
+                drawable.setIsDisabled(mIsDisabled);
+                return drawable;
+            }
+        }
+    }
+}
diff --git a/src/com/android/launcher3/icons/ComponentWithLabel.java b/src/com/android/launcher3/icons/ComponentWithLabel.java
index 832956d..f7ee5f9 100644
--- a/src/com/android/launcher3/icons/ComponentWithLabel.java
+++ b/src/com/android/launcher3/icons/ComponentWithLabel.java
@@ -57,10 +57,8 @@
         }
 
         @Override
-        public void loadIcon(Context context,
-                ComponentWithLabel object, BitmapInfo target) {
-            // Do not load icon.
-            target.icon = BitmapInfo.LOW_RES_ICON;
+        public BitmapInfo loadIcon(Context context, ComponentWithLabel object) {
+            return BitmapInfo.LOW_RES_INFO;
         }
 
         @Override
diff --git a/src/com/android/launcher3/icons/IconCache.java b/src/com/android/launcher3/icons/IconCache.java
index 9886f53..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;
@@ -37,7 +38,6 @@
 import androidx.annotation.NonNull;
 
 import com.android.launcher3.AppInfo;
-import com.android.launcher3.IconProvider;
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.ItemInfoWithIcon;
 import com.android.launcher3.LauncherFiles;
@@ -50,6 +50,8 @@
 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;
 import com.android.launcher3.util.Preconditions;
@@ -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,10 +81,11 @@
                 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);
-        mIconProvider = IconProvider.INSTANCE.get(context);
+        mIconProvider = new IconProvider(context);
     }
 
     @Override
@@ -161,7 +165,7 @@
         CacheEntry entry = cacheLocked(application.componentName,
                 application.user, () -> null, mLauncherActivityInfoCachingLogic,
                 false, application.usingLowResIcon());
-        if (entry.icon != null && !isDefaultIcon(entry.icon, application.user)) {
+        if (entry.bitmap != null && !isDefaultIcon(entry.bitmap, application.user)) {
             applyCacheEntry(entry, application);
         }
     }
@@ -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.
      */
@@ -183,7 +195,7 @@
         // null info means not installed, but if we have a component from the intent then
         // we should still look in the cache for restored app icons.
         if (info.getTargetComponent() == null) {
-            info.applyFrom(getDefaultIcon(info.user));
+            info.bitmap = getDefaultIcon(info.user);
             info.title = "";
             info.contentDescription = "";
         } else {
@@ -226,15 +238,11 @@
     protected void applyCacheEntry(CacheEntry entry, ItemInfoWithIcon info) {
         info.title = Utilities.trim(entry.title);
         info.contentDescription = entry.contentDescription;
-        info.applyFrom((entry.icon == null) ? getDefaultIcon(info.user) : entry);
+        info.bitmap = (entry.bitmap == null) ? getDefaultIcon(info.user) : entry.bitmap;
     }
 
     public Drawable getFullResIcon(LauncherActivityInfo info) {
-        return getFullResIcon(info, true);
-    }
-
-    public Drawable getFullResIcon(LauncherActivityInfo info, boolean flattenDrawable) {
-        return mIconProvider.getIcon(info, mIconDpi, flattenDrawable);
+        return mIconProvider.getIcon(info, mIconDpi);
     }
 
     public void updateSessionCache(PackageUserKey key, PackageInstaller.SessionInfo info) {
@@ -247,6 +255,15 @@
                 + ",flags_asi:" + FeatureFlags.APP_SEARCH_IMPROVEMENTS.get();
     }
 
+    @Override
+    protected boolean getEntryFromDB(ComponentKey cacheKey, CacheEntry entry, boolean lowRes) {
+        if (mIconProvider.isClockIcon(cacheKey)) {
+            // For clock icon, we always load the dynamic icon
+            return false;
+        }
+        return super.getEntryFromDB(cacheKey, entry, lowRes);
+    }
+
     public static abstract class IconLoadRequest extends HandlerRunnable {
         IconLoadRequest(Handler handler, Runnable endRunnable) {
             super(handler, endRunnable);
diff --git a/src/com/android/launcher3/icons/IconProvider.java b/src/com/android/launcher3/icons/IconProvider.java
new file mode 100644
index 0000000..26b7eae
--- /dev/null
+++ b/src/com/android/launcher3/icons/IconProvider.java
@@ -0,0 +1,251 @@
+/*
+ * 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.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ActivityInfo;
+import android.content.pm.LauncherActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Process;
+import android.os.UserHandle;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.launcher3.R;
+import com.android.launcher3.compat.UserManagerCompat;
+import com.android.launcher3.icons.BitmapInfo.Extender;
+import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.SafeCloseable;
+
+import java.util.Calendar;
+import java.util.function.BiConsumer;
+import java.util.function.BiFunction;
+
+/**
+ * Class to handle icon loading from different packages
+ */
+public class IconProvider {
+
+    private static final String TAG = "IconProvider";
+    private static final boolean DEBUG = false;
+
+    private static final String ICON_METADATA_KEY_PREFIX = ".dynamic_icons";
+
+    private static final String SYSTEM_STATE_SEPARATOR = " ";
+
+    // Default value returned if there are problems getting resources.
+    private static final int NO_ID = 0;
+
+    private static final BiFunction<LauncherActivityInfo, Integer, Drawable> LAI_LOADER =
+            LauncherActivityInfo::getIcon;
+
+    private static final BiFunction<ActivityInfo, PackageManager, Drawable> AI_LOADER =
+            ActivityInfo::loadUnbadgedIcon;
+
+
+    private final Context mContext;
+    private final ComponentName mCalendar;
+    private final ComponentName mClock;
+
+    public IconProvider(Context context) {
+        mContext = context;
+        mCalendar = parseComponentOrNull(context, R.string.calendar_component_name);
+        mClock = parseComponentOrNull(context, R.string.clock_component_name);
+    }
+
+    /**
+     * Adds any modification to the provided systemState for dynamic icons. This system state
+     * is used by caches to check for icon invalidation.
+     */
+    public String getSystemStateForPackage(String systemState, String packageName) {
+        if (mCalendar != null && mCalendar.getPackageName().equals(packageName)) {
+            return systemState + SYSTEM_STATE_SEPARATOR + getDay();
+        } else {
+            return systemState;
+        }
+    }
+
+    /**
+     * Loads the icon for the provided LauncherActivityInfo such that it can be drawn directly
+     * on the UI
+     */
+    public Drawable getIconForUI(LauncherActivityInfo info, int iconDpi) {
+        Drawable icon = getIcon(info, iconDpi);
+        if (icon instanceof BitmapInfo.Extender) {
+            ((Extender) icon).prepareToDrawOnUi();
+        }
+        return icon;
+    }
+
+    /**
+     * Loads the icon for the provided LauncherActivityInfo
+     */
+    public Drawable getIcon(LauncherActivityInfo info, int iconDpi) {
+        return getIcon(info.getApplicationInfo().packageName, info.getUser(),
+                info, iconDpi, LAI_LOADER);
+    }
+
+    /**
+     * Loads the icon for the provided activity info
+     */
+    public Drawable getIcon(ActivityInfo info, UserHandle user) {
+        return getIcon(info.applicationInfo.packageName, user, info, mContext.getPackageManager(),
+                AI_LOADER);
+    }
+
+    private <T, P> Drawable getIcon(String packageName, UserHandle user, T obj, P param,
+            BiFunction<T, P, Drawable> loader) {
+        Drawable icon = null;
+        if (mCalendar != null && mCalendar.getPackageName().equals(packageName)) {
+            icon = loadCalendarDrawable(0);
+        } else if (mClock != null
+                && mClock.getPackageName().equals(packageName)
+                && Process.myUserHandle().equals(user)) {
+            icon = loadClockDrawable(0);
+        }
+        return icon == null ? loader.apply(obj, param) : icon;
+    }
+
+    private Drawable loadCalendarDrawable(int iconDpi) {
+        PackageManager pm = mContext.getPackageManager();
+        try {
+            final Bundle metadata = pm.getActivityInfo(
+                    mCalendar,
+                    PackageManager.GET_UNINSTALLED_PACKAGES | PackageManager.GET_META_DATA)
+                    .metaData;
+            final Resources resources = pm.getResourcesForApplication(mCalendar.getPackageName());
+            final int id = getDynamicIconId(metadata, resources);
+            if (id != NO_ID) {
+                if (DEBUG) Log.d(TAG, "Got icon #" + id);
+                return resources.getDrawableForDensity(id, iconDpi, null /* theme */);
+            }
+        } catch (PackageManager.NameNotFoundException e) {
+            if (DEBUG) {
+                Log.d(TAG, "Could not get activityinfo or resources for package: "
+                        + mCalendar.getPackageName());
+            }
+        }
+        return null;
+    }
+
+    private Drawable loadClockDrawable(int iconDpi) {
+        return ClockDrawableWrapper.forPackage(mContext, mClock.getPackageName(), iconDpi);
+    }
+
+    protected boolean isClockIcon(ComponentKey key) {
+        return mClock != null && mClock.equals(key.componentName)
+                && Process.myUserHandle().equals(key.user);
+    }
+
+    /**
+     * @param metadata metadata of the default activity of Calendar
+     * @param resources from the Calendar package
+     * @return the resource id for today's Calendar icon; 0 if resources cannot be found.
+     */
+    private int getDynamicIconId(Bundle metadata, Resources resources) {
+        if (metadata == null) {
+            return NO_ID;
+        }
+        String key = mCalendar.getPackageName() + ICON_METADATA_KEY_PREFIX;
+        final int arrayId = metadata.getInt(key, NO_ID);
+        if (arrayId == NO_ID) {
+            return NO_ID;
+        }
+        try {
+            return resources.obtainTypedArray(arrayId).getResourceId(getDay(), NO_ID);
+        } catch (Resources.NotFoundException e) {
+            if (DEBUG) {
+                Log.d(TAG, "package defines '" + key + "' but corresponding array not found");
+            }
+            return NO_ID;
+        }
+    }
+
+    /**
+     * @return Today's day of the month, zero-indexed.
+     */
+    private int getDay() {
+        return Calendar.getInstance().get(Calendar.DAY_OF_MONTH) - 1;
+    }
+
+
+    /**
+     * Registers a callback to listen for calendar icon changes.
+     * The callback receives the packageName for the calendar icon
+     */
+    public static SafeCloseable registerIconChangeListener(Context context,
+            BiConsumer<String, UserHandle> callback, Handler handler) {
+        ComponentName calendar = parseComponentOrNull(context, R.string.calendar_component_name);
+        ComponentName clock = parseComponentOrNull(context, R.string.clock_component_name);
+
+        if (calendar == null && clock == null) {
+            return () -> { };
+        }
+
+        BroadcastReceiver receiver = new DateTimeChangeReceiver(callback);
+        final IntentFilter filter = new IntentFilter(Intent.ACTION_TIMEZONE_CHANGED);
+        if (calendar != null) {
+            filter.addAction(Intent.ACTION_TIME_CHANGED);
+            filter.addAction(Intent.ACTION_DATE_CHANGED);
+        }
+        context.registerReceiver(receiver, filter, null, handler);
+
+        return () -> context.unregisterReceiver(receiver);
+    }
+
+    private static class DateTimeChangeReceiver extends BroadcastReceiver {
+
+        private final BiConsumer<String, UserHandle> mCallback;
+
+        DateTimeChangeReceiver(BiConsumer<String, UserHandle> callback) {
+            mCallback = callback;
+        }
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (Intent.ACTION_TIMEZONE_CHANGED.equals(intent.getAction())) {
+                ComponentName clock = parseComponentOrNull(context, R.string.clock_component_name);
+                if (clock != null) {
+                    mCallback.accept(clock.getPackageName(), Process.myUserHandle());
+                }
+            }
+
+            ComponentName calendar =
+                    parseComponentOrNull(context, R.string.calendar_component_name);
+            if (calendar != null) {
+                for (UserHandle user : UserManagerCompat.getInstance(context).getUserProfiles()) {
+                    mCallback.accept(calendar.getPackageName(), user);
+                }
+            }
+
+        }
+    }
+
+    private static ComponentName parseComponentOrNull(Context context, int resId) {
+        String cn = context.getString(resId);
+        return TextUtils.isEmpty(cn) ? null : ComponentName.unflattenFromString(cn);
+
+    }
+}
diff --git a/src/com/android/launcher3/icons/LauncherActivityCachingLogic.java b/src/com/android/launcher3/icons/LauncherActivityCachingLogic.java
index f9a94da..93de35a 100644
--- a/src/com/android/launcher3/icons/LauncherActivityCachingLogic.java
+++ b/src/com/android/launcher3/icons/LauncherActivityCachingLogic.java
@@ -20,7 +20,6 @@
 import android.content.pm.LauncherActivityInfo;
 import android.os.UserHandle;
 
-import com.android.launcher3.IconProvider;
 import com.android.launcher3.R;
 import com.android.launcher3.icons.cache.CachingLogic;
 import com.android.launcher3.util.ResourceBasedOverride;
@@ -55,13 +54,11 @@
     }
 
     @Override
-    public void loadIcon(Context context, LauncherActivityInfo object,
-            BitmapInfo target) {
-        LauncherIcons li = LauncherIcons.obtain(context);
-        li.createBadgedIconBitmap(
-                IconProvider.INSTANCE.get(context)
-                        .getIcon(object, li.mFillResIconDpi, true /* flattenDrawable */),
-                object.getUser(), object.getApplicationInfo().targetSdkVersion).applyTo(target);
-        li.recycle();
+    public BitmapInfo loadIcon(Context context, LauncherActivityInfo object) {
+        try (LauncherIcons li = LauncherIcons.obtain(context)) {
+            return li.createBadgedIconBitmap(new IconProvider(context)
+                            .getIcon(object, li.mFillResIconDpi),
+                    object.getUser(), object.getApplicationInfo().targetSdkVersion);
+        }
     }
 }
diff --git a/src/com/android/launcher3/icons/LauncherIcons.java b/src/com/android/launcher3/icons/LauncherIcons.java
index adc92c4..4d3599e 100644
--- a/src/com/android/launcher3/icons/LauncherIcons.java
+++ b/src/com/android/launcher3/icons/LauncherIcons.java
@@ -25,13 +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.R;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.graphics.IconShape;
 import com.android.launcher3.model.PackageItemInfo;
 import com.android.launcher3.shortcuts.DeepShortcutManager;
@@ -45,8 +46,6 @@
  */
 public class LauncherIcons extends BaseIconFactory implements AutoCloseable {
 
-    private static final String EXTRA_BADGEPKG = "badge_package";
-
     private static final Object sPoolSync = new Object();
     private static LauncherIcons sPool;
     private static int sPoolId = 0;
@@ -115,7 +114,6 @@
     }
 
     // below methods should also migrate to BaseIconFactory
-
     public BitmapInfo createShortcutIcon(ShortcutInfo shortcutInfo) {
         return createShortcutIcon(shortcutInfo, true /* badged */);
     }
@@ -124,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);
@@ -137,37 +143,68 @@
             if (fallbackIconProvider != null) {
                 // Fallback icons are already badged and with appropriate shadow
                 ItemInfoWithIcon fullIcon = fallbackIconProvider.get();
-                if (fullIcon != null && fullIcon.iconBitmap != null) {
-                    BitmapInfo result = new BitmapInfo();
-                    result.icon = fullIcon.iconBitmap;
-                    result.color = fullIcon.iconColor;
-                    return result;
+                if (fullIcon != null && fullIcon.bitmap != null) {
+                    return fullIcon.bitmap;
                 }
             }
             unbadgedBitmap = cache.getDefaultIcon(Process.myUserHandle()).icon;
         }
 
-        BitmapInfo result = new BitmapInfo();
         if (!badged) {
-            result.color = Themes.getColorAccent(mContext);
-            result.icon = unbadgedBitmap;
-            return result;
+            return BitmapInfo.of(unbadgedBitmap, Themes.getColorAccent(mContext));
         }
 
         final Bitmap unbadgedfinal = unbadgedBitmap;
         final ItemInfoWithIcon badge = getShortcutInfoBadge(shortcutInfo, cache);
 
-        result.color = badge.iconColor;
-        result.icon = BitmapRenderer.createHardwareBitmap(mIconBitmapSize, mIconBitmapSize, (c) -> {
+        Bitmap icon = BitmapRenderer.createHardwareBitmap(mIconBitmapSize, mIconBitmapSize, (c) -> {
             getShadowGenerator().recreateIcon(unbadgedfinal, c);
-            badgeWithDrawable(c, new FastBitmapDrawable(badge));
+            badgeWithDrawable(c, new FastBitmapDrawable(badge.bitmap));
         });
-        return result;
+        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 = getBadgePackage(shortcutInfo);
+        String badgePkg = shortcutInfo.getPackage();
         boolean hasBadgePkgSet = !badgePkg.equals(shortcutInfo.getPackage());
         if (cn != null && !hasBadgePkgSet) {
             // Get the app info for the source activity.
@@ -185,14 +222,4 @@
             return pkgInfo;
         }
     }
-
-    private String getBadgePackage(ShortcutInfo si) {
-        String whitelistedPkg = mContext.getString(R.string.shortcutinfo_badgepkg_whitelist);
-        if (whitelistedPkg.equals(si.getPackage())
-                && si.getExtras() != null
-                && si.getExtras().containsKey(EXTRA_BADGEPKG)) {
-            return si.getExtras().getString(EXTRA_BADGEPKG);
-        }
-        return si.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 2d62c9e..fa0fe1b 100644
--- a/src/com/android/launcher3/model/AddWorkspaceItemsTask.java
+++ b/src/com/android/launcher3/model/AddWorkspaceItemsTask.java
@@ -76,6 +76,11 @@
                     if (shortcutExists(dataModel, item.getIntent(), item.user)) {
                         continue;
                     }
+
+                    // b/139663018 Short-circuit this logic if the icon is a system app
+                    if (PackageManagerHelper.isSystemApp(app.getContext(), item.getIntent())) {
+                        continue;
+                    }
                 }
 
                 if (item.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION) {
@@ -135,9 +140,18 @@
                         // 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.applyFrom(app.getIconCache().getDefaultIcon(item.user));
+                        wii.bitmap = app.getIconCache().getDefaultIcon(item.user);
                         app.getIconCache().getTitleAndIcon(wii,
                                 ((WorkspaceItemInfo) itemInfo).usingLowResIcon());
                     }
diff --git a/src/com/android/launcher3/model/LoaderCursor.java b/src/com/android/launcher3/model/LoaderCursor.java
index 6154e7e..95268d0 100644
--- a/src/com/android/launcher3/model/LoaderCursor.java
+++ b/src/com/android/launcher3/model/LoaderCursor.java
@@ -153,7 +153,7 @@
         info.title = getTitle();
         // the fallback icon
         if (!loadIcon(info)) {
-            info.applyFrom(mIconCache.getDefaultIcon(info.user));
+            info.bitmap = mIconCache.getDefaultIcon(info.user);
         }
 
         // TODO: If there's an explicit component and we can't install that, delete it.
@@ -183,7 +183,7 @@
                 info.iconResource.resourceName = resourceName;
                 BitmapInfo iconInfo = li.createIconBitmap(info.iconResource);
                 if (iconInfo != null) {
-                    info.applyFrom(iconInfo);
+                    info.bitmap = iconInfo;
                     return true;
                 }
             }
@@ -192,7 +192,7 @@
         // Failed to load from resource, try loading from DB.
         byte[] data = getBlob(iconIndex);
         try {
-            info.applyFrom(li.createIconBitmap(BitmapFactory.decodeByteArray(data, 0, data.length)));
+            info.bitmap = li.createIconBitmap(BitmapFactory.decodeByteArray(data, 0, data.length));
             return true;
         } catch (Exception e) {
             Log.e(TAG, "Failed to decode byte array for info " + info, e);
@@ -273,7 +273,7 @@
         info.intent = newIntent;
 
         mIconCache.getTitleAndIcon(info, lai, useLowResIcon);
-        if (mIconCache.isDefaultIcon(info.iconBitmap, user)) {
+        if (mIconCache.isDefaultIcon(info.bitmap, user)) {
             loadIcon(info);
         }
 
diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java
index 353a0d2..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);
@@ -503,14 +518,16 @@
                                     // use the last saved icon instead of the default.
                                     Supplier<ItemInfoWithIcon> fallbackIconProvider = () ->
                                             c.loadIcon(finalInfo, li) ? finalInfo : null;
-                                    info.applyFrom(li.createShortcutIcon(pinnedShortcut,
-                                            true /* badged */, fallbackIconProvider));
+                                    info.bitmap = li.createShortcutIcon(
+                                            pinnedShortcut, true /* badged */,
+                                            fallbackIconProvider);
                                     li.recycle();
                                     if (pmHelper.isAppSuspended(
                                             pinnedShortcut.getPackage(), info.user)) {
                                         info.runtimeStatusFlags |= FLAG_DISABLED_SUSPENDED;
                                     }
                                     intent = info.intent;
+                                    allDeepShortcuts.add(pinnedShortcut);
                                 } else {
                                     // Create a shortcut info in disabled mode for now.
                                     info = c.loadSimpleWorkspaceItem();
@@ -859,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) {
@@ -867,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/model/PackageUpdatedTask.java b/src/com/android/launcher3/model/PackageUpdatedTask.java
index db63b7c..1e614bd 100644
--- a/src/com/android/launcher3/model/PackageUpdatedTask.java
+++ b/src/com/android/launcher3/model/PackageUpdatedTask.java
@@ -189,7 +189,7 @@
                             BitmapInfo iconInfo = li.createIconBitmap(si.iconResource);
                             li.recycle();
                             if (iconInfo != null) {
-                                si.applyFrom(iconInfo);
+                                si.bitmap = iconInfo;
                                 infoUpdated = true;
                             }
                         }
diff --git a/src/com/android/launcher3/model/ShortcutsChangedTask.java b/src/com/android/launcher3/model/ShortcutsChangedTask.java
index 6c358b1..05225d4 100644
--- a/src/com/android/launcher3/model/ShortcutsChangedTask.java
+++ b/src/com/android/launcher3/model/ShortcutsChangedTask.java
@@ -93,8 +93,8 @@
                     // If the shortcut is pinned but no longer has an icon in the system,
                     // keep the current icon instead of reverting to the default icon.
                     LauncherIcons li = LauncherIcons.obtain(context);
-                    workspaceItemInfo.applyFrom(li.createShortcutIcon(fullDetails, true,
-                            () -> workspaceItemInfo));
+                    workspaceItemInfo.bitmap = li.createShortcutIcon(
+                            fullDetails, true, () -> workspaceItemInfo);
                     li.recycle();
                     updatedWorkspaceItemInfos.add(workspaceItemInfo);
                 }
diff --git a/src/com/android/launcher3/model/UserLockStateChangedTask.java b/src/com/android/launcher3/model/UserLockStateChangedTask.java
index 4b773d7..db1c307 100644
--- a/src/com/android/launcher3/model/UserLockStateChangedTask.java
+++ b/src/com/android/launcher3/model/UserLockStateChangedTask.java
@@ -92,7 +92,7 @@
                     // If the shortcut is pinned but no longer has an icon in the system,
                     // keep the current icon instead of reverting to the default icon.
                     LauncherIcons li = LauncherIcons.obtain(context);
-                    si.applyFrom(li.createShortcutIcon(shortcut, true, () -> si));
+                    si.bitmap = li.createShortcutIcon(shortcut, true, () -> si);
                     li.recycle();
                 } else {
                     si.runtimeStatusFlags |= FLAG_DISABLED_LOCKED_USER;
diff --git a/src/com/android/launcher3/notification/NotificationListener.java b/src/com/android/launcher3/notification/NotificationListener.java
index 10378ee..0e73829 100644
--- a/src/com/android/launcher3/notification/NotificationListener.java
+++ b/src/com/android/launcher3/notification/NotificationListener.java
@@ -16,6 +16,7 @@
 
 package com.android.launcher3.notification;
 
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
 import static com.android.launcher3.util.SecureSettingsObserver.newNotificationSettingsObserver;
 
@@ -32,9 +33,10 @@
 import android.util.Log;
 import android.util.Pair;
 
+import androidx.annotation.AnyThread;
 import androidx.annotation.Nullable;
+import androidx.annotation.WorkerThread;
 
-import com.android.launcher3.util.IntSet;
 import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.util.SecureSettingsObserver;
 
@@ -44,6 +46,7 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.stream.Collectors;
 
 /**
  * A {@link NotificationListenerService} that sends updates to its
@@ -59,16 +62,17 @@
     private static final int MSG_NOTIFICATION_POSTED = 1;
     private static final int MSG_NOTIFICATION_REMOVED = 2;
     private static final int MSG_NOTIFICATION_FULL_REFRESH = 3;
+    private static final int MSG_CANCEL_NOTIFICATION = 4;
+    private static final int MSG_RANKING_UPDATE = 5;
 
     private static NotificationListener sNotificationListenerInstance = null;
     private static NotificationsChangedListener sNotificationsChangedListener;
-    private static StatusBarNotificationsChangedListener sStatusBarNotificationsChangedListener;
     private static boolean sIsConnected;
-    private static boolean sIsCreated;
 
     private final Handler mWorkerHandler;
     private final Handler mUiHandler;
     private final Ranking mTempRanking = new Ranking();
+
     /** Maps groupKey's to the corresponding group of notifications. */
     private final Map<String, NotificationGroup> mNotificationGroupMap = new HashMap<>();
     /** Maps keys to their corresponding current group key */
@@ -79,85 +83,12 @@
 
     private SecureSettingsObserver mNotificationDotsObserver;
 
-    private final Handler.Callback mWorkerCallback = new Handler.Callback() {
-        @Override
-        public boolean handleMessage(Message message) {
-            switch (message.what) {
-                case MSG_NOTIFICATION_POSTED:
-                    mUiHandler.obtainMessage(message.what, message.obj).sendToTarget();
-                    break;
-                case MSG_NOTIFICATION_REMOVED:
-                    mUiHandler.obtainMessage(message.what, message.obj).sendToTarget();
-                    break;
-                case MSG_NOTIFICATION_FULL_REFRESH:
-                    List<StatusBarNotification> activeNotifications;
-                    if (sIsConnected) {
-                        try {
-                            activeNotifications = filterNotifications(getActiveNotifications());
-                        } catch (SecurityException ex) {
-                            Log.e(TAG, "SecurityException: failed to fetch notifications");
-                            activeNotifications = new ArrayList<StatusBarNotification>();
-
-                        }
-                    } else {
-                        activeNotifications = new ArrayList<StatusBarNotification>();
-                    }
-
-                    mUiHandler.obtainMessage(message.what, activeNotifications).sendToTarget();
-                    break;
-            }
-            return true;
-        }
-    };
-
-    private final Handler.Callback mUiCallback = new Handler.Callback() {
-        @Override
-        public boolean handleMessage(Message message) {
-            switch (message.what) {
-                case MSG_NOTIFICATION_POSTED:
-                    if (sNotificationsChangedListener != null) {
-                        NotificationPostedMsg msg = (NotificationPostedMsg) message.obj;
-                        sNotificationsChangedListener.onNotificationPosted(msg.packageUserKey,
-                                msg.notificationKey, msg.shouldBeFilteredOut);
-                    }
-                    break;
-                case MSG_NOTIFICATION_REMOVED:
-                    if (sNotificationsChangedListener != null) {
-                        Pair<PackageUserKey, NotificationKeyData> pair
-                                = (Pair<PackageUserKey, NotificationKeyData>) message.obj;
-                        sNotificationsChangedListener.onNotificationRemoved(pair.first, pair.second);
-                    }
-                    break;
-                case MSG_NOTIFICATION_FULL_REFRESH:
-                    if (sNotificationsChangedListener != null) {
-                        sNotificationsChangedListener.onNotificationFullRefresh(
-                                (List<StatusBarNotification>) message.obj);
-                    }
-                    break;
-            }
-            return true;
-        }
-    };
-
     public NotificationListener() {
-        super();
-        mWorkerHandler = new Handler(MODEL_EXECUTOR.getLooper(), mWorkerCallback);
-        mUiHandler = new Handler(Looper.getMainLooper(), mUiCallback);
+        mWorkerHandler = new Handler(MODEL_EXECUTOR.getLooper(), this::handleWorkerMessage);
+        mUiHandler = new Handler(Looper.getMainLooper(), this::handleUiMessage);
         sNotificationListenerInstance = this;
     }
 
-    @Override
-    public void onCreate() {
-        super.onCreate();
-        sIsCreated = true;
-    }
-
-    @Override
-    public void onDestroy() {
-        super.onDestroy();
-        sIsCreated = false;
-    }
-
     public static @Nullable NotificationListener getInstanceIfConnected() {
         return sIsConnected ? sNotificationListenerInstance : null;
     }
@@ -168,25 +99,107 @@
         NotificationListener notificationListener = getInstanceIfConnected();
         if (notificationListener != null) {
             notificationListener.onNotificationFullRefresh();
-        } else if (!sIsCreated && sNotificationsChangedListener != null) {
+        } else {
             // User turned off dots globally, so we unbound this service;
             // tell the listener that there are no notifications to remove dots.
-            sNotificationsChangedListener.onNotificationFullRefresh(
-                    Collections.<StatusBarNotification>emptyList());
+            MODEL_EXECUTOR.submit(() -> MAIN_EXECUTOR.submit(() ->
+                            listener.onNotificationFullRefresh(Collections.emptyList())));
         }
     }
 
-    public static void setStatusBarNotificationsChangedListener
-            (StatusBarNotificationsChangedListener listener) {
-        sStatusBarNotificationsChangedListener = listener;
-    }
-
     public static void removeNotificationsChangedListener() {
         sNotificationsChangedListener = null;
     }
 
-    public static void removeStatusBarNotificationsChangedListener() {
-        sStatusBarNotificationsChangedListener = null;
+    private boolean handleWorkerMessage(Message message) {
+        switch (message.what) {
+            case MSG_NOTIFICATION_POSTED: {
+                StatusBarNotification sbn = (StatusBarNotification) message.obj;
+                mUiHandler.obtainMessage(shouldBeFilteredOut(sbn)
+                                ? MSG_NOTIFICATION_REMOVED : MSG_NOTIFICATION_POSTED,
+                        toKeyPair(sbn)).sendToTarget();
+                return true;
+            }
+            case MSG_NOTIFICATION_REMOVED: {
+                StatusBarNotification sbn = (StatusBarNotification) message.obj;
+                mUiHandler.obtainMessage(MSG_NOTIFICATION_REMOVED,
+                        toKeyPair(sbn)).sendToTarget();
+
+                NotificationGroup notificationGroup = mNotificationGroupMap.get(sbn.getGroupKey());
+                String key = sbn.getKey();
+                if (notificationGroup != null) {
+                    notificationGroup.removeChildKey(key);
+                    if (notificationGroup.isEmpty()) {
+                        if (key.equals(mLastKeyDismissedByLauncher)) {
+                            // Only cancel the group notification if launcher dismissed the
+                            // last child.
+                            cancelNotification(notificationGroup.getGroupSummaryKey());
+                        }
+                        mNotificationGroupMap.remove(sbn.getGroupKey());
+                    }
+                }
+                if (key.equals(mLastKeyDismissedByLauncher)) {
+                    mLastKeyDismissedByLauncher = null;
+                }
+                return true;
+            }
+            case MSG_NOTIFICATION_FULL_REFRESH:
+                List<StatusBarNotification> activeNotifications = null;
+                if (sIsConnected) {
+                    try {
+                        activeNotifications = Arrays.stream(getActiveNotifications())
+                                .filter(this::shouldBeFilteredOut)
+                                .collect(Collectors.toList());
+                    } catch (SecurityException ex) {
+                        Log.e(TAG, "SecurityException: failed to fetch notifications");
+                        activeNotifications = new ArrayList<>();
+                    }
+                } else {
+                    activeNotifications = new ArrayList<>();
+                }
+
+                mUiHandler.obtainMessage(message.what, activeNotifications).sendToTarget();
+                return true;
+            case MSG_CANCEL_NOTIFICATION: {
+                mLastKeyDismissedByLauncher = (String) message.obj;
+                cancelNotification(mLastKeyDismissedByLauncher);
+                return true;
+            }
+            case MSG_RANKING_UPDATE: {
+                String[] keys = ((RankingMap) message.obj).getOrderedKeys();
+                for (StatusBarNotification sbn : getActiveNotifications(keys)) {
+                    updateGroupKeyIfNecessary(sbn);
+                }
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private boolean handleUiMessage(Message message) {
+        switch (message.what) {
+            case MSG_NOTIFICATION_POSTED:
+                if (sNotificationsChangedListener != null) {
+                    Pair<PackageUserKey, NotificationKeyData> msg = (Pair) message.obj;
+                    sNotificationsChangedListener.onNotificationPosted(
+                            msg.first, msg.second);
+                }
+                break;
+            case MSG_NOTIFICATION_REMOVED:
+                if (sNotificationsChangedListener != null) {
+                    Pair<PackageUserKey, NotificationKeyData> msg = (Pair) message.obj;
+                    sNotificationsChangedListener.onNotificationRemoved(
+                            msg.first, msg.second);
+                }
+                break;
+            case MSG_NOTIFICATION_FULL_REFRESH:
+                if (sNotificationsChangedListener != null) {
+                    sNotificationsChangedListener.onNotificationFullRefresh(
+                            (List<StatusBarNotification>) message.obj);
+                }
+                break;
+        }
+        return true;
     }
 
     @Override
@@ -217,84 +230,37 @@
         super.onListenerDisconnected();
         sIsConnected = false;
         mNotificationDotsObserver.unregister();
+        onNotificationFullRefresh();
     }
 
     @Override
     public void onNotificationPosted(final StatusBarNotification sbn) {
-        super.onNotificationPosted(sbn);
-        if (sbn == null) {
-            // There is a bug in platform where we can get a null notification; just ignore it.
-            return;
-        }
-        mWorkerHandler.obtainMessage(MSG_NOTIFICATION_POSTED, new NotificationPostedMsg(sbn))
-            .sendToTarget();
-        if (sStatusBarNotificationsChangedListener != null) {
-            sStatusBarNotificationsChangedListener.onNotificationPosted(sbn);
-        }
-    }
-
-    /**
-     * An object containing data to send to MSG_NOTIFICATION_POSTED targets.
-     */
-    private class NotificationPostedMsg {
-        final PackageUserKey packageUserKey;
-        final NotificationKeyData notificationKey;
-        final boolean shouldBeFilteredOut;
-
-        NotificationPostedMsg(StatusBarNotification sbn) {
-            packageUserKey = PackageUserKey.fromNotification(sbn);
-            notificationKey = NotificationKeyData.fromNotification(sbn);
-            shouldBeFilteredOut = shouldBeFilteredOut(sbn);
+        if (sbn != null) {
+            mWorkerHandler.obtainMessage(MSG_NOTIFICATION_POSTED, sbn).sendToTarget();
         }
     }
 
     @Override
     public void onNotificationRemoved(final StatusBarNotification sbn) {
-        super.onNotificationRemoved(sbn);
-        if (sbn == null) {
-            // There is a bug in platform where we can get a null notification; just ignore it.
-            return;
+        if (sbn != null) {
+            mWorkerHandler.obtainMessage(MSG_NOTIFICATION_REMOVED, sbn).sendToTarget();
         }
-        Pair<PackageUserKey, NotificationKeyData> packageUserKeyAndNotificationKey
-            = new Pair<>(PackageUserKey.fromNotification(sbn),
-            NotificationKeyData.fromNotification(sbn));
-        mWorkerHandler.obtainMessage(MSG_NOTIFICATION_REMOVED, packageUserKeyAndNotificationKey)
-            .sendToTarget();
-        if (sStatusBarNotificationsChangedListener != null) {
-            sStatusBarNotificationsChangedListener.onNotificationRemoved(sbn);
-        }
-
-        NotificationGroup notificationGroup = mNotificationGroupMap.get(sbn.getGroupKey());
-        String key = sbn.getKey();
-        if (notificationGroup != null) {
-            notificationGroup.removeChildKey(key);
-            if (notificationGroup.isEmpty()) {
-                if (key.equals(mLastKeyDismissedByLauncher)) {
-                    // Only cancel the group notification if launcher dismissed the last child.
-                    cancelNotification(notificationGroup.getGroupSummaryKey());
-                }
-                mNotificationGroupMap.remove(sbn.getGroupKey());
-            }
-        }
-        if (key.equals(mLastKeyDismissedByLauncher)) {
-            mLastKeyDismissedByLauncher = null;
-        }
-    }
-
-    public void cancelNotificationFromLauncher(String key) {
-        mLastKeyDismissedByLauncher = key;
-        cancelNotification(key);
     }
 
     @Override
     public void onNotificationRankingUpdate(RankingMap rankingMap) {
-        super.onNotificationRankingUpdate(rankingMap);
-        String[] keys = rankingMap.getOrderedKeys();
-        for (StatusBarNotification sbn : getActiveNotifications(keys)) {
-            updateGroupKeyIfNecessary(sbn);
-        }
+        mWorkerHandler.obtainMessage(MSG_RANKING_UPDATE, rankingMap).sendToTarget();
     }
 
+    /**
+     * Cancels a notification
+     */
+    @AnyThread
+    public void cancelNotificationFromLauncher(String key) {
+        mWorkerHandler.obtainMessage(MSG_CANCEL_NOTIFICATION, key).sendToTarget();
+    }
+
+    @WorkerThread
     private void updateGroupKeyIfNecessary(StatusBarNotification sbn) {
         String childKey = sbn.getKey();
         String oldGroupKey = mNotificationGroupKeyMap.get(childKey);
@@ -328,43 +294,23 @@
         }
     }
 
-    /** This makes a potentially expensive binder call and should be run on a background thread. */
+    /**
+     * This makes a potentially expensive binder call and should be run on a background thread.
+     */
+    @WorkerThread
     public List<StatusBarNotification> getNotificationsForKeys(List<NotificationKeyData> keys) {
-        StatusBarNotification[] notifications = NotificationListener.this
-                .getActiveNotifications(NotificationKeyData.extractKeysOnly(keys)
-                        .toArray(new String[keys.size()]));
-        return notifications == null
-                ? Collections.<StatusBarNotification>emptyList() : Arrays.asList(notifications);
+        StatusBarNotification[] notifications = getActiveNotifications(
+                keys.stream().map(n -> n.notificationKey).toArray(String[]::new));
+        return notifications == null ? Collections.emptyList() : Arrays.asList(notifications);
     }
 
     /**
-     * Filter out notifications that don't have an intent
-     * or are headers for grouped notifications.
-     *
-     * @see #shouldBeFilteredOut(StatusBarNotification)
+     * Returns true for notifications that don't have an intent
+     * or are headers for grouped notifications and should be filtered out.
      */
-    private List<StatusBarNotification> filterNotifications(
-            StatusBarNotification[] notifications) {
-        if (notifications == null) return null;
-        IntSet removedNotifications = new IntSet();
-        for (int i = 0; i < notifications.length; i++) {
-            if (shouldBeFilteredOut(notifications[i])) {
-                removedNotifications.add(i);
-            }
-        }
-        List<StatusBarNotification> filteredNotifications = new ArrayList<>(
-                notifications.length - removedNotifications.size());
-        for (int i = 0; i < notifications.length; i++) {
-            if (!removedNotifications.contains(i)) {
-                filteredNotifications.add(notifications[i]);
-            }
-        }
-        return filteredNotifications;
-    }
-
+    @WorkerThread
     private boolean shouldBeFilteredOut(StatusBarNotification sbn) {
         Notification notification = sbn.getNotification();
-
         updateGroupKeyIfNecessary(sbn);
 
         getCurrentRanking().getRanking(sbn.getKey(), mTempRanking);
@@ -385,16 +331,16 @@
         return (isGroupHeader || missingTitleAndText);
     }
 
+    private static Pair<PackageUserKey, NotificationKeyData> toKeyPair(StatusBarNotification sbn) {
+        return Pair.create(PackageUserKey.fromNotification(sbn),
+                NotificationKeyData.fromNotification(sbn));
+    }
+
     public interface NotificationsChangedListener {
         void onNotificationPosted(PackageUserKey postedPackageUserKey,
-                NotificationKeyData notificationKey, boolean shouldBeFilteredOut);
+                NotificationKeyData notificationKey);
         void onNotificationRemoved(PackageUserKey removedPackageUserKey,
                 NotificationKeyData notificationKey);
         void onNotificationFullRefresh(List<StatusBarNotification> activeNotifications);
     }
-
-    public interface StatusBarNotificationsChangedListener {
-        void onNotificationPosted(StatusBarNotification sbn);
-        void onNotificationRemoved(StatusBarNotification sbn);
-    }
 }
diff --git a/src/com/android/launcher3/pm/PinRequestHelper.java b/src/com/android/launcher3/pm/PinRequestHelper.java
index 68ea6c4..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.applyFrom(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/popup/ArrowPopup.java b/src/com/android/launcher3/popup/ArrowPopup.java
index 28000b9..98f7fd8 100644
--- a/src/com/android/launcher3/popup/ArrowPopup.java
+++ b/src/com/android/launcher3/popup/ArrowPopup.java
@@ -360,10 +360,14 @@
         final TimeInterpolator revealInterpolator = ACCEL_DEACCEL;
 
         // Rectangular reveal.
+        mEndRect.set(0, 0, getMeasuredWidth(), getMeasuredHeight());
         final ValueAnimator revealAnim = createOpenCloseOutlineProvider()
                 .createRevealAnimator(this, false);
         revealAnim.setDuration(revealDuration);
         revealAnim.setInterpolator(revealInterpolator);
+        // Clip the popup to the initial outline while the notification dot and arrow animate.
+        revealAnim.start();
+        revealAnim.pause();
 
         ValueAnimator fadeIn = ValueAnimator.ofFloat(0, 1);
         fadeIn.setDuration(revealDuration + arrowDuration);
@@ -399,7 +403,6 @@
         if (!mIsOpen) {
             return;
         }
-        mEndRect.setEmpty();
         if (getOutlineProvider() instanceof RevealOutlineAnimation) {
             ((RevealOutlineAnimation) getOutlineProvider()).getOutline(mEndRect);
         }
@@ -471,9 +474,6 @@
 
         mStartRect.set(arrowCenterX - halfArrowWidth, arrowCenterY, arrowCenterX + halfArrowWidth,
                 arrowCenterY);
-        if (mEndRect.isEmpty()) {
-            mEndRect.set(0, 0, getMeasuredWidth(), getMeasuredHeight());
-        }
 
         return new RoundedRectRevealOutlineProvider
                 (arrowCornerRadius, mOutlineRadius, mStartRect, mEndRect);
diff --git a/src/com/android/launcher3/popup/PopupContainerWithArrow.java b/src/com/android/launcher3/popup/PopupContainerWithArrow.java
index 2034926..e70673a 100644
--- a/src/com/android/launcher3/popup/PopupContainerWithArrow.java
+++ b/src/com/android/launcher3/popup/PopupContainerWithArrow.java
@@ -197,9 +197,6 @@
      * @return the container if shown or null.
      */
     public static PopupContainerWithArrow showForIcon(BubbleTextView icon) {
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.NO_CONTEXT_MENU, "showForIcon");
-        }
         Launcher launcher = Launcher.getLauncher(icon.getContext());
         if (getOpen(launcher) != null) {
             // There is already an items container open, so don't open this one.
@@ -240,9 +237,6 @@
     }
 
     protected void populateAndShow(BubbleTextView icon, ItemInfo item) {
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.NO_CONTEXT_MENU, "populateAndShow");
-        }
         PopupDataProvider popupDataProvider = mLauncher.getPopupDataProvider();
         populateAndShow(icon,
                 popupDataProvider.getShortcutCountForItem(item),
@@ -507,7 +501,7 @@
         DotInfo dotInfo = mLauncher.getDotInfoForItem(itemInfo);
         if (mNotificationItemView != null && dotInfo != null) {
             mNotificationItemView.updateHeader(
-                    dotInfo.getNotificationCount(), itemInfo.iconColor);
+                    dotInfo.getNotificationCount(), itemInfo.bitmap.color);
         }
     }
 
@@ -576,8 +570,11 @@
 
     @Override
     protected void closeComplete() {
-        mOriginalIcon.setTextVisibility(mOriginalIcon.shouldTextBeVisible());
-        mOriginalIcon.setForceHideDot(false);
+        PopupContainerWithArrow openPopup = getOpen(mLauncher);
+        if (openPopup == null || openPopup.mOriginalIcon != mOriginalIcon) {
+            mOriginalIcon.setTextVisibility(mOriginalIcon.shouldTextBeVisible());
+            mOriginalIcon.setForceHideDot(false);
+        }
         super.closeComplete();
     }
 
diff --git a/src/com/android/launcher3/popup/PopupDataProvider.java b/src/com/android/launcher3/popup/PopupDataProvider.java
index 4612b2a..c5aa836 100644
--- a/src/com/android/launcher3/popup/PopupDataProvider.java
+++ b/src/com/android/launcher3/popup/PopupDataProvider.java
@@ -20,13 +20,15 @@
 import android.service.notification.StatusBarNotification;
 import android.util.Log;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
 import com.android.launcher3.ItemInfo;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.dot.DotInfo;
 import com.android.launcher3.model.WidgetItem;
 import com.android.launcher3.notification.NotificationKeyData;
 import com.android.launcher3.notification.NotificationListener;
-import com.android.launcher3.shortcuts.DeepShortcutManager;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.util.ShortcutUtil;
@@ -39,13 +41,9 @@
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
 
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
 /**
  * Provides data for the popup menu that appears after long-clicking on apps.
  */
@@ -76,28 +74,14 @@
 
     @Override
     public void onNotificationPosted(PackageUserKey postedPackageUserKey,
-            NotificationKeyData notificationKey, boolean shouldBeFilteredOut) {
+            NotificationKeyData notificationKey) {
         DotInfo dotInfo = mPackageUserToDotInfos.get(postedPackageUserKey);
-        boolean dotShouldBeRefreshed;
         if (dotInfo == null) {
-            if (!shouldBeFilteredOut) {
-                DotInfo newDotInfo = new DotInfo();
-                newDotInfo.addOrUpdateNotificationKey(notificationKey);
-                mPackageUserToDotInfos.put(postedPackageUserKey, newDotInfo);
-                dotShouldBeRefreshed = true;
-            } else {
-                dotShouldBeRefreshed = false;
-            }
-        } else {
-            dotShouldBeRefreshed = shouldBeFilteredOut
-                    ? dotInfo.removeNotificationKey(notificationKey)
-                    : dotInfo.addOrUpdateNotificationKey(notificationKey);
-            if (dotInfo.getNotificationKeys().size() == 0) {
-                mPackageUserToDotInfos.remove(postedPackageUserKey);
-            }
+            dotInfo = new DotInfo();
+            mPackageUserToDotInfos.put(postedPackageUserKey, dotInfo);
         }
-        if (dotShouldBeRefreshed) {
-            updateNotificationDots(t -> postedPackageUserKey.equals(t));
+        if (dotInfo.addOrUpdateNotificationKey(notificationKey)) {
+            updateNotificationDots(postedPackageUserKey::equals);
         }
     }
 
@@ -109,7 +93,7 @@
             if (oldDotInfo.getNotificationKeys().size() == 0) {
                 mPackageUserToDotInfos.remove(removedPackageUserKey);
             }
-            updateNotificationDots(t -> removedPackageUserKey.equals(t));
+            updateNotificationDots(removedPackageUserKey::equals);
             trimNotifications(mPackageUserToDotInfos);
         }
     }
@@ -195,14 +179,6 @@
                 : getNotificationsForItem(info, dotInfo.getNotificationKeys());
     }
 
-    /** This makes a potentially expensive binder call and should be run on a background thread. */
-    public @NonNull List<StatusBarNotification> getStatusBarNotificationsForKeys(
-            List<NotificationKeyData> notificationKeys) {
-        NotificationListener notificationListener = NotificationListener.getInstanceIfConnected();
-        return notificationListener == null ? Collections.EMPTY_LIST
-                : notificationListener.getNotificationsForKeys(notificationKeys);
-    }
-
     public void cancelNotification(String notificationKey) {
         NotificationListener notificationListener = NotificationListener.getInstanceIfConnected();
         if (notificationListener == null) {
diff --git a/src/com/android/launcher3/popup/PopupPopulator.java b/src/com/android/launcher3/popup/PopupPopulator.java
index dbfe988..80c6683 100644
--- a/src/com/android/launcher3/popup/PopupPopulator.java
+++ b/src/com/android/launcher3/popup/PopupPopulator.java
@@ -20,7 +20,9 @@
 import android.content.pm.ShortcutInfo;
 import android.os.Handler;
 import android.os.UserHandle;
-import android.service.notification.StatusBarNotification;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.ItemInfo;
 import com.android.launcher3.Launcher;
@@ -28,6 +30,7 @@
 import com.android.launcher3.icons.LauncherIcons;
 import com.android.launcher3.notification.NotificationInfo;
 import com.android.launcher3.notification.NotificationKeyData;
+import com.android.launcher3.notification.NotificationListener;
 import com.android.launcher3.shortcuts.DeepShortcutManager;
 import com.android.launcher3.shortcuts.DeepShortcutView;
 import com.android.launcher3.util.PackageUserKey;
@@ -37,9 +40,7 @@
 import java.util.Comparator;
 import java.util.Iterator;
 import java.util.List;
-
-import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
+import java.util.stream.Collectors;
 
 /**
  * Contains logic relevant to populating a {@link PopupContainerWithArrow}. In particular,
@@ -130,12 +131,15 @@
         final UserHandle user = originalInfo.user;
         return () -> {
             if (!notificationKeys.isEmpty()) {
-                List<StatusBarNotification> notifications = launcher.getPopupDataProvider()
-                        .getStatusBarNotificationsForKeys(notificationKeys);
-                List<NotificationInfo> infos = new ArrayList<>(notifications.size());
-                for (int i = 0; i < notifications.size(); i++) {
-                    StatusBarNotification notification = notifications.get(i);
-                    infos.add(new NotificationInfo(launcher, notification));
+                NotificationListener notificationListener =
+                        NotificationListener.getInstanceIfConnected();
+                final List<NotificationInfo> infos;
+                if (notificationListener == null) {
+                    infos = Collections.emptyList();
+                } else {
+                    infos = notificationListener.getNotificationsForKeys(notificationKeys).stream()
+                            .map(sbn -> new NotificationInfo(launcher, sbn))
+                            .collect(Collectors.toList());
                 }
                 uiHandler.post(() -> container.applyNotificationInfos(infos));
             }
@@ -150,7 +154,7 @@
                 final WorkspaceItemInfo si = new WorkspaceItemInfo(shortcut, launcher);
                 // Use unbadged icon for the menu.
                 LauncherIcons li = LauncherIcons.obtain(launcher);
-                si.applyFrom(li.createShortcutIcon(shortcut, false /* badged */));
+                si.bitmap = li.createShortcutIcon(shortcut, false /* badged */);
                 li.recycle();
                 si.rank = i;
 
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/testing/TestInformationHandler.java b/src/com/android/launcher3/testing/TestInformationHandler.java
index 140a06a..64df384 100644
--- a/src/com/android/launcher3/testing/TestInformationHandler.java
+++ b/src/com/android/launcher3/testing/TestInformationHandler.java
@@ -24,9 +24,10 @@
 import android.graphics.Color;
 import android.os.Bundle;
 import android.os.Debug;
-import android.util.Log;
 import android.view.View;
 
+import androidx.annotation.Keep;
+
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.Launcher;
@@ -186,6 +187,22 @@
         Runtime.getRuntime().runFinalization();
 
         final CountDownLatch fence = new CountDownLatch(1);
+        createFinalizationObserver(fence);
+        try {
+            do {
+                Runtime.getRuntime().gc();
+                Runtime.getRuntime().runFinalization();
+            } while (!fence.await(100, TimeUnit.MILLISECONDS));
+        } catch (InterruptedException ex) {
+            throw new RuntimeException(ex);
+        }
+    }
+
+    // Create the observer in the scope of a method to minimize the chance that
+    // it remains live in a DEX/machine register at the point of the fence guard.
+    // This must be kept to avoid R8 inlining it.
+    @Keep
+    private static void createFinalizationObserver(CountDownLatch fence) {
         new Object() {
             @Override
             protected void finalize() throws Throwable {
@@ -196,13 +213,5 @@
                 }
             }
         };
-        try {
-            do {
-                Runtime.getRuntime().gc();
-                Runtime.getRuntime().runFinalization();
-            } while (!fence.await(100, TimeUnit.MILLISECONDS));
-        } catch (InterruptedException ex) {
-            throw new RuntimeException(ex);
-        }
     }
 }
diff --git a/src/com/android/launcher3/testing/TestProtocol.java b/src/com/android/launcher3/testing/TestProtocol.java
index 1766814..1cfa4af 100644
--- a/src/com/android/launcher3/testing/TestProtocol.java
+++ b/src/com/android/launcher3/testing/TestProtocol.java
@@ -84,6 +84,4 @@
     public static final String NO_BACKGROUND_TO_OVERVIEW_TAG = "b/138251824";
     public static final String NO_DRAG_TO_WORKSPACE = "b/138729456";
     public static final String APP_NOT_DISABLED = "b/139891609";
-    public static final String NO_CONTEXT_MENU = "b/141770616";
-    public static final String CRASH_ADD_CUSTOM_SHORTCUT = "b/141568904";
 }
diff --git a/src/com/android/launcher3/touch/ItemLongClickListener.java b/src/com/android/launcher3/touch/ItemLongClickListener.java
index 86d2b39..aa02d0a 100644
--- a/src/com/android/launcher3/touch/ItemLongClickListener.java
+++ b/src/com/android/launcher3/touch/ItemLongClickListener.java
@@ -79,19 +79,10 @@
     }
 
     private static boolean onAllAppsItemLongClick(View v) {
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.NO_CONTEXT_MENU, "onAllAppsItemLongClick1");
-        }
         Launcher launcher = Launcher.getLauncher(v.getContext());
         if (!canStartDrag(launcher)) return false;
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.NO_CONTEXT_MENU, "onAllAppsItemLongClick2");
-        }
         // When we have exited all apps or are in transition, disregard long clicks
         if (!launcher.isInState(ALL_APPS) && !launcher.isInState(OVERVIEW)) return false;
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.NO_CONTEXT_MENU, "onAllAppsItemLongClick3");
-        }
         if (launcher.getWorkspace().isSwitchingState()) return false;
 
         // Start the drag
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/ContentWriter.java b/src/com/android/launcher3/util/ContentWriter.java
index 00adf10..2d64353 100644
--- a/src/com/android/launcher3/util/ContentWriter.java
+++ b/src/com/android/launcher3/util/ContentWriter.java
@@ -26,6 +26,7 @@
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.compat.UserManagerCompat;
+import com.android.launcher3.icons.BitmapInfo;
 import com.android.launcher3.icons.GraphicsUtils;
 
 /**
@@ -37,7 +38,7 @@
     private final Context mContext;
 
     private CommitParams mCommitParams;
-    private Bitmap mIcon;
+    private BitmapInfo mIcon;
     private UserHandle mUser;
 
     public ContentWriter(Context context, CommitParams commitParams) {
@@ -79,7 +80,7 @@
         return this;
     }
 
-    public ContentWriter putIcon(Bitmap value, UserHandle user) {
+    public ContentWriter putIcon(BitmapInfo value, UserHandle user) {
         mIcon = value;
         mUser = user;
         return this;
@@ -97,7 +98,7 @@
         Preconditions.assertNonUiThread();
         if (mIcon != null && !LauncherAppState.getInstance(context).getIconCache()
                 .isDefaultIcon(mIcon, mUser)) {
-            mValues.put(LauncherSettings.Favorites.ICON, GraphicsUtils.flattenBitmap(mIcon));
+            mValues.put(LauncherSettings.Favorites.ICON, GraphicsUtils.flattenBitmap(mIcon.icon));
             mIcon = null;
         }
         return mValues;
diff --git a/src/com/android/launcher3/util/PackageManagerHelper.java b/src/com/android/launcher3/util/PackageManagerHelper.java
index 7b4e0c6..91f687e 100644
--- a/src/com/android/launcher3/util/PackageManagerHelper.java
+++ b/src/com/android/launcher3/util/PackageManagerHelper.java
@@ -274,6 +274,9 @@
         } else {
             packageName = cn.getPackageName();
         }
+        if (packageName == null) {
+            packageName = intent.getPackage();
+        }
         if (packageName != null) {
             try {
                 PackageInfo info = pm.getPackageInfo(packageName, 0);
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/src/com/android/launcher3/util/VibratorWrapper.java b/src/com/android/launcher3/util/VibratorWrapper.java
new file mode 100644
index 0000000..04741a1
--- /dev/null
+++ b/src/com/android/launcher3/util/VibratorWrapper.java
@@ -0,0 +1,84 @@
+/*
+ * 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.util;
+
+import static android.os.VibrationEffect.createPredefined;
+import static android.provider.Settings.System.HAPTIC_FEEDBACK_ENABLED;
+
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
+
+import android.annotation.TargetApi;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.os.Build;
+import android.os.VibrationEffect;
+import android.os.Vibrator;
+import android.provider.Settings;
+
+/**
+ * Wrapper around {@link Vibrator} to easily perform haptic feedback where necessary.
+ */
+@TargetApi(Build.VERSION_CODES.Q)
+public class VibratorWrapper {
+
+    public static final MainThreadInitializedObject<VibratorWrapper> INSTANCE =
+            new MainThreadInitializedObject<>(VibratorWrapper::new);
+
+    private static final VibrationEffect EFFECT_CLICK =
+            createPredefined(VibrationEffect.EFFECT_CLICK);
+
+    /**
+     * Haptic when entering overview.
+     */
+    public static final VibrationEffect OVERVIEW_HAPTIC = EFFECT_CLICK;
+
+    private final Vibrator mVibrator;
+    private final boolean mHasVibrator;
+
+    private boolean mIsHapticFeedbackEnabled;
+
+    public VibratorWrapper(Context context) {
+        mVibrator = context.getSystemService(Vibrator.class);
+        mHasVibrator = mVibrator.hasVibrator();
+        if (mHasVibrator) {
+            final ContentResolver resolver = context.getContentResolver();
+            mIsHapticFeedbackEnabled = isHapticFeedbackEnabled(resolver);
+            final ContentObserver observer = new ContentObserver(MAIN_EXECUTOR.getHandler()) {
+                @Override
+                public void onChange(boolean selfChange) {
+                    mIsHapticFeedbackEnabled = isHapticFeedbackEnabled(resolver);
+                }
+            };
+            resolver.registerContentObserver(Settings.System.getUriFor(HAPTIC_FEEDBACK_ENABLED),
+                    false /* notifyForDescendents */, observer);
+        } else {
+            mIsHapticFeedbackEnabled = false;
+        }
+    }
+
+    private boolean isHapticFeedbackEnabled(ContentResolver resolver) {
+        return Settings.System.getInt(resolver, HAPTIC_FEEDBACK_ENABLED, 0) == 1;
+    }
+
+    /** Vibrates with the given effect if haptic feedback is available and enabled. */
+    public void vibrate(VibrationEffect vibrationEffect) {
+        if (mHasVibrator && mIsHapticFeedbackEnabled) {
+            UI_HELPER_EXECUTOR.execute(() -> mVibrator.vibrate(vibrationEffect));
+        }
+    }
+}
diff --git a/src/com/android/launcher3/views/BaseDragLayer.java b/src/com/android/launcher3/views/BaseDragLayer.java
index 2a4c5a7..e43fc8a 100644
--- a/src/com/android/launcher3/views/BaseDragLayer.java
+++ b/src/com/android/launcher3/views/BaseDragLayer.java
@@ -170,10 +170,8 @@
             // Only look for controllers if we are not dispatching from gesture area and proxy is
             // not active
             mActiveController = findControllerToHandleTouch(ev);
-
-            if (mActiveController != null) return true;
         }
-        return false;
+        return mActiveController != null;
     }
 
     @Override
diff --git a/src/com/android/launcher3/views/FloatingIconView.java b/src/com/android/launcher3/views/FloatingIconView.java
index 45c0d90..c63d745 100644
--- a/src/com/android/launcher3/views/FloatingIconView.java
+++ b/src/com/android/launcher3/views/FloatingIconView.java
@@ -415,7 +415,7 @@
             int width = isFolderIcon ? originalView.getWidth() : (int) pos.width();
             int height = isFolderIcon ? originalView.getHeight() : (int) pos.height();
             if (supportsAdaptiveIcons) {
-                drawable = getFullDrawable(l, info, width, height, false, sTmpObjArray);
+                drawable = getFullDrawable(l, info, width, height, sTmpObjArray);
                 if (drawable instanceof AdaptiveIconDrawable) {
                     badge = getBadge(l, info, sTmpObjArray[0]);
                 } else {
@@ -428,7 +428,7 @@
                     // Similar to DragView, we simply use the BubbleTextView icon here.
                     drawable = btvIcon;
                 } else {
-                    drawable = getFullDrawable(l, info, width, height, false, sTmpObjArray);
+                    drawable = getFullDrawable(l, info, width, height, sTmpObjArray);
                 }
             }
         }
diff --git a/src/com/android/launcher3/widget/PendingAppWidgetHostView.java b/src/com/android/launcher3/widget/PendingAppWidgetHostView.java
index 50db40f..6038873 100644
--- a/src/com/android/launcher3/widget/PendingAppWidgetHostView.java
+++ b/src/com/android/launcher3/widget/PendingAppWidgetHostView.java
@@ -16,6 +16,9 @@
 
 package com.android.launcher3.widget;
 
+import static com.android.launcher3.FastBitmapDrawable.newIcon;
+import static com.android.launcher3.graphics.PreloadIconDrawable.newPendingIcon;
+
 import android.content.Context;
 import android.graphics.Canvas;
 import android.graphics.Color;
@@ -33,12 +36,11 @@
 
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.FastBitmapDrawable;
-import com.android.launcher3.icons.IconCache;
-import com.android.launcher3.icons.IconCache.ItemInfoUpdateReceiver;
 import com.android.launcher3.ItemInfoWithIcon;
 import com.android.launcher3.LauncherAppWidgetInfo;
 import com.android.launcher3.R;
-import com.android.launcher3.graphics.DrawableFactory;
+import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.icons.IconCache.ItemInfoUpdateReceiver;
 import com.android.launcher3.model.PackageItemInfo;
 import com.android.launcher3.touch.ItemClickHandler;
 import com.android.launcher3.util.Themes;
@@ -128,24 +130,22 @@
             mCenterDrawable.setCallback(null);
             mCenterDrawable = null;
         }
-        if (info.iconBitmap != null) {
+        if (info.bitmap.icon != null) {
             // The view displays three modes,
             //   1) App icon in the center
             //   2) Preload icon in the center
             //   3) Setup icon in the center and app icon in the top right corner.
-            DrawableFactory drawableFactory = DrawableFactory.INSTANCE.get(getContext());
             if (mDisabledForSafeMode) {
-                FastBitmapDrawable disabledIcon = drawableFactory.newIcon(getContext(), info);
+                FastBitmapDrawable disabledIcon = newIcon(getContext(), info);
                 disabledIcon.setIsDisabled(true);
                 mCenterDrawable = disabledIcon;
                 mSettingIconDrawable = null;
             } else if (isReadyForClickSetup()) {
-                mCenterDrawable = drawableFactory.newIcon(getContext(), info);
+                mCenterDrawable = newIcon(getContext(), info);
                 mSettingIconDrawable = getResources().getDrawable(R.drawable.ic_setting).mutate();
-                updateSettingColor(info.iconColor);
+                updateSettingColor(info.bitmap.color);
             } else {
-                mCenterDrawable = DrawableFactory.INSTANCE.get(getContext())
-                        .newPendingIcon(getContext(), info);
+                mCenterDrawable = newPendingIcon(getContext(), info);
                 mSettingIconDrawable = null;
                 applyState();
             }
diff --git a/src/com/android/launcher3/widget/WidgetCell.java b/src/com/android/launcher3/widget/WidgetCell.java
index 6944879..f713b33 100644
--- a/src/com/android/launcher3/widget/WidgetCell.java
+++ b/src/com/android/launcher3/widget/WidgetCell.java
@@ -36,7 +36,6 @@
 import com.android.launcher3.SimpleOnStylusPressListener;
 import com.android.launcher3.StylusEventHelper;
 import com.android.launcher3.WidgetPreviewLoader;
-import com.android.launcher3.graphics.DrawableFactory;
 import com.android.launcher3.icons.BaseIconFactory;
 import com.android.launcher3.model.WidgetItem;
 
@@ -182,10 +181,8 @@
             return;
         }
         if (bitmap != null) {
-            mWidgetImage.setBitmap(bitmap,
-                    DrawableFactory.INSTANCE.get(getContext()).getBadgeForUser(mItem.user,
-                            getContext(), BaseIconFactory.getBadgeSizeForIconSize(
-                                    mDeviceProfile.allAppsIconSizePx)));
+            mWidgetImage.setBitmap(bitmap, mWidgetPreviewLoader.getBadgeForUser(mItem.user,
+                    BaseIconFactory.getBadgeSizeForIconSize(mDeviceProfile.allAppsIconSizePx)));
             if (mAnimatePreview) {
                 mWidgetImage.setAlpha(0f);
                 ViewPropertyAnimator anim = mWidgetImage.animate();
diff --git a/src/com/android/launcher3/widget/WidgetsDiffReporter.java b/src/com/android/launcher3/widget/WidgetsDiffReporter.java
index 435125b..f3b325d 100644
--- a/src/com/android/launcher3/widget/WidgetsDiffReporter.java
+++ b/src/com/android/launcher3/widget/WidgetsDiffReporter.java
@@ -18,6 +18,8 @@
 
 import android.util.Log;
 
+import androidx.recyclerview.widget.RecyclerView;
+
 import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.model.PackageItemInfo;
 import com.android.launcher3.widget.WidgetsListAdapter.WidgetListRowEntryComparator;
@@ -25,8 +27,6 @@
 import java.util.ArrayList;
 import java.util.Iterator;
 
-import androidx.recyclerview.widget.RecyclerView;
-
 /**
  * Do diff on widget's tray list items and call the {@link RecyclerView.Adapter}
  * methods accordingly.
@@ -137,7 +137,7 @@
     }
 
     private boolean isSamePackageItemInfo(PackageItemInfo curInfo, PackageItemInfo newInfo) {
-        return curInfo.iconBitmap.equals(newInfo.iconBitmap) &&
-                !mIconCache.isDefaultIcon(curInfo.iconBitmap, curInfo.user);
+        return curInfo.bitmap.icon.equals(newInfo.bitmap.icon)
+                && !mIconCache.isDefaultIcon(curInfo.bitmap, curInfo.user);
     }
 }
diff --git a/tests/src/com/android/launcher3/model/LoaderCursorTest.java b/tests/src/com/android/launcher3/model/LoaderCursorTest.java
index 7029ad5..0dcfaa8 100644
--- a/tests/src/com/android/launcher3/model/LoaderCursorTest.java
+++ b/tests/src/com/android/launcher3/model/LoaderCursorTest.java
@@ -142,7 +142,7 @@
         when(mMockIconCache.getDefaultIcon(eq(mLoaderCursor.user)))
                 .thenReturn(BitmapInfo.fromBitmap(icon));
         WorkspaceItemInfo info = mLoaderCursor.loadSimpleWorkspaceItem();
-        assertEquals(icon, info.iconBitmap);
+        assertEquals(icon, info.bitmap.icon);
         assertEquals("my-shortcut", info.title);
         assertEquals(ITEM_TYPE_SHORTCUT, info.itemType);
     }
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/TaplTestsLauncher3.java b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
index 29da0fa..191b405 100644
--- a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
+++ b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
@@ -356,9 +356,9 @@
      * Custom shortcuts are replaced by deep shortcuts after api 25.
      */
     @Test
-    @Ignore("Temporarily disabled to unblock merging to master")
     @PortraitLandscape
     public void testDragCustomShortcut() {
+        if (!TestHelpers.isInLauncherProcess()) return;     // b/143725213
         mLauncher.getWorkspace().openAllWidgets()
                 .getWidget("com.android.launcher3.testcomponent.CustomShortcutConfigActivity")
                 .dragToWorkspace();
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/src/com/android/launcher3/util/rule/TestStabilityRule.java b/tests/src/com/android/launcher3/util/rule/TestStabilityRule.java
index 08fa098..858cb38 100644
--- a/tests/src/com/android/launcher3/util/rule/TestStabilityRule.java
+++ b/tests/src/com/android/launcher3/util/rule/TestStabilityRule.java
@@ -21,6 +21,7 @@
 import android.os.Build;
 import android.util.Log;
 
+import androidx.test.InstrumentationRegistry;
 import androidx.test.uiautomator.UiDevice;
 
 import org.junit.rules.TestRule;
@@ -86,6 +87,19 @@
     }
 
     private static int getRunFlavor() {
+        final String flavorOverride = InstrumentationRegistry.getArguments().getString("flavor");
+
+        if (flavorOverride != null) {
+            Log.d(TAG, "Flavor override: " + flavorOverride);
+            try {
+                return (int) TestStabilityRule.class.getField(flavorOverride).get(null);
+            } catch (NoSuchFieldException e) {
+                throw new AssertionError("Unrecognized run flavor override: " + flavorOverride);
+            } catch (IllegalAccessException e) {
+                throw new RuntimeException(e);
+            }
+        }
+
         final String launcherVersion;
         try {
             launcherVersion = getInstrumentation().
diff --git a/tests/src/com/android/launcher3/widget/WidgetsListAdapterTest.java b/tests/src/com/android/launcher3/widget/WidgetsListAdapterTest.java
index a31d8a6..c7f7cd6 100644
--- a/tests/src/com/android/launcher3/widget/WidgetsListAdapterTest.java
+++ b/tests/src/com/android/launcher3/widget/WidgetsListAdapterTest.java
@@ -23,16 +23,19 @@
 import android.appwidget.AppWidgetProviderInfo;
 import android.content.Context;
 import android.graphics.Bitmap;
+import android.view.LayoutInflater;
+
+import androidx.recyclerview.widget.RecyclerView;
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
-import android.view.LayoutInflater;
 
-import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.WidgetPreviewLoader;
 import com.android.launcher3.compat.AppWidgetManagerCompat;
+import com.android.launcher3.icons.BitmapInfo;
+import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.model.PackageItemInfo;
 import com.android.launcher3.model.WidgetItem;
 import com.android.launcher3.util.MultiHashMap;
@@ -46,8 +49,6 @@
 import java.util.ArrayList;
 import java.util.Map;
 
-import androidx.recyclerview.widget.RecyclerView;
-
 @SmallTest
 @RunWith(AndroidJUnit4.class)
 public class WidgetsListAdapterTest {
@@ -136,7 +137,7 @@
             PackageItemInfo pInfo = new PackageItemInfo(wi.componentName.getPackageName());
             pInfo.title = pInfo.packageName;
             pInfo.user = wi.user;
-            pInfo.iconBitmap = Bitmap.createBitmap(10, 10, Bitmap.Config.ALPHA_8);
+            pInfo.bitmap = BitmapInfo.of(Bitmap.createBitmap(10, 10, Bitmap.Config.ALPHA_8), 0);
             newMap.addToList(pInfo, wi);
             if (newMap.size() == num) {
                 break;
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();