Merge "Fix AA+ decorator drawing out of bounds issue / Make AA+ opaque" into sc-dev
diff --git a/OWNERS b/OWNERS
index 1d6ad8c..daad057 100644
--- a/OWNERS
+++ b/OWNERS
@@ -4,6 +4,13 @@
 # People who can approve changes for submission
 #
 
+petrcermak@google.com
+pbdr@google.com
+kideckel@google.com
+stevenckng@google.com
+ydixit@google.com
+boadway@google.com
+alinazaidi@google.com
 adamcohen@google.com
 hyunyoungs@google.com
 mrcasey@google.com
diff --git a/quickstep/res/drawable/task_menu_bg.xml b/quickstep/res/drawable/task_menu_bg.xml
index 7334d98..a60defc 100644
--- a/quickstep/res/drawable/task_menu_bg.xml
+++ b/quickstep/res/drawable/task_menu_bg.xml
@@ -14,25 +14,7 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
-    <item android:gravity="bottom">
-        <!-- Shadow -->
-        <shape>
-            <gradient android:angle="270"
-                android:endColor="@android:color/transparent"
-                android:startColor="#26000000" />
-            <size android:height="@dimen/task_card_menu_shadow_height" />
-        </shape>
-    </item>
-    <item android:bottom="@dimen/task_card_menu_shadow_height">
-        <!-- Background -->
-        <shape>
-            <corners
-                android:topLeftRadius="?android:attr/dialogCornerRadius"
-                android:topRightRadius="?android:attr/dialogCornerRadius"
-                android:bottomLeftRadius="0dp"
-                android:bottomRightRadius="0dp" />
-            <solid android:color="?attr/popupColorPrimary" />
-        </shape>
-    </item>
-</layer-list>
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <solid android:color="?attr/popupColorPrimary" />
+</shape>
diff --git a/quickstep/res/layout/taskbar.xml b/quickstep/res/layout/taskbar.xml
index b124b33..84e2304 100644
--- a/quickstep/res/layout/taskbar.xml
+++ b/quickstep/res/layout/taskbar.xml
@@ -25,7 +25,6 @@
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:background="@color/taskbar_background"
-        android:gravity="center"
-        android:animateLayoutChanges="true"/>
+        android:gravity="center"/>
 
 </com.android.launcher3.taskbar.TaskbarContainerView>
\ No newline at end of file
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index 9773366..2a24624 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -16,20 +16,30 @@
 
 <resources>
 
-    <dimen name="task_thumbnail_top_margin">24dp</dimen>
-    <dimen name="task_thumbnail_half_top_margin">12dp</dimen>
+    <dimen name="task_thumbnail_top_margin">80dp</dimen>
+    <dimen name="task_thumbnail_half_top_margin">40dp</dimen>
     <dimen name="task_thumbnail_icon_size">48dp</dimen>
-    <dimen name="task_icon_top_margin">-16dp</dimen>
+    <dimen name="task_icon_top_margin">16dp</dimen>
     <!-- For screens without rounded corners -->
     <dimen name="task_corner_radius_small">2dp</dimen>
 
+    <dimen name="overview_proactive_row_height">48dp</dimen>
+    <dimen name="overview_proactive_row_bottom_margin">16dp</dimen>
+
+    <dimen name="overview_minimum_next_prev_size">48dp</dimen>
+    <dimen name="overview_task_margin">16dp</dimen>
+
     <!-- Overrideable in overlay that provides the Overview Actions. -->
-    <dimen name="overview_actions_height">66dp</dimen>
-    <dimen name="overview_actions_bottom_margin_gesture">16dp</dimen>
+    <dimen name="overview_actions_height">48dp</dimen>
+    <dimen name="overview_actions_bottom_margin_gesture">12dp</dimen>
     <dimen name="overview_actions_bottom_margin_three_button">8dp</dimen>
     <dimen name="overview_actions_horizontal_margin">16dp</dimen>
 
-    <dimen name="recents_row_spacing">48dp</dimen>
+    <dimen name="overview_grid_top_margin">77dp</dimen>
+    <dimen name="overview_grid_bottom_margin">90dp</dimen>
+    <dimen name="overview_grid_side_margin">54dp</dimen>
+    <dimen name="overview_grid_row_spacing">42dp</dimen>
+
     <dimen name="recents_page_spacing">16dp</dimen>
     <dimen name="recents_clear_all_deadzone_vertical_margin">70dp</dimen>
 
@@ -59,10 +69,6 @@
     <dimen name="task_card_menu_option_vertical_padding">8dp</dimen>
     <dimen name="task_card_menu_shadow_height">3dp</dimen>
     <dimen name="task_card_menu_horizontal_padding">0dp</dimen>
-    <dimen name="portrait_task_card_horz_space_big_overview">132dp</dimen>
-    <dimen name="portrait_modal_task_card_horz_space">60dp</dimen>
-    <dimen name="landscape_task_card_horz_space">200dp</dimen>
-    <dimen name="multi_window_task_card_horz_space">100dp</dimen>
     <!-- Copied from framework resource:
        docked_stack_divider_thickness - 2 * docked_stack_divider_insets -->
     <dimen name="multi_window_task_divider_size">10dp</dimen>
diff --git a/quickstep/res/values/override.xml b/quickstep/res/values/override.xml
index 605774d..705ec9d 100644
--- a/quickstep/res/values/override.xml
+++ b/quickstep/res/values/override.xml
@@ -17,8 +17,6 @@
 <!-- Class overrides for launcher with quickstep. -->
 
 <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-  <string name="app_transition_manager_class" translatable="false">com.android.launcher3.LauncherAppTransitionManagerImpl</string>
-
   <string name="instant_app_resolver_class" translatable="false">com.android.quickstep.InstantAppResolverImpl</string>
 
   <string name="app_launch_tracker_class" translatable="false">com.android.launcher3.appprediction.PredictionAppTracker</string>
diff --git a/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java b/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
index 56d25f2..cdfd1a2 100644
--- a/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
@@ -21,11 +21,11 @@
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.util.DisplayController.DisplayHolder.CHANGE_SIZE;
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
+import static com.android.quickstep.SysUINavigationMode.Mode.TWO_BUTTONS;
 import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_HOME_KEY;
 
 import android.animation.AnimatorSet;
 import android.animation.ValueAnimator;
-import android.app.ActivityOptions;
 import android.content.Intent;
 import android.content.IntentSender;
 import android.os.Bundle;
@@ -47,6 +47,7 @@
 import com.android.launcher3.taskbar.TaskbarController;
 import com.android.launcher3.taskbar.TaskbarStateHandler;
 import com.android.launcher3.uioverrides.RecentsViewStateController;
+import com.android.launcher3.util.ActivityOptionsWrapper;
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.UiThreadHelper;
 import com.android.quickstep.RecentsModel;
@@ -73,12 +74,13 @@
         implements NavigationModeChangeListener {
 
     private DepthController mDepthController = new DepthController(this);
+    private QuickstepTransitionManager mAppTransitionManager;
 
     /**
      * Reusable command for applying the back button alpha on the background thread.
      */
     public static final UiThreadHelper.AsyncCommand SET_BACK_BUTTON_ALPHA =
-            (context, arg1, arg2) -> SystemUiProxy.INSTANCE.get(context).setBackButtonAlpha(
+            (context, arg1, arg2) -> SystemUiProxy.INSTANCE.get(context).setNavBarButtonAlpha(
                     Float.intBitsToFloat(arg1), arg2 != 0);
 
     private OverviewActionsView mActionsView;
@@ -86,11 +88,13 @@
     private @Nullable TaskbarController mTaskbarController;
     private final TaskbarStateHandler mTaskbarStateHandler = new TaskbarStateHandler(this);
     // Will be updated when dragging from taskbar.
-    private DragOptions mWorkspaceDragOptions = new DragOptions();
+    private @Nullable DragOptions mNextWorkspaceDragOptions = null;
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+        mAppTransitionManager = new QuickstepTransitionManager(this);
+        mAppTransitionManager.registerRemoteAnimations();
 
         SysUINavigationMode.INSTANCE.get(this).addModeChangeListener(this);
         addMultiWindowModeChangedListener(mDepthController);
@@ -98,8 +102,9 @@
 
     @Override
     public void onDestroy() {
-        SysUINavigationMode.INSTANCE.get(this).removeModeChangeListener(this);
+        mAppTransitionManager.onActivityDestroyed();
 
+        SysUINavigationMode.INSTANCE.get(this).removeModeChangeListener(this);
         if (mTaskbarController != null) {
             mTaskbarController.cleanup();
         }
@@ -107,6 +112,10 @@
         super.onDestroy();
     }
 
+    public QuickstepTransitionManager getAppTransitionManager() {
+        return mAppTransitionManager;
+    }
+
     @Override
     public void onNavigationModeChanged(Mode newMode) {
         getDragLayer().recreateControllers();
@@ -270,19 +279,29 @@
         return mTaskbarController != null && mTaskbarController.isViewInTaskbar(v);
     }
 
-    @Override
-    public DragOptions getDefaultWorkspaceDragOptions() {
-        return mWorkspaceDragOptions;
+    public boolean supportsAdaptiveIconAnimation(View clickedView) {
+        return mAppTransitionManager.hasControlRemoteAppTransitionPermission()
+                && FeatureFlags.ADAPTIVE_ICON_WINDOW_ANIM.get()
+                && !isViewInTaskbar(clickedView);
     }
 
-    public void setWorkspaceDragOptions(DragOptions dragOptions) {
-        mWorkspaceDragOptions = dragOptions;
+    @Override
+    public DragOptions getDefaultWorkspaceDragOptions() {
+        if (mNextWorkspaceDragOptions != null) {
+            DragOptions options = mNextWorkspaceDragOptions;
+            mNextWorkspaceDragOptions = null;
+            return options;
+        }
+        return super.getDefaultWorkspaceDragOptions();
+    }
+
+    public void setNextWorkspaceDragOptions(DragOptions dragOptions) {
+        mNextWorkspaceDragOptions = dragOptions;
     }
 
     @Override
     public void useFadeOutAnimationForLauncherStart(CancellationSignal signal) {
-        QuickstepAppTransitionManagerImpl appTransitionManager =
-                (QuickstepAppTransitionManagerImpl) getAppTransitionManager();
+        QuickstepTransitionManager appTransitionManager = getAppTransitionManager();
         appTransitionManager.setRemoteAnimationProvider(new RemoteAnimationProvider() {
             @Override
             public AnimatorSet createWindowAnimation(RemoteAnimationTargetCompat[] appTargets,
@@ -310,6 +329,10 @@
     @Override
     public void onDragLayerHierarchyChanged() {
         onLauncherStateOrFocusChanged();
+
+        if (mTaskbarController != null) {
+            mTaskbarController.onLauncherDragLayerHierarchyChanged();
+        }
     }
 
     @Override
@@ -351,8 +374,10 @@
      */
     private void onLauncherStateOrFocusChanged() {
         boolean shouldBackButtonBeHidden = shouldBackButtonBeHidden(getStateManager().getState());
-        UiThreadHelper.setBackButtonAlphaAsync(this, SET_BACK_BUTTON_ALPHA,
-                shouldBackButtonBeHidden ? 0f : 1f, true /* animate */);
+        if (SysUINavigationMode.getMode(this) == TWO_BUTTONS) {
+            UiThreadHelper.setBackButtonAlphaAsync(this, SET_BACK_BUTTON_ALPHA,
+                    shouldBackButtonBeHidden ? 0f : 1f, true /* animate */);
+        }
         if (getDragLayer() != null) {
             getRootView().setDisallowBackGesture(shouldBackButtonBeHidden);
         }
@@ -374,10 +399,14 @@
     }
 
     @Override
-    public ActivityOptions getActivityLaunchOptions(View v) {
-        ActivityOptions activityOptions = super.getActivityLaunchOptions(v);
-        if (activityOptions != null && mLastTouchUpTime > 0) {
-            ActivityOptionsCompat.setLauncherSourceInfo(activityOptions, mLastTouchUpTime);
+    public ActivityOptionsWrapper getActivityLaunchOptions(View v) {
+        ActivityOptionsWrapper activityOptions =
+                mAppTransitionManager.hasControlRemoteAppTransitionPermission()
+                        ? mAppTransitionManager.getActivityLaunchOptions(this, v)
+                        : super.getActivityLaunchOptions(v);
+        if (mLastTouchUpTime > 0) {
+            ActivityOptionsCompat.setLauncherSourceInfo(
+                    activityOptions.options, mLastTouchUpTime);
         }
         return activityOptions;
     }
diff --git a/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java b/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java
index 588d676..be98157 100644
--- a/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java
+++ b/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java
@@ -17,6 +17,7 @@
 
 import static com.android.launcher3.Utilities.postAsyncCallback;
 import static com.android.launcher3.util.DisplayController.getSingleFrameMs;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 import static com.android.systemui.shared.recents.utilities.Utilities.postAtFrontOfQueueAsynchronously;
 
@@ -29,6 +30,7 @@
 import android.os.Handler;
 
 import androidx.annotation.BinderThread;
+import androidx.annotation.Nullable;
 import androidx.annotation.UiThread;
 
 import com.android.systemui.shared.system.RemoteAnimationRunnerCompat;
@@ -37,8 +39,6 @@
 @TargetApi(Build.VERSION_CODES.P)
 public abstract class LauncherAnimationRunner implements RemoteAnimationRunnerCompat {
 
-    private static final String TAG = "LauncherAnimationRunner";
-
     private final Handler mHandler;
     private final boolean mStartAtFrontOfQueue;
     private AnimationResult mAnimationResult;
@@ -66,10 +66,7 @@
             Runnable runnable) {
         Runnable r = () -> {
             finishExistingAnimation();
-            mAnimationResult = new AnimationResult(() -> {
-                UI_HELPER_EXECUTOR.execute(runnable);
-                mAnimationResult = null;
-            });
+            mAnimationResult = new AnimationResult(() -> mAnimationResult = null, runnable);
             onCreateAnimation(transit, appTargets, wallpaperTargets, nonAppTargets,
                     mAnimationResult);
         };
@@ -126,37 +123,60 @@
 
     public static final class AnimationResult {
 
-        private final Runnable mFinishRunnable;
+        private final Runnable mSyncFinishRunnable;
+        private final Runnable mASyncFinishRunnable;
 
         private AnimatorSet mAnimator;
+        private Runnable mOnCompleteCallback;
         private boolean mFinished = false;
         private boolean mInitialized = false;
 
-        private AnimationResult(Runnable finishRunnable) {
-            mFinishRunnable = finishRunnable;
+        private AnimationResult(Runnable syncFinishRunnable, Runnable asyncFinishRunnable) {
+            mSyncFinishRunnable = syncFinishRunnable;
+            mASyncFinishRunnable = asyncFinishRunnable;
         }
 
         @UiThread
         private void finish() {
             if (!mFinished) {
-                mFinishRunnable.run();
+                mSyncFinishRunnable.run();
+                UI_HELPER_EXECUTOR.execute(() -> {
+                    mASyncFinishRunnable.run();
+                    if (mOnCompleteCallback != null) {
+                        MAIN_EXECUTOR.execute(mOnCompleteCallback);
+                    }
+                });
                 mFinished = true;
             }
         }
 
         @UiThread
         public void setAnimation(AnimatorSet animation, Context context) {
+            setAnimation(animation, context, null);
+
+        }
+
+        /**
+         * Sets the animation to play for this app launch
+         */
+        @UiThread
+        public void setAnimation(AnimatorSet animation, Context context,
+                @Nullable Runnable onCompleteCallback) {
             if (mInitialized) {
                 throw new IllegalStateException("Animation already initialized");
             }
             mInitialized = true;
             mAnimator = animation;
+            mOnCompleteCallback = onCompleteCallback;
             if (mAnimator == null) {
                 finish();
             } else if (mFinished) {
                 // Animation callback was already finished, skip the animation.
                 mAnimator.start();
                 mAnimator.end();
+                if (mOnCompleteCallback != null) {
+                    mOnCompleteCallback.run();
+                }
             } else {
                 // Start the animation
                 mAnimator.addListener(new AnimatorListenerAdapter() {
diff --git a/quickstep/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java b/quickstep/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java
deleted file mode 100644
index ae4bd96..0000000
--- a/quickstep/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * Copyright (C) 2018 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.anim.Interpolators.AGGRESSIVE_EASE;
-import static com.android.launcher3.anim.Interpolators.LINEAR;
-import static com.android.quickstep.TaskViewUtils.findTaskViewToLaunch;
-
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
-import android.content.Context;
-import android.view.View;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import com.android.quickstep.TaskViewUtils;
-import com.android.quickstep.views.RecentsView;
-import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
-
-/**
- * A {@link QuickstepAppTransitionManagerImpl} that also implements recents transitions from
- * {@link RecentsView}.
- */
-public final class LauncherAppTransitionManagerImpl extends QuickstepAppTransitionManagerImpl {
-
-    public LauncherAppTransitionManagerImpl(Context context) {
-        super(context);
-    }
-
-    @Override
-    protected boolean isLaunchingFromRecents(@NonNull View v,
-            @Nullable RemoteAnimationTargetCompat[] targets) {
-        return mLauncher.getStateManager().getState().overviewUi
-                && findTaskViewToLaunch(mLauncher.getOverviewPanel(), v, targets) != null;
-    }
-
-    @Override
-    protected void composeRecentsLaunchAnimator(@NonNull AnimatorSet anim, @NonNull View v,
-            @NonNull RemoteAnimationTargetCompat[] appTargets,
-            @NonNull RemoteAnimationTargetCompat[] wallpaperTargets, boolean launcherClosing) {
-        TaskViewUtils.composeRecentsLaunchAnimator(anim, v, appTargets, wallpaperTargets,
-                launcherClosing, mLauncher.getStateManager(), mLauncher.getOverviewPanel(),
-                mLauncher.getDepthController());
-    }
-
-    @Override
-    protected Runnable composeViewContentAnimator(@NonNull AnimatorSet anim, float[] alphas,
-            float[] trans) {
-        RecentsView overview = mLauncher.getOverviewPanel();
-        ObjectAnimator alpha = ObjectAnimator.ofFloat(overview,
-                RecentsView.CONTENT_ALPHA, alphas);
-        alpha.setDuration(CONTENT_ALPHA_DURATION);
-        alpha.setInterpolator(LINEAR);
-        anim.play(alpha);
-        overview.setFreezeViewVisibility(true);
-
-        ObjectAnimator transY = ObjectAnimator.ofFloat(overview, View.TRANSLATION_Y, trans);
-        transY.setInterpolator(AGGRESSIVE_EASE);
-        transY.setDuration(CONTENT_TRANSLATION_DURATION);
-        anim.play(transY);
-
-        return () -> {
-            overview.setFreezeViewVisibility(false);
-            overview.setTranslationY(0);
-            mLauncher.getStateManager().reapplyState();
-        };
-    }
-}
diff --git a/quickstep/src/com/android/launcher3/LauncherInitListener.java b/quickstep/src/com/android/launcher3/LauncherInitListener.java
index 7fb0d43..5fc79f0 100644
--- a/quickstep/src/com/android/launcher3/LauncherInitListener.java
+++ b/quickstep/src/com/android/launcher3/LauncherInitListener.java
@@ -46,8 +46,8 @@
     @Override
     public boolean handleInit(Launcher launcher, boolean alreadyOnHome) {
         if (mRemoteAnimationProvider != null) {
-            QuickstepAppTransitionManagerImpl appTransitionManager =
-                    (QuickstepAppTransitionManagerImpl) launcher.getAppTransitionManager();
+            QuickstepTransitionManager appTransitionManager =
+                    ((BaseQuickstepLauncher) launcher).getAppTransitionManager();
 
             // Set a one-time animation provider. After the first call, this will get cleared.
             // TODO: Probably also check the intended target id.
diff --git a/quickstep/src/com/android/launcher3/QuickstepAppTransitionManagerImpl.java b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
similarity index 88%
rename from quickstep/src/com/android/launcher3/QuickstepAppTransitionManagerImpl.java
rename to quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
index c4b6961..f04c58d 100644
--- a/quickstep/src/com/android/launcher3/QuickstepAppTransitionManagerImpl.java
+++ b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
@@ -16,6 +16,9 @@
 
 package com.android.launcher3;
 
+import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_NONE;
+import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_SPLASH_SCREEN;
+
 import static com.android.launcher3.BaseActivity.INVISIBLE_ALL;
 import static com.android.launcher3.BaseActivity.INVISIBLE_BY_APP_TRANSITIONS;
 import static com.android.launcher3.BaseActivity.INVISIBLE_BY_PENDING_FLAGS;
@@ -34,6 +37,7 @@
 import static com.android.launcher3.dragndrop.DragLayer.ALPHA_INDEX_TRANSITIONS;
 import static com.android.launcher3.statehandlers.DepthController.DEPTH;
 import static com.android.quickstep.TaskUtils.taskIsATargetWithMode;
+import static com.android.quickstep.TaskViewUtils.findTaskViewToLaunch;
 import static com.android.systemui.shared.system.QuickStepContract.getWindowCornerRadius;
 import static com.android.systemui.shared.system.QuickStepContract.supportsRoundedCornersOnWindows;
 import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING;
@@ -44,7 +48,6 @@
 import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
 import android.animation.ValueAnimator;
-import android.app.ActivityOptions;
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.content.res.Resources;
@@ -66,20 +69,23 @@
 import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener;
 import com.android.launcher3.allapps.AllAppsTransitionController;
 import com.android.launcher3.anim.AnimationSuccessListener;
-import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.dragndrop.DragLayer;
 import com.android.launcher3.shortcuts.DeepShortcutView;
 import com.android.launcher3.statehandlers.DepthController;
+import com.android.launcher3.util.ActivityOptionsWrapper;
 import com.android.launcher3.util.DynamicResource;
 import com.android.launcher3.util.MultiValueAlpha;
 import com.android.launcher3.util.MultiValueAlpha.AlphaProperty;
+import com.android.launcher3.util.RunnableList;
 import com.android.launcher3.views.FloatingIconView;
 import com.android.quickstep.RemoteAnimationTargets;
 import com.android.quickstep.SystemUiProxy;
+import com.android.quickstep.TaskViewUtils;
 import com.android.quickstep.util.MultiValueUpdateListener;
 import com.android.quickstep.util.RemoteAnimationProvider;
 import com.android.quickstep.util.StaggeredWorkspaceAnim;
 import com.android.quickstep.util.SurfaceTransactionApplier;
+import com.android.quickstep.views.RecentsView;
 import com.android.systemui.shared.system.ActivityCompat;
 import com.android.systemui.shared.system.ActivityOptionsCompat;
 import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
@@ -91,14 +97,14 @@
 import com.android.systemui.shared.system.RemoteTransitionCompat;
 import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat.SurfaceParams;
 import com.android.systemui.shared.system.WindowManagerWrapper;
+import com.android.wm.shell.startingsurface.IStartingWindowListener;
+
+import java.util.LinkedHashMap;
 
 /**
- * {@link LauncherAppTransitionManager} with Quickstep-specific app transitions for launching from
- * home and/or all-apps.  Not used for 3p launchers.
+ * Manages the opening and closing app transitions from Launcher
  */
-@SuppressWarnings("unused")
-public abstract class QuickstepAppTransitionManagerImpl extends LauncherAppTransitionManager
-        implements OnDeviceProfileChangeListener {
+public class QuickstepTransitionManager implements OnDeviceProfileChangeListener {
 
     private static final String TAG = "QuickstepTransition";
 
@@ -109,8 +115,8 @@
     public static final int STATUS_BAR_TRANSITION_DURATION = 120;
 
     /**
-     * Since our animations decelerate heavily when finishing, we want to start status bar animations
-     * x ms before the ending.
+     * Since our animations decelerate heavily when finishing, we want to start status bar
+     * animations x ms before the ending.
      */
     public static final int STATUS_BAR_TRANSITION_PRE_DELAY = 96;
 
@@ -145,6 +151,8 @@
     // Progress = 0: All apps is fully pulled up, Progress = 1: All apps is fully pulled down.
     public static final float ALL_APPS_PROGRESS_OFF_SCREEN = 1.3059858f;
 
+    private static final int MAX_NUM_TASKS = 5;
+
     protected final BaseQuickstepLauncher mLauncher;
 
     private final DragLayer mDragLayer;
@@ -180,7 +188,10 @@
         }
     };
 
-    public QuickstepAppTransitionManagerImpl(Context context) {
+    // Will never be larger than MAX_NUM_TASKS
+    private LinkedHashMap<Integer, Integer> mTypeForTaskId;
+
+    public QuickstepTransitionManager(Context context) {
         mLauncher = Launcher.cast(Launcher.getLauncher(context));
         mDragLayer = mLauncher.getDragLayer();
         mDragLayerAlpha = mDragLayer.getAlphaProperty(ALPHA_INDEX_TRANSITIONS);
@@ -194,6 +205,23 @@
         mMaxShadowRadius = res.getDimensionPixelSize(R.dimen.max_shadow_radius);
 
         mLauncher.addOnDeviceProfileChangeListener(this);
+
+        if (supportsSSplashScreen()) {
+            mTypeForTaskId = new LinkedHashMap<Integer, Integer>(MAX_NUM_TASKS) {
+                @Override
+                protected boolean removeEldestEntry(Entry<Integer, Integer> entry) {
+                    return size() > MAX_NUM_TASKS;
+                }
+            };
+
+            SystemUiProxy.INSTANCE.get(mLauncher).setStartingWindowListener(
+                    new IStartingWindowListener.Stub() {
+                        @Override
+                        public void onTaskLaunching(int taskId, int supportedType) {
+                            mTypeForTaskId.put(taskId, supportedType);
+                        }
+                    });
+        }
     }
 
     @Override
@@ -201,37 +229,29 @@
         mDeviceProfile = dp;
     }
 
-    @Override
-    public boolean supportsAdaptiveIconAnimation(View clickedView) {
-        return hasControlRemoteAppTransitionPermission()
-                && FeatureFlags.ADAPTIVE_ICON_WINDOW_ANIM.get()
-                && !mLauncher.isViewInTaskbar(clickedView);
-    }
-
     /**
      * @return ActivityOptions with remote animations that controls how the window of the opening
      *         targets are displayed.
      */
-    @Override
-    public ActivityOptions getActivityLaunchOptions(Launcher launcher, View v) {
-        if (hasControlRemoteAppTransitionPermission()) {
-            boolean fromRecents = isLaunchingFromRecents(v, null /* targets */);
-            mAppLaunchRunner = new AppLaunchAnimationRunner(mHandler, v);
-            RemoteAnimationRunnerCompat runner = new WrappedLauncherAnimationRunner<>(
-                    mAppLaunchRunner, true /* startAtFrontOfQueue */);
+    public ActivityOptionsWrapper getActivityLaunchOptions(Launcher launcher, View v) {
+        boolean fromRecents = isLaunchingFromRecents(v, null /* targets */);
+        RunnableList onEndCallback = new RunnableList();
+        mAppLaunchRunner = new AppLaunchAnimationRunner(mHandler, v, onEndCallback);
+        RemoteAnimationRunnerCompat runner = new WrappedLauncherAnimationRunner<>(
+                mHandler, mAppLaunchRunner, true /* startAtFrontOfQueue */);
 
-            // Note that this duration is a guess as we do not know if the animation will be a
-            // recents launch or not for sure until we know the opening app targets.
-            long duration = fromRecents
-                    ? RECENTS_LAUNCH_DURATION
-                    : APP_LAUNCH_DURATION;
+        // Note that this duration is a guess as we do not know if the animation will be a
+        // recents launch or not for sure until we know the opening app targets.
+        long duration = fromRecents
+                ? RECENTS_LAUNCH_DURATION
+                : APP_LAUNCH_DURATION;
 
-            long statusBarTransitionDelay = duration - STATUS_BAR_TRANSITION_DURATION
-                    - STATUS_BAR_TRANSITION_PRE_DELAY;
-            return ActivityOptionsCompat.makeRemoteAnimation(new RemoteAnimationAdapterCompat(
-                    runner, duration, statusBarTransitionDelay));
-        }
-        return super.getActivityLaunchOptions(launcher, v);
+        long statusBarTransitionDelay = duration - STATUS_BAR_TRANSITION_DURATION
+                - STATUS_BAR_TRANSITION_PRE_DELAY;
+        RemoteAnimationAdapterCompat adapterCompat =
+                new RemoteAnimationAdapterCompat(runner, duration, statusBarTransitionDelay);
+        return new ActivityOptionsWrapper(
+                ActivityOptionsCompat.makeRemoteAnimation(adapterCompat), onEndCallback);
     }
 
     /**
@@ -244,8 +264,11 @@
      * @param targets apps that are opening/closing
      * @return true if the app is launching from recents, false if it most likely is not
      */
-    protected abstract boolean isLaunchingFromRecents(@NonNull View v,
-            @Nullable RemoteAnimationTargetCompat[] targets);
+    protected boolean isLaunchingFromRecents(@NonNull View v,
+            @Nullable RemoteAnimationTargetCompat[] targets) {
+        return mLauncher.getStateManager().getState().overviewUi
+                && findTaskViewToLaunch(mLauncher.getOverviewPanel(), v, targets) != null;
+    }
 
     /**
      * Composes the animations for a launch from the recents list.
@@ -255,9 +278,13 @@
      * @param appTargets the apps that are opening/closing
      * @param launcherClosing true if the launcher app is closing
      */
-    protected abstract void composeRecentsLaunchAnimator(@NonNull AnimatorSet anim, @NonNull View v,
+    protected void composeRecentsLaunchAnimator(@NonNull AnimatorSet anim, @NonNull View v,
             @NonNull RemoteAnimationTargetCompat[] appTargets,
-            @NonNull RemoteAnimationTargetCompat[] wallpaperTargets, boolean launcherClosing);
+            @NonNull RemoteAnimationTargetCompat[] wallpaperTargets, boolean launcherClosing) {
+        TaskViewUtils.composeRecentsLaunchAnimator(anim, v, appTargets, wallpaperTargets,
+                launcherClosing, mLauncher.getStateManager(), mLauncher.getOverviewPanel(),
+                mLauncher.getDepthController());
+    }
 
     private boolean areAllTargetsTranslucent(@NonNull RemoteAnimationTargetCompat[] targets) {
         boolean isAllOpeningTargetTrs = true;
@@ -336,6 +363,15 @@
         return bounds;
     }
 
+    private int getOpeningTaskId(RemoteAnimationTargetCompat[] appTargets) {
+        for (RemoteAnimationTargetCompat target : appTargets) {
+            if (target.mode == MODE_OPENING) {
+                return target.taskId;
+            }
+        }
+        return -1;
+    }
+
     public void setRemoteAnimationProvider(final RemoteAnimationProvider animationProvider,
             CancellationSignal cancellationSignal) {
         mRemoteAnimationProvider = animationProvider;
@@ -446,8 +482,27 @@
      * @param trans the translation Y values to animator to over time
      * @return listener to run when the animation ends
      */
-    protected abstract Runnable composeViewContentAnimator(@NonNull AnimatorSet anim,
-            float[] alphas, float[] trans);
+    protected Runnable composeViewContentAnimator(@NonNull AnimatorSet anim,
+            float[] alphas, float[] trans) {
+        RecentsView overview = mLauncher.getOverviewPanel();
+        ObjectAnimator alpha = ObjectAnimator.ofFloat(overview,
+                RecentsView.CONTENT_ALPHA, alphas);
+        alpha.setDuration(CONTENT_ALPHA_DURATION);
+        alpha.setInterpolator(LINEAR);
+        anim.play(alpha);
+        overview.setFreezeViewVisibility(true);
+
+        ObjectAnimator transY = ObjectAnimator.ofFloat(overview, View.TRANSLATION_Y, trans);
+        transY.setInterpolator(AGGRESSIVE_EASE);
+        transY.setDuration(CONTENT_TRANSLATION_DURATION);
+        anim.play(transY);
+
+        return () -> {
+            overview.setFreezeViewVisibility(false);
+            overview.setTranslationY(0);
+            mLauncher.getStateManager().reapplyState();
+        };
+    }
 
     /**
      * @return Animator that controls the window of the opening targets from app icons.
@@ -471,8 +526,19 @@
         int[] dragLayerBounds = new int[2];
         mDragLayer.getLocationOnScreen(dragLayerBounds);
 
+        final boolean hasSplashScreen;
+        if (supportsSSplashScreen()) {
+            int taskId = getOpeningTaskId(appTargets);
+            int type = mTypeForTaskId.getOrDefault(taskId, STARTING_WINDOW_TYPE_NONE);
+            mTypeForTaskId.remove(taskId);
+            hasSplashScreen = type == STARTING_WINDOW_TYPE_SPLASH_SCREEN;
+        } else {
+            hasSplashScreen = false;
+        }
+
         AnimOpenProperties prop = new AnimOpenProperties(mLauncher.getResources(), mDeviceProfile,
-                windowTargetBounds, launcherIconBounds, v, dragLayerBounds[0], dragLayerBounds[1]);
+                windowTargetBounds, launcherIconBounds, v, dragLayerBounds[0], dragLayerBounds[1],
+                hasSplashScreen);
         int left = (int) (prop.cropCenterXStart - prop.cropWidthStart / 2);
         int top = (int) (prop.cropCenterYStart - prop.cropHeightStart / 2);
         int right = (int) (left + prop.cropWidthStart);
@@ -562,7 +628,7 @@
                 Utilities.scaleRectFAboutCenter(tmpRectF, mIconScaleToFitScreen.value);
                 float windowTransX0 = tmpRectF.left - offsetX;
                 float windowTransY0 = tmpRectF.top - offsetY;
-                if (ENABLE_SHELL_STARTING_SURFACE) {
+                if (hasSplashScreen) {
                     windowTransX0 -= crop.left * scale;
                     windowTransY0 -= crop.top * scale;
                 }
@@ -638,7 +704,6 @@
     /**
      * Registers remote animations used when closing apps to home screen.
      */
-    @Override
     public void registerRemoteAnimations() {
         if (SEPARATE_RECENTS_ACTIVITY.get()) {
             return;
@@ -650,7 +715,7 @@
             definition.addRemoteAnimation(WindowManagerWrapper.TRANSIT_WALLPAPER_OPEN,
                     WindowManagerWrapper.ACTIVITY_TYPE_STANDARD,
                     new RemoteAnimationAdapterCompat(
-                            new WrappedLauncherAnimationRunner<>(mWallpaperOpenRunner,
+                            new WrappedLauncherAnimationRunner<>(mHandler, mWallpaperOpenRunner,
                                     false /* startAtFrontOfQueue */),
                             CLOSING_TRANSITION_DURATION_MS, 0 /* statusBarTransitionDelay */));
 
@@ -659,7 +724,8 @@
                 definition.addRemoteAnimation(
                         WindowManagerWrapper.TRANSIT_KEYGUARD_GOING_AWAY_ON_WALLPAPER,
                         new RemoteAnimationAdapterCompat(
-                                new WrappedLauncherAnimationRunner<>(mKeyguardGoingAwayRunner,
+                                new WrappedLauncherAnimationRunner<>(
+                                        mHandler, mKeyguardGoingAwayRunner,
                                         true /* startAtFrontOfQueue */),
                                 CLOSING_TRANSITION_DURATION_MS, 0 /* statusBarTransitionDelay */));
             }
@@ -671,7 +737,6 @@
     /**
      * Registers remote animations used when closing apps to home screen.
      */
-    @Override
     public void registerRemoteTransitions() {
         if (SEPARATE_RECENTS_ACTIVITY.get()) {
             return;
@@ -679,18 +744,20 @@
         if (hasControlRemoteAppTransitionPermission()) {
             mWallpaperOpenTransitionRunner = createWallpaperOpenRunner(false /* fromUnlock */);
             mLauncherOpenTransition = RemoteAnimationAdapterCompat.buildRemoteTransition(
-                    new WrappedLauncherAnimationRunner<>(mWallpaperOpenTransitionRunner,
+                    new WrappedLauncherAnimationRunner<>(mHandler, mWallpaperOpenTransitionRunner,
                             false /* startAtFrontOfQueue */));
             mLauncherOpenTransition.addHomeOpenCheck();
             SystemUiProxy.INSTANCE.getNoCreate().registerRemoteTransition(mLauncherOpenTransition);
         }
     }
 
-    /**
-     * Unregisters all remote animations.
-     */
-    @Override
-    public void unregisterRemoteAnimations() {
+    public void onActivityDestroyed() {
+        unregisterRemoteAnimations();
+        unregisterRemoteTransitions();
+        SystemUiProxy.INSTANCE.getNoCreate().setStartingWindowListener(null);
+    }
+
+    private void unregisterRemoteAnimations() {
         if (SEPARATE_RECENTS_ACTIVITY.get()) {
             return;
         }
@@ -705,8 +772,7 @@
         }
     }
 
-    @Override
-    public void unregisterRemoteTransitions() {
+    private void unregisterRemoteTransitions() {
         if (SEPARATE_RECENTS_ACTIVITY.get()) {
             return;
         }
@@ -821,7 +887,16 @@
         return closingAnimator;
     }
 
-    private boolean hasControlRemoteAppTransitionPermission() {
+    private boolean supportsSSplashScreen() {
+        return hasControlRemoteAppTransitionPermission()
+                && Utilities.ATLEAST_S
+                && ENABLE_SHELL_STARTING_SURFACE;
+    }
+
+    /**
+     * Returns true if we have permission to control remote app transisions
+     */
+    public boolean hasControlRemoteAppTransitionPermission() {
         return mLauncher.checkSelfPermission(CONTROL_REMOTE_APP_TRANSITION_PERMISSION)
                 == PackageManager.PERMISSION_GRANTED;
     }
@@ -861,11 +936,6 @@
         }
 
         @Override
-        public Handler getHandler() {
-            return mHandler;
-        }
-
-        @Override
         public void onCreateAnimation(int transit,
                 RemoteAnimationTargetCompat[] appTargets,
                 RemoteAnimationTargetCompat[] wallpaperTargets,
@@ -955,15 +1025,12 @@
 
         private final Handler mHandler;
         private final View mV;
+        private final RunnableList mOnEndCallback;
 
-        AppLaunchAnimationRunner(Handler handler, View v) {
+        AppLaunchAnimationRunner(Handler handler, View v, RunnableList onEndCallback) {
             mHandler = handler;
             mV = v;
-        }
-
-        @Override
-        public Handler getHandler() {
-            return mHandler;
+            mOnEndCallback = onEndCallback;
         }
 
         @Override
@@ -998,7 +1065,7 @@
                 anim.addListener(mForceInvisibleListener);
             }
 
-            result.setAnimation(anim, mLauncher);
+            result.setAnimation(anim, mLauncher, mOnEndCallback::executeAllAndDestroy);
         }
     }
 
@@ -1030,7 +1097,8 @@
         public final float iconAlphaStart;
 
         AnimOpenProperties(Resources r, DeviceProfile dp, Rect windowTargetBounds,
-                RectF launcherIconBounds, View view, int dragLayerLeft, int dragLayerTop) {
+                RectF launcherIconBounds, View view, int dragLayerLeft, int dragLayerTop,
+                boolean hasSplashScreen) {
             // Scale the app icon to take up the entire screen. This simplifies the math when
             // animating the app window position / scale.
             float smallestSize = Math.min(windowTargetBounds.height(), windowTargetBounds.width());
@@ -1063,7 +1131,7 @@
             alphaDuration = useUpwardAnimation ? APP_LAUNCH_ALPHA_DURATION
                     : APP_LAUNCH_ALPHA_DOWN_DURATION;
 
-            if (ENABLE_SHELL_STARTING_SURFACE) {
+            if (hasSplashScreen) {
                 iconAlphaStart = 0;
 
                 // TOOD: Share value from shell when available.
diff --git a/quickstep/src/com/android/launcher3/WrappedAnimationRunnerImpl.java b/quickstep/src/com/android/launcher3/WrappedAnimationRunnerImpl.java
index 03cc28e..16727ec 100644
--- a/quickstep/src/com/android/launcher3/WrappedAnimationRunnerImpl.java
+++ b/quickstep/src/com/android/launcher3/WrappedAnimationRunnerImpl.java
@@ -16,8 +16,6 @@
 
 package com.android.launcher3;
 
-import android.os.Handler;
-
 import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
 
 /**
@@ -25,7 +23,7 @@
  * implementation.
  */
 public interface WrappedAnimationRunnerImpl {
-    Handler getHandler();
+
     void onCreateAnimation(int transit,
             RemoteAnimationTargetCompat[] appTargets,
             RemoteAnimationTargetCompat[] wallpaperTargets,
diff --git a/quickstep/src/com/android/launcher3/WrappedLauncherAnimationRunner.java b/quickstep/src/com/android/launcher3/WrappedLauncherAnimationRunner.java
index 1e1631b..e319275 100644
--- a/quickstep/src/com/android/launcher3/WrappedLauncherAnimationRunner.java
+++ b/quickstep/src/com/android/launcher3/WrappedLauncherAnimationRunner.java
@@ -16,6 +16,8 @@
 
 package com.android.launcher3;
 
+import android.os.Handler;
+
 import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
 
 import java.lang.ref.WeakReference;
@@ -40,8 +42,9 @@
         extends LauncherAnimationRunner {
     private WeakReference<R> mImpl;
 
-    public WrappedLauncherAnimationRunner(R animationRunnerImpl, boolean startAtFrontOfQueue) {
-        super(animationRunnerImpl.getHandler(), startAtFrontOfQueue);
+    public WrappedLauncherAnimationRunner(
+            Handler handler, R animationRunnerImpl, boolean startAtFrontOfQueue) {
+        super(handler, startAtFrontOfQueue);
         mImpl = new WeakReference<>(animationRunnerImpl);
     }
 
diff --git a/quickstep/src/com/android/launcher3/statehandlers/BackButtonAlphaHandler.java b/quickstep/src/com/android/launcher3/statehandlers/BackButtonAlphaHandler.java
index 13501a4..ce94305 100644
--- a/quickstep/src/com/android/launcher3/statehandlers/BackButtonAlphaHandler.java
+++ b/quickstep/src/com/android/launcher3/statehandlers/BackButtonAlphaHandler.java
@@ -18,6 +18,7 @@
 
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.quickstep.AnimatedFloat.VALUE;
+import static com.android.quickstep.SysUINavigationMode.Mode.TWO_BUTTONS;
 
 import com.android.launcher3.BaseQuickstepLauncher;
 import com.android.launcher3.LauncherState;
@@ -30,7 +31,7 @@
 import com.android.quickstep.SystemUiProxy;
 
 /**
- * State handler for animating back button alpha
+ * State handler for animating back button alpha in two-button nav mode.
  */
 public class BackButtonAlphaHandler implements StateHandler<LauncherState> {
 
@@ -51,14 +52,11 @@
             return;
         }
 
-        if (!SysUINavigationMode.getMode(mLauncher).hasGestures) {
-            // If the nav mode is not gestural, then force back button alpha to be 1
-            UiThreadHelper.setBackButtonAlphaAsync(mLauncher,
-                    BaseQuickstepLauncher.SET_BACK_BUTTON_ALPHA, 1f, true /* animate */);
+        if (SysUINavigationMode.getMode(mLauncher) != TWO_BUTTONS) {
             return;
         }
 
-        mBackAlpha.value = SystemUiProxy.INSTANCE.get(mLauncher).getLastBackButtonAlpha();
+        mBackAlpha.value = SystemUiProxy.INSTANCE.get(mLauncher).getLastNavButtonAlpha();
         animation.setFloat(mBackAlpha, VALUE,
                 mLauncher.shouldBackButtonBeHidden(toState) ? 0 : 1, LINEAR);
     }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarContainerView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarContainerView.java
index 1e5e3e7..528f43e 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarContainerView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarContainerView.java
@@ -83,10 +83,15 @@
 
     private ViewTreeObserverWrapper.OnComputeInsetsListener createTaskbarInsetsComputer() {
         return insetsInfo -> {
-            if (getAlpha() < AlphaUpdateListener.ALPHA_CUTOFF_THRESHOLD) {
-                // We're invisible, let touches pass through us.
+            if (getAlpha() < AlphaUpdateListener.ALPHA_CUTOFF_THRESHOLD
+                    || mTaskbarView.isDraggingItem()) {
+                // We're invisible or dragging out of taskbar, let touches pass through us.
                 insetsInfo.touchableRegion.setEmpty();
                 insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_REGION);
+                // TODO(b/182234653): Shouldn't need to do this, but for the meantime, reporting
+                // that visibleInsets is empty allows DragEvents through. Setting them as completely
+                // empty reverts to default behavior, so set 1 px instead.
+                insetsInfo.visibleInsets.set(0, 0, 0, 1);
             } else {
                  // We're visible again, accept touches anywhere in our bounds.
                 insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_FRAME);
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarController.java
index 544835c..744339b 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarController.java
@@ -19,6 +19,8 @@
 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
 
+import static com.android.launcher3.AbstractFloatingView.TYPE_ALL;
+import static com.android.launcher3.AbstractFloatingView.TYPE_REPLACE_TASKBAR_WITH_HOTSEAT;
 import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.systemui.shared.system.WindowManagerWrapper.ITYPE_BOTTOM_TAPPABLE_ELEMENT;
@@ -40,8 +42,9 @@
 
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.BaseQuickstepLauncher;
+import com.android.launcher3.Hotseat;
 import com.android.launcher3.LauncherState;
-import com.android.launcher3.QuickstepAppTransitionManagerImpl;
+import com.android.launcher3.QuickstepTransitionManager;
 import com.android.launcher3.R;
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.folder.Folder;
@@ -144,22 +147,13 @@
                         ActivityManagerWrapper.getInstance().startActivityFromRecents(task.key,
                                 ActivityOptions.makeBasic());
                     } else if (tag instanceof FolderInfo) {
-                        FolderIcon folderIcon = (FolderIcon) view;
-                        Folder folder = folderIcon.getFolder();
-
-                        setTaskbarWindowFullscreen(true);
-
-                        mTaskbarContainerView.post(() -> {
-                            folder.animateOpen();
-
-                            folder.iterateOverItems((itemInfo, itemView) -> {
-                                itemView.setOnClickListener(getItemOnClickListener());
-                                itemView.setOnLongClickListener(getItemOnLongClickListener());
-                                // To play haptic when dragging, like other Taskbar items do.
-                                itemView.setHapticFeedbackEnabled(true);
-                                return false;
-                            });
-                        });
+                        if (mLauncher.hasBeenResumed()) {
+                            FolderInfo folderInfo = (FolderInfo) tag;
+                            onClickedOnFolderFromHome(folderInfo);
+                        } else {
+                            FolderIcon folderIcon = (FolderIcon) view;
+                            onClickedOnFolderInApp(folderIcon);
+                        }
                     } else {
                         ItemClickHandler.INSTANCE.onClick(view);
                     }
@@ -169,6 +163,34 @@
                 };
             }
 
+            // Open the real folder in Launcher.
+            private void onClickedOnFolderFromHome(FolderInfo folderInfo) {
+                alignRealHotseatWithTaskbar();
+
+                FolderIcon folderIcon = (FolderIcon) mLauncher.getHotseat()
+                        .getFirstItemMatch((info, v) -> info == folderInfo);
+                folderIcon.post(folderIcon::performClick);
+            }
+
+            // Open the Taskbar folder, and handle clicks on folder items.
+            private void onClickedOnFolderInApp(FolderIcon folderIcon) {
+                Folder folder = folderIcon.getFolder();
+
+                setTaskbarWindowFullscreen(true);
+
+                mTaskbarContainerView.post(() -> {
+                    folder.animateOpen();
+
+                    folder.iterateOverItems((itemInfo, itemView) -> {
+                        itemView.setOnClickListener(getItemOnClickListener());
+                        itemView.setOnLongClickListener(getItemOnLongClickListener());
+                        // To play haptic when dragging, like other Taskbar items do.
+                        itemView.setHapticFeedbackEnabled(true);
+                        return false;
+                    });
+                });
+            }
+
             @Override
             public View.OnLongClickListener getItemOnLongClickListener() {
                 return view -> {
@@ -246,6 +268,11 @@
      * Removes the Taskbar from the screen, and removes any obsolete listeners etc.
      */
     public void cleanup() {
+        if (mAnimator != null) {
+            // End this first, in case it relies on properties that are about to be cleaned up.
+            mAnimator.end();
+        }
+
         mTaskbarView.cleanup();
         mTaskbarContainerView.cleanup();
         removeFromWindowManager();
@@ -253,10 +280,6 @@
         mTaskbarVisibilityController.cleanup();
         mHotseatController.cleanup();
         mRecentsController.cleanup();
-
-        if (mAnimator != null) {
-            mAnimator.end();
-        }
     }
 
     private void removeFromWindowManager() {
@@ -298,7 +321,7 @@
      * Should be called from onResume() and onPause(), and animates the Taskbar accordingly.
      */
     public void onLauncherResumedOrPaused(boolean isResumed) {
-        long duration = QuickstepAppTransitionManagerImpl.CONTENT_ALPHA_DURATION;
+        long duration = QuickstepTransitionManager.CONTENT_ALPHA_DURATION;
         if (mAnimator != null) {
             mAnimator.cancel();
         }
@@ -306,6 +329,7 @@
             mAnimator = createAnimToLauncher(null, duration);
         } else {
             mAnimator = createAnimToApp(duration);
+            replaceTaskbarWithHotseatOrViceVersa();
         }
         mAnimator.addListener(new AnimatorListenerAdapter() {
             @Override
@@ -355,6 +379,7 @@
             @Override
             public void onAnimationStart(Animator animation) {
                 mTaskbarView.updateHotseatItemsVisibility();
+                setReplaceTaskbarWithHotseat(false);
             }
         });
         return anim.buildAnim();
@@ -451,6 +476,35 @@
                 mTaskbarView.getHeight() - hotseatBounds.bottom);
     }
 
+    /**
+     * A view was added or removed from DragLayer, check if we need to hide our hotseat copy and
+     * show the real one instead.
+     */
+    public void onLauncherDragLayerHierarchyChanged() {
+        replaceTaskbarWithHotseatOrViceVersa();
+    }
+
+    private void replaceTaskbarWithHotseatOrViceVersa() {
+        boolean replaceTaskbarWithHotseat = AbstractFloatingView.getTopOpenViewWithType(mLauncher,
+                TYPE_ALL & TYPE_REPLACE_TASKBAR_WITH_HOTSEAT) != null;
+        if (!mLauncher.hasBeenResumed()) {
+            replaceTaskbarWithHotseat = false;
+        }
+        setReplaceTaskbarWithHotseat(replaceTaskbarWithHotseat);
+    }
+
+    private void setReplaceTaskbarWithHotseat(boolean replaceTaskbarWithHotseat) {
+        Hotseat hotseat = mLauncher.getHotseat();
+        if (replaceTaskbarWithHotseat) {
+            alignRealHotseatWithTaskbar();
+            hotseat.getReplaceTaskbarAlpha().setValue(1f);
+            mTaskbarView.setHotseatViewsHidden(true);
+        } else {
+            hotseat.getReplaceTaskbarAlpha().setValue(0f);
+            mTaskbarView.setHotseatViewsHidden(false);
+        }
+    }
+
     private float getTaskbarScaleOnHome() {
         return 1f / mTaskbarContainerView.getTaskbarActivityContext().getTaskbarIconScale();
     }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragListener.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragListener.java
index 2bd5861..dc27df1 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragListener.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragListener.java
@@ -37,7 +37,6 @@
 
     private final BaseQuickstepLauncher mLauncher;
     private final ItemInfo mDraggedItem;
-    private final DragOptions mDragOptions;
     // Randomly generated id used to verify the drag event.
     private final String mId;
 
@@ -51,19 +50,20 @@
     public TaskbarDragListener(BaseQuickstepLauncher launcher, ItemInfo draggedItem) {
         mLauncher = launcher;
         mDraggedItem = draggedItem;
-        mDragOptions = new DragOptions();
-        mDragOptions.simulatedDndStartPoint = new Point();
         mId = UUID.randomUUID().toString();
     }
 
     protected void init(DragLayer dragLayer) {
         mDragLayer = dragLayer;
         mDragLayer.setOnDragListener(this);
+        // Temporarily disable haptics, as system will already play one when drag and drop starts.
+        mDragLayer.setHapticFeedbackEnabled(false);
     }
 
     private void cleanup() {
         mDragLayer.setOnDragListener(null);
-        mLauncher.setWorkspaceDragOptions(new DragOptions());
+        mLauncher.setNextWorkspaceDragOptions(null);
+        mDragLayer.setHapticFeedbackEnabled(true);
     }
 
     /**
@@ -88,8 +88,10 @@
                 cleanup();
                 return false;
             }
-            mDragOptions.simulatedDndStartPoint.set((int) dragEvent.getX(), (int) dragEvent.getY());
-            mLauncher.setWorkspaceDragOptions(mDragOptions);
+            DragOptions dragOptions = new DragOptions();
+            dragOptions.simulatedDndStartPoint = new Point((int) dragEvent.getX(),
+                    (int) dragEvent.getY());
+            mLauncher.setNextWorkspaceDragOptions(dragOptions);
             hotseatView.performLongClick();
         } else if (dragEvent.getAction() == DragEvent.ACTION_DRAG_ENDED) {
             cleanup();
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarHotseatController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarHotseatController.java
index 082343e..b1bafdb 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarHotseatController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarHotseatController.java
@@ -78,7 +78,10 @@
                 CellLayout.LayoutParams lp = (CellLayout.LayoutParams) child.getLayoutParams();
                 // Since the hotseat might be laid out vertically or horizontally, use whichever
                 // index is higher.
-                hotseatItemInfos[Math.max(lp.cellX, lp.cellY)] = itemInfo;
+                int index = Math.max(lp.cellX, lp.cellY);
+                if (0 <= index && index < hotseatItemInfos.length) {
+                    hotseatItemInfos[index] = itemInfo;
+                }
             }
         }
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
index a729e77..1d762e9 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
@@ -15,6 +15,7 @@
  */
 package com.android.launcher3.taskbar;
 
+import android.animation.LayoutTransition;
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.Canvas;
@@ -63,6 +64,7 @@
     private TaskbarController.TaskbarViewCallbacks mControllerCallbacks;
 
     // Initialized in init().
+    private LayoutTransition mLayoutTransition;
     private int mHotseatStartIndex;
     private int mHotseatEndIndex;
     private View mHotseatRecentsDivider;
@@ -76,6 +78,7 @@
     private boolean mIsDraggingItem;
     // Only non-null when the corresponding Folder is open.
     private @Nullable FolderIcon mLeaveBehindFolderIcon;
+    private boolean mIsHotseatHidden;
 
     public TaskbarView(@NonNull Context context) {
         this(context, null);
@@ -107,6 +110,9 @@
     }
 
     protected void init(int numHotseatIcons, int numRecentIcons) {
+        mLayoutTransition = new LayoutTransition();
+        setLayoutTransitionsEnabled(true);
+
         mHotseatStartIndex = 0;
         mHotseatEndIndex = mHotseatStartIndex + numHotseatIcons - 1;
         updateHotseatItems(new ItemInfo[numHotseatIcons]);
@@ -119,6 +125,10 @@
         updateRecentTasks(new Task[numRecentIcons]);
     }
 
+    private void setLayoutTransitionsEnabled(boolean enabled) {
+        setLayoutTransition(enabled ? mLayoutTransition : null);
+    }
+
     protected void cleanup() {
         removeAllViews();
     }
@@ -206,9 +216,19 @@
         }
     }
 
+    /**
+     * Hides or shows the hotseat items immediately (without layout transitions).
+     */
+    protected void setHotseatViewsHidden(boolean hidden) {
+        mIsHotseatHidden = hidden;
+        setLayoutTransitionsEnabled(false);
+        updateHotseatItemsVisibility();
+        setLayoutTransitionsEnabled(true);
+    }
+
     private void updateHotseatItemVisibility(View hotseatView) {
         if (hotseatView.getTag() != null) {
-            hotseatView.setVisibility(VISIBLE);
+            hotseatView.setVisibility(mIsHotseatHidden ? INVISIBLE : VISIBLE);
         } else {
             int oldVisibility = hotseatView.getVisibility();
             int newVisibility = mControllerCallbacks.getEmptyHotseatViewVisibility();
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index b009629..e02f2c2 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -30,7 +30,6 @@
 import static com.android.launcher3.testing.TestProtocol.HINT_STATE_ORDINAL;
 import static com.android.launcher3.testing.TestProtocol.OVERVIEW_STATE_ORDINAL;
 import static com.android.launcher3.testing.TestProtocol.QUICK_SWITCH_STATE_ORDINAL;
-import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_HOME_KEY;
 
 import android.content.Intent;
@@ -262,14 +261,13 @@
                 RecentsView rv = getOverviewPanel();
                 TaskView tasktolaunch = rv.getTaskViewAt(0);
                 if (tasktolaunch != null) {
-                    tasktolaunch.launchTask(false, success -> {
+                    tasktolaunch.launchTask(success -> {
                         if (!success) {
                             getStateManager().goToState(OVERVIEW);
-                            tasktolaunch.notifyTaskLaunchFailed(TAG);
                         } else {
                             getStateManager().moveToRestState();
                         }
-                    }, MAIN_EXECUTOR.getHandler());
+                    });
                 } else {
                     getStateManager().goToState(NORMAL);
                 }
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java b/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java
index d480b6d..1f68a04 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java
@@ -155,7 +155,7 @@
     public void onBackPressed(Launcher launcher) {
         TaskView taskView = launcher.<RecentsView>getOverviewPanel().getRunningTaskView();
         if (taskView != null) {
-            taskView.launchTask(true);
+            taskView.launchTaskAnimated();
         } else {
             super.onBackPressed(launcher);
         }
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index e0c041e..cf345e6 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -22,7 +22,7 @@
 
 import static com.android.launcher3.BaseActivity.INVISIBLE_BY_STATE_HANDLER;
 import static com.android.launcher3.BaseActivity.STATE_HANDLER_INVISIBILITY_FLAGS;
-import static com.android.launcher3.QuickstepAppTransitionManagerImpl.RECENTS_LAUNCH_DURATION;
+import static com.android.launcher3.QuickstepTransitionManager.RECENTS_LAUNCH_DURATION;
 import static com.android.launcher3.anim.Interpolators.ACCEL_DEACCEL;
 import static com.android.launcher3.anim.Interpolators.DEACCEL;
 import static com.android.launcher3.anim.Interpolators.OVERSHOOT_1_2;
@@ -1624,19 +1624,17 @@
                 mGestureState.updateLastStartedTaskId(taskId);
                 boolean hasTaskPreviouslyAppeared = mGestureState.getPreviouslyAppearedTaskIds()
                         .contains(taskId);
-                nextTask.launchTask(false /* animate */, true /* freezeTaskList */,
-                        success -> {
-                            resultCallback.accept(success);
-                            if (success) {
-                                if (hasTaskPreviouslyAppeared) {
-                                    onRestartPreviouslyAppearedTask();
-                                }
-                            } else {
-                                mActivityInterface.onLaunchTaskFailed();
-                                nextTask.notifyTaskLaunchFailed(TAG);
-                                mRecentsAnimationController.finish(true /* toRecents */, null);
-                            }
-                        }, MAIN_EXECUTOR.getHandler());
+                nextTask.launchTask(success -> {
+                    resultCallback.accept(success);
+                    if (success) {
+                        if (hasTaskPreviouslyAppeared) {
+                            onRestartPreviouslyAppearedTask();
+                        }
+                    } else {
+                        mActivityInterface.onLaunchTaskFailed();
+                        mRecentsAnimationController.finish(true /* toRecents */, null);
+                    }
+                }, true /* freezeTaskList */);
             } else {
                 mActivityInterface.onLaunchTaskFailed();
                 Toast.makeText(mContext, R.string.activity_not_available, LENGTH_SHORT).show();
diff --git a/quickstep/src/com/android/quickstep/BaseActivityInterface.java b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
index ce14197..7c1d9fa 100644
--- a/quickstep/src/com/android/quickstep/BaseActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
@@ -33,6 +33,7 @@
 import android.content.res.Resources;
 import android.graphics.Rect;
 import android.os.Build;
+import android.view.Gravity;
 import android.view.MotionEvent;
 
 import androidx.annotation.Nullable;
@@ -53,6 +54,7 @@
 import com.android.quickstep.util.AnimatorControllerWithResistance;
 import com.android.quickstep.util.SplitScreenBounds;
 import com.android.quickstep.views.RecentsView;
+import com.android.quickstep.views.TaskView;
 import com.android.systemui.shared.recents.model.ThumbnailData;
 import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
 
@@ -197,33 +199,23 @@
      */
     public final void calculateTaskSize(Context context, DeviceProfile dp, Rect outRect,
             PagedOrientationHandler orientedState) {
-        calculateTaskSize(context, dp, getExtraSpace(context, dp, orientedState), outRect);
-    }
-
-    protected abstract float getExtraSpace(Context context, DeviceProfile dp,
-            PagedOrientationHandler orientedState);
-
-    private void calculateTaskSize(Context context, DeviceProfile dp, float extraVerticalSpace,
-            Rect outRect) {
         Resources res = context.getResources();
 
-        final int paddingResId;
-        if (dp.isMultiWindowMode) {
-            paddingResId = R.dimen.multi_window_task_card_horz_space;
-        } else if (dp.isVerticalBarLayout()) {
-            paddingResId = R.dimen.landscape_task_card_horz_space;
-        } else {
-            paddingResId = R.dimen.portrait_task_card_horz_space_big_overview;
-        }
-        float paddingHorz = res.getDimension(paddingResId);
-        float paddingVert = 0;
+        int taskMargin = res.getDimensionPixelSize(R.dimen.overview_task_margin);
+        int taskIconAndMargin = res.getDimensionPixelSize(R.dimen.task_thumbnail_icon_size)
+                + res.getDimensionPixelSize(R.dimen.task_icon_top_margin);
+        int proactiveRowAndMargin = res.getDimensionPixelSize(R.dimen.overview_proactive_row_height)
+                + res.getDimensionPixelSize(R.dimen.overview_proactive_row_bottom_margin);
 
-        calculateTaskSizeInternal(context, dp, extraVerticalSpace, paddingHorz, paddingVert,
-                res.getDimension(R.dimen.task_thumbnail_top_margin), outRect);
+        calculateTaskSizeInternal(context, dp,
+                taskIconAndMargin + taskMargin,
+                proactiveRowAndMargin + getOverviewActionsHeight(context) + taskMargin,
+                res.getDimensionPixelSize(R.dimen.overview_minimum_next_prev_size) + taskMargin,
+                outRect);
     }
 
     private void calculateTaskSizeInternal(Context context, DeviceProfile dp,
-            float extraVerticalSpace, float paddingHorz, float paddingVert, float topIconMargin,
+            int claimedSpaceAbove, int claimedSpaceBelow, int minimumHorizontalPadding,
             Rect outRect) {
         float taskWidth, taskHeight;
         Rect insets = dp.getInsets();
@@ -231,52 +223,64 @@
             WindowBounds bounds = SplitScreenBounds.INSTANCE.getSecondaryWindowBounds(context);
             taskWidth = bounds.availableSize.x;
             taskHeight = bounds.availableSize.y;
-        } else {
+        } else if (TaskView.CLIP_STATUS_AND_NAV_BARS) {
             taskWidth = dp.availableWidthPx;
             taskHeight = dp.availableHeightPx;
+        } else {
+            taskWidth = dp.widthPx;
+            taskHeight = dp.heightPx;
         }
 
-        // Note this should be same as dp.availableWidthPx and dp.availableHeightPx unless
-        // we override the insets ourselves.
-        int launcherVisibleWidth = dp.widthPx - insets.left - insets.right;
-        int launcherVisibleHeight = dp.heightPx - insets.top - insets.bottom;
+        Rect potentialTaskRect = new Rect(0, 0, dp.widthPx, dp.heightPx);
+        potentialTaskRect.inset(insets.left, insets.top, insets.right, insets.bottom);
+        potentialTaskRect.inset(
+                minimumHorizontalPadding,
+                claimedSpaceAbove,
+                minimumHorizontalPadding,
+                claimedSpaceBelow);
 
-        float availableHeight = launcherVisibleHeight
-                - topIconMargin - extraVerticalSpace - paddingVert;
-        float availableWidth = launcherVisibleWidth - paddingHorz;
+        float scale = Math.min(
+                potentialTaskRect.width() / taskWidth,
+                potentialTaskRect.height() / taskHeight);
+        int outWidth = Math.round(scale * taskWidth);
+        int outHeight = Math.round(scale * taskHeight);
 
-        float scale = Math.min(availableWidth / taskWidth, availableHeight / taskHeight);
-        float outWidth = scale * taskWidth;
-        float outHeight = scale * taskHeight;
+        Gravity.apply(Gravity.CENTER, outWidth, outHeight, potentialTaskRect, outRect);
+    }
 
-        // Center in the visible space
-        float x = insets.left + (launcherVisibleWidth - outWidth) / 2;
-        float y = insets.top + Math.max(topIconMargin,
-                (launcherVisibleHeight - extraVerticalSpace - outHeight) / 2);
-        outRect.set(Math.round(x), Math.round(y),
-                Math.round(x) + Math.round(outWidth), Math.round(y) + Math.round(outHeight));
+    /**
+     * Calculates the overview grid size for the provided device configuration.
+     */
+    public final void calculateGridSize(Context context, DeviceProfile dp, Rect outRect) {
+        Resources res = context.getResources();
+        int topMargin = res.getDimensionPixelSize(R.dimen.overview_grid_top_margin);
+        int bottomMargin = res.getDimensionPixelSize(R.dimen.overview_grid_bottom_margin);
+        int sideMargin = res.getDimensionPixelSize(R.dimen.overview_grid_side_margin);
+
+        Rect insets = dp.getInsets();
+        outRect.set(0, 0, dp.widthPx, dp.heightPx);
+        outRect.inset(Math.max(insets.left, sideMargin), Math.max(insets.top, topMargin),
+                Math.max(insets.right, sideMargin), Math.max(insets.bottom, bottomMargin));
     }
 
     /**
      * Calculates the modal taskView size for the provided device configuration
      */
     public final void calculateModalTaskSize(Context context, DeviceProfile dp, Rect outRect) {
-        float paddingHorz = context.getResources().getDimension(dp.isMultiWindowMode
-                ? R.dimen.multi_window_task_card_horz_space
-                : dp.isVerticalBarLayout()
-                        ? R.dimen.landscape_task_card_horz_space
-                        : R.dimen.portrait_modal_task_card_horz_space);
-        float extraVerticalSpace = getOverviewActionsHeight(context);
-        float paddingVert = 0;
-        float topIconMargin = 0;
-        calculateTaskSizeInternal(context, dp, extraVerticalSpace, paddingHorz, paddingVert,
-                topIconMargin, outRect);
+        Resources res = context.getResources();
+        calculateTaskSizeInternal(
+                context, dp,
+                res.getDimensionPixelSize(R.dimen.overview_task_margin),
+                getOverviewActionsHeight(context)
+                        + res.getDimensionPixelSize(R.dimen.overview_task_margin),
+                res.getDimensionPixelSize(R.dimen.overview_task_margin),
+                outRect);
     }
 
-    /** Gets the space that the overview actions will take, including margins. */
-    public final float getOverviewActionsHeight(Context context) {
+    /** Gets the space that the overview actions will take, including bottom margin. */
+    public final int getOverviewActionsHeight(Context context) {
         Resources res = context.getResources();
-        float actionsBottomMargin = 0;
+        int actionsBottomMargin = 0;
         if (getMode(context) == Mode.THREE_BUTTONS) {
             actionsBottomMargin = res.getDimensionPixelSize(
                     R.dimen.overview_actions_bottom_margin_three_button);
@@ -284,9 +288,8 @@
             actionsBottomMargin = res.getDimensionPixelSize(
                     R.dimen.overview_actions_bottom_margin_gesture);
         }
-        float overviewActionsHeight = actionsBottomMargin
+        return actionsBottomMargin
                 + res.getDimensionPixelSize(R.dimen.overview_actions_height);
-        return overviewActionsHeight;
     }
 
     /**
diff --git a/quickstep/src/com/android/quickstep/FallbackActivityInterface.java b/quickstep/src/com/android/quickstep/FallbackActivityInterface.java
index 96e4f38..db290d6 100644
--- a/quickstep/src/com/android/quickstep/FallbackActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/FallbackActivityInterface.java
@@ -26,7 +26,6 @@
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.R;
 import com.android.launcher3.touch.PagedOrientationHandler;
 import com.android.quickstep.fallback.RecentsState;
 import com.android.quickstep.util.ActivityInitListener;
@@ -155,10 +154,4 @@
         }
         activity.<RecentsView>getOverviewPanel().startHome();
     }
-
-    @Override
-    protected float getExtraSpace(Context context, DeviceProfile dp,
-            PagedOrientationHandler orientationHandler) {
-        return context.getResources().getDimensionPixelSize(R.dimen.overview_actions_height);
-    }
 }
diff --git a/quickstep/src/com/android/quickstep/LauncherActivityInterface.java b/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
index 3f3e5ad..7efbfb8 100644
--- a/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
@@ -19,13 +19,10 @@
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.LauncherState.OVERVIEW;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
-import static com.android.quickstep.SysUINavigationMode.getMode;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SHOWING;
 
 import android.content.Context;
-import android.content.res.Resources;
 import android.graphics.Rect;
-import android.util.Log;
 import android.view.MotionEvent;
 
 import androidx.annotation.Nullable;
@@ -36,13 +33,11 @@
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherInitListener;
 import com.android.launcher3.LauncherState;
-import com.android.launcher3.R;
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.statehandlers.DepthController;
 import com.android.launcher3.statehandlers.DepthController.ClampedDepthProperty;
 import com.android.launcher3.statemanager.StateManager;
 import com.android.launcher3.taskbar.TaskbarController;
-import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.touch.PagedOrientationHandler;
 import com.android.quickstep.GestureState.GestureEndTarget;
 import com.android.quickstep.SysUINavigationMode.Mode;
@@ -263,27 +258,6 @@
     }
 
     @Override
-    protected float getExtraSpace(Context context, DeviceProfile dp,
-            PagedOrientationHandler orientationHandler) {
-        Resources res = context.getResources();
-        //TODO: this needs to account for the swipe gesture height and accessibility
-        // UI when shown.
-        float actionsBottomMargin = 0;
-        if (!dp.isVerticalBarLayout()) {
-            if (getMode(context) == Mode.THREE_BUTTONS) {
-                actionsBottomMargin = res.getDimensionPixelSize(
-                    R.dimen.overview_actions_bottom_margin_three_button);
-            } else {
-                actionsBottomMargin = res.getDimensionPixelSize(
-                    R.dimen.overview_actions_bottom_margin_gesture);
-            }
-        }
-        float actionsHeight = actionsBottomMargin
-                + res.getDimensionPixelSize(R.dimen.overview_actions_height);
-        return actionsHeight;
-    }
-
-    @Override
     void onOverviewServiceBound() {
         final BaseQuickstepLauncher activity = getCreatedActivity();
         if (activity == null) return;
diff --git a/quickstep/src/com/android/quickstep/OverviewCommandHelper.java b/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
index 985389e..63fdd0b 100644
--- a/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
+++ b/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
@@ -132,7 +132,7 @@
             }
             int currentPage = recents.getNextPage();
             if (currentPage >= 0 && currentPage < recents.getTaskViewCount()) {
-                ((TaskView) recents.getPageAt(currentPage)).launchTask(true);
+                ((TaskView) recents.getPageAt(currentPage)).launchTaskAnimated();
             } else {
                 recents.startHome();
             }
diff --git a/quickstep/src/com/android/quickstep/RecentsActivity.java b/quickstep/src/com/android/quickstep/RecentsActivity.java
index 1b2fd41..3d68d64 100644
--- a/quickstep/src/com/android/quickstep/RecentsActivity.java
+++ b/quickstep/src/com/android/quickstep/RecentsActivity.java
@@ -18,9 +18,9 @@
 import static android.content.pm.ActivityInfo.CONFIG_ORIENTATION;
 import static android.content.pm.ActivityInfo.CONFIG_SCREEN_SIZE;
 
-import static com.android.launcher3.QuickstepAppTransitionManagerImpl.RECENTS_LAUNCH_DURATION;
-import static com.android.launcher3.QuickstepAppTransitionManagerImpl.STATUS_BAR_TRANSITION_DURATION;
-import static com.android.launcher3.QuickstepAppTransitionManagerImpl.STATUS_BAR_TRANSITION_PRE_DELAY;
+import static com.android.launcher3.QuickstepTransitionManager.RECENTS_LAUNCH_DURATION;
+import static com.android.launcher3.QuickstepTransitionManager.STATUS_BAR_TRANSITION_DURATION;
+import static com.android.launcher3.QuickstepTransitionManager.STATUS_BAR_TRANSITION_PRE_DELAY;
 import static com.android.launcher3.Utilities.createHomeIntent;
 import static com.android.launcher3.testing.TestProtocol.OVERVIEW_STATE_ORDINAL;
 import static com.android.quickstep.TaskUtils.taskIsATargetWithMode;
@@ -30,7 +30,6 @@
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.AnimatorSet;
-import android.app.ActivityOptions;
 import android.content.Intent;
 import android.content.res.Configuration;
 import android.os.Bundle;
@@ -46,7 +45,6 @@
 import com.android.launcher3.R;
 import com.android.launcher3.WrappedAnimationRunnerImpl;
 import com.android.launcher3.WrappedLauncherAnimationRunner;
-import com.android.launcher3.allapps.search.SearchAdapterProvider;
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.compat.AccessibilityManagerCompat;
@@ -54,7 +52,9 @@
 import com.android.launcher3.statemanager.StateManager.AtomicAnimationFactory;
 import com.android.launcher3.statemanager.StateManager.StateHandler;
 import com.android.launcher3.statemanager.StatefulActivity;
+import com.android.launcher3.util.ActivityOptionsWrapper;
 import com.android.launcher3.util.ActivityTracker;
+import com.android.launcher3.util.RunnableList;
 import com.android.launcher3.util.SystemUiController;
 import com.android.launcher3.util.Themes;
 import com.android.launcher3.views.BaseDragLayer;
@@ -94,7 +94,6 @@
 
     // Strong refs to runners which are cleared when the activity is destroyed
     private WrappedAnimationRunnerImpl mActivityLaunchAnimationRunner;
-    private SearchAdapterProvider mSearchAdapterProvider;
 
     /**
      * Init drag layer and overview panel views.
@@ -172,42 +171,40 @@
     }
 
     @Override
-    public ActivityOptions getActivityLaunchOptions(final View v) {
+    public ActivityOptionsWrapper getActivityLaunchOptions(final View v) {
         if (!(v instanceof TaskView)) {
-            return null;
+            return super.getActivityLaunchOptions(v);
         }
 
         final TaskView taskView = (TaskView) v;
-        mActivityLaunchAnimationRunner = new WrappedAnimationRunnerImpl() {
-            @Override
-            public Handler getHandler() {
-                return mUiHandler;
-            }
+        RunnableList onEndCallback = new RunnableList();
 
-            @Override
-            public void onCreateAnimation(int transit,
+        mActivityLaunchAnimationRunner = (int transit,
                     RemoteAnimationTargetCompat[] appTargets,
                     RemoteAnimationTargetCompat[] wallpaperTargets,
                     RemoteAnimationTargetCompat[] nonAppTargets,
-                    AnimationResult result) {
-                AnimatorSet anim = composeRecentsLaunchAnimator(taskView, appTargets,
-                        wallpaperTargets);
-                anim.addListener(resetStateListener());
-                result.setAnimation(anim, RecentsActivity.this);
-            }
+                    AnimationResult result) -> {
+            AnimatorSet anim = composeRecentsLaunchAnimator(taskView, appTargets,
+                    wallpaperTargets);
+            anim.addListener(resetStateListener());
+            result.setAnimation(anim, RecentsActivity.this, onEndCallback::executeAllAndDestroy);
         };
+
         final LauncherAnimationRunner wrapper = new WrappedLauncherAnimationRunner<>(
-                mActivityLaunchAnimationRunner, true /* startAtFrontOfQueue */);
-        return ActivityOptionsCompat.makeRemoteAnimation(new RemoteAnimationAdapterCompat(
+                mUiHandler, mActivityLaunchAnimationRunner, true /* startAtFrontOfQueue */);
+        RemoteAnimationAdapterCompat adapterCompat = new RemoteAnimationAdapterCompat(
                 wrapper, RECENTS_LAUNCH_DURATION,
                 RECENTS_LAUNCH_DURATION - STATUS_BAR_TRANSITION_DURATION
-                        - STATUS_BAR_TRANSITION_PRE_DELAY));
+                        - STATUS_BAR_TRANSITION_PRE_DELAY);
+        return new ActivityOptionsWrapper(
+                ActivityOptionsCompat.makeRemoteAnimation(adapterCompat),
+                onEndCallback);
     }
 
     /**
      * Composes the animations for a launch from the recents list if possible.
      */
-    private AnimatorSet composeRecentsLaunchAnimator(TaskView taskView,
+    private AnimatorSet  composeRecentsLaunchAnimator(TaskView taskView,
             RemoteAnimationTargetCompat[] appTargets,
             RemoteAnimationTargetCompat[] wallpaperTargets) {
         AnimatorSet target = new AnimatorSet();
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java
index 5668817..a1cbec7 100644
--- a/quickstep/src/com/android/quickstep/SystemUiProxy.java
+++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java
@@ -35,38 +35,55 @@
 import android.view.MotionEvent;
 
 import com.android.launcher3.util.MainThreadInitializedObject;
-import com.android.systemui.shared.recents.IPinnedStackAnimationListener;
-import com.android.systemui.shared.recents.ISplitScreenListener;
-import com.android.systemui.shared.recents.IStartingWindowListener;
 import com.android.systemui.shared.recents.ISystemUiProxy;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.system.RemoteTransitionCompat;
+import com.android.wm.shell.onehanded.IOneHanded;
+import com.android.wm.shell.pip.IPip;
+import com.android.wm.shell.pip.IPipAnimationListener;
+import com.android.wm.shell.splitscreen.ISplitScreen;
+import com.android.wm.shell.splitscreen.ISplitScreenListener;
+import com.android.wm.shell.startingsurface.IStartingWindow;
+import com.android.wm.shell.startingsurface.IStartingWindowListener;
+import com.android.wm.shell.transition.IShellTransitions;
 
 /**
  * Holds the reference to SystemUI.
  */
-public class SystemUiProxy implements ISystemUiProxy {
+public class SystemUiProxy implements ISystemUiProxy,
+        SysUINavigationMode.NavigationModeChangeListener {
     private static final String TAG = SystemUiProxy.class.getSimpleName();
 
     public static final MainThreadInitializedObject<SystemUiProxy> INSTANCE =
             new MainThreadInitializedObject<>(SystemUiProxy::new);
 
     private ISystemUiProxy mSystemUiProxy;
+    private IPip mPip;
+    private ISplitScreen mSplitScreen;
+    private IOneHanded mOneHanded;
+    private IShellTransitions mShellTransitions;
+    private IStartingWindow mStartingWindow;
     private final DeathRecipient mSystemUiProxyDeathRecipient = () -> {
-        MAIN_EXECUTOR.execute(() -> setProxy(null));
+        MAIN_EXECUTOR.execute(() -> clearProxy());
     };
 
     // Used to dedupe calls to SystemUI
     private int mLastShelfHeight;
     private boolean mLastShelfVisible;
-    private float mLastBackButtonAlpha;
-    private boolean mLastBackButtonAnimate;
+    private float mLastNavButtonAlpha;
+    private boolean mLastNavButtonAnimate;
 
     // TODO(141886704): Find a way to remove this
     private int mLastSystemUiStateFlags;
 
     public SystemUiProxy(Context context) {
-        // Do nothing
+        SysUINavigationMode.INSTANCE.get(context).addModeChangeListener(this);
+    }
+
+    @Override
+    public void onNavigationModeChanged(SysUINavigationMode.Mode newMode) {
+        // Whenever the nav mode changes, force reset the nav button alpha
+        setNavBarButtonAlpha(1f, false);
     }
 
     @Override
@@ -75,12 +92,23 @@
         return null;
     }
 
-    public void setProxy(ISystemUiProxy proxy) {
+    public void setProxy(ISystemUiProxy proxy, IPip pip, ISplitScreen splitScreen,
+            IOneHanded oneHanded, IShellTransitions shellTransitions,
+            IStartingWindow startingWindow) {
         unlinkToDeath();
         mSystemUiProxy = proxy;
+        mPip = pip;
+        mSplitScreen = splitScreen;
+        mOneHanded = oneHanded;
+        mShellTransitions = shellTransitions;
+        mStartingWindow = startingWindow;
         linkToDeath();
     }
 
+    public void clearProxy() {
+        setProxy(null, null, null, null, null, null);
+    }
+
     // TODO(141886704): Find a way to remove this
     public void setLastSystemUiStateFlags(int stateFlags) {
         mLastSystemUiStateFlags = stateFlags;
@@ -149,28 +177,17 @@
         return null;
     }
 
-    @Override
-    public void setBackButtonAlpha(float alpha, boolean animate) {
-        boolean changed = Float.compare(alpha, mLastBackButtonAlpha) != 0
-                || animate != mLastBackButtonAnimate;
-        if (mSystemUiProxy != null && changed) {
-            mLastBackButtonAlpha = alpha;
-            mLastBackButtonAnimate = animate;
-            try {
-                mSystemUiProxy.setBackButtonAlpha(alpha, animate);
-            } catch (RemoteException e) {
-                Log.w(TAG, "Failed call setBackButtonAlpha", e);
-            }
-        }
-    }
-
-    public float getLastBackButtonAlpha() {
-        return mLastBackButtonAlpha;
+    public float getLastNavButtonAlpha() {
+        return mLastNavButtonAlpha;
     }
 
     @Override
     public void setNavBarButtonAlpha(float alpha, boolean animate) {
-        if (mSystemUiProxy != null) {
+        boolean changed = Float.compare(alpha, mLastNavButtonAlpha) != 0
+                || animate != mLastNavButtonAnimate;
+        if (mSystemUiProxy != null && changed) {
+            mLastNavButtonAlpha = alpha;
+            mLastNavButtonAnimate = animate;
             try {
                 mSystemUiProxy.setNavBarButtonAlpha(alpha, animate);
             } catch (RemoteException e) {
@@ -269,21 +286,6 @@
     }
 
     @Override
-    public void setShelfHeight(boolean visible, int shelfHeight) {
-        boolean changed = visible != mLastShelfVisible || shelfHeight != mLastShelfHeight;
-        if (mSystemUiProxy != null && changed) {
-            mLastShelfVisible = visible;
-            mLastShelfHeight = shelfHeight;
-            try {
-                mSystemUiProxy.setShelfHeight(visible, shelfHeight);
-            } catch (RemoteException e) {
-                Log.w(TAG, "Failed call setShelfHeight visible: " + visible
-                        + " height: " + shelfHeight, e);
-            }
-        }
-    }
-
-    @Override
     public void handleImageAsScreenshot(Bitmap bitmap, Rect rect, Insets insets, int i) {
         if (mSystemUiProxy != null) {
             try {
@@ -319,20 +321,6 @@
         }
     }
 
-    /**
-     * Sets listener to get pinned stack animation callbacks.
-     */
-    @Override
-    public void setPinnedStackAnimationListener(IPinnedStackAnimationListener listener) {
-        if (mSystemUiProxy != null) {
-            try {
-                mSystemUiProxy.setPinnedStackAnimationListener(listener);
-            } catch (RemoteException e) {
-                Log.w(TAG, "Failed call setPinnedStackAnimationListener", e);
-            }
-        }
-    }
-
     @Override
     public void onQuickSwitchToNewTask(int rotation) {
         if (mSystemUiProxy != null) {
@@ -358,28 +346,6 @@
     }
 
     @Override
-    public void startOneHandedMode() {
-        if (mSystemUiProxy != null) {
-            try {
-                mSystemUiProxy.startOneHandedMode();
-            } catch (RemoteException e) {
-                Log.w(TAG, "Failed call startOneHandedMode", e);
-            }
-        }
-    }
-
-    @Override
-    public void stopOneHandedMode() {
-        if (mSystemUiProxy != null) {
-            try {
-                mSystemUiProxy.stopOneHandedMode();
-            } catch (RemoteException e) {
-                Log.w(TAG, "Failed call stopOneHandedMode", e);
-            }
-        }
-    }
-
-    @Override
     public void expandNotificationPanel() {
         if (mSystemUiProxy != null) {
             try {
@@ -390,12 +356,45 @@
         }
     }
 
-    @Override
+    //
+    // Pip
+    //
+
+    /**
+     * Sets the shelf height.
+     */
+    public void setShelfHeight(boolean visible, int shelfHeight) {
+        boolean changed = visible != mLastShelfVisible || shelfHeight != mLastShelfHeight;
+        if (mPip != null && changed) {
+            mLastShelfVisible = visible;
+            mLastShelfHeight = shelfHeight;
+            try {
+                mPip.setShelfHeight(visible, shelfHeight);
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed call setShelfHeight visible: " + visible
+                        + " height: " + shelfHeight, e);
+            }
+        }
+    }
+
+    /**
+     * Sets listener to get pinned stack animation callbacks.
+     */
+    public void setPinnedStackAnimationListener(IPipAnimationListener listener) {
+        if (mPip != null) {
+            try {
+                mPip.setPinnedStackAnimationListener(listener);
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed call setPinnedStackAnimationListener", e);
+            }
+        }
+    }
+
     public Rect startSwipePipToHome(ComponentName componentName, ActivityInfo activityInfo,
             PictureInPictureParams pictureInPictureParams, int launcherRotation, int shelfHeight) {
-        if (mSystemUiProxy != null) {
+        if (mPip != null) {
             try {
-                return mSystemUiProxy.startSwipePipToHome(componentName, activityInfo,
+                return mPip.startSwipePipToHome(componentName, activityInfo,
                         pictureInPictureParams, launcherRotation, shelfHeight);
             } catch (RemoteException e) {
                 Log.w(TAG, "Failed call startSwipePipToHome", e);
@@ -404,111 +403,85 @@
         return null;
     }
 
-    @Override
     public void stopSwipePipToHome(ComponentName componentName, Rect destinationBounds) {
-        if (mSystemUiProxy != null) {
+        if (mPip != null) {
             try {
-                mSystemUiProxy.stopSwipePipToHome(componentName, destinationBounds);
+                mPip.stopSwipePipToHome(componentName, destinationBounds);
             } catch (RemoteException e) {
                 Log.w(TAG, "Failed call stopSwipePipToHome");
             }
         }
     }
 
-    @Override
-    public void registerRemoteTransition(RemoteTransitionCompat remoteTransition) {
-        if (mSystemUiProxy != null) {
-            try {
-                mSystemUiProxy.registerRemoteTransition(remoteTransition);
-            } catch (RemoteException e) {
-                Log.w(TAG, "Failed call registerRemoteTransition");
-            }
-        }
-    }
+    //
+    // Splitscreen
+    //
 
-    @Override
-    public void unregisterRemoteTransition(RemoteTransitionCompat remoteTransition) {
-        if (mSystemUiProxy != null) {
-            try {
-                mSystemUiProxy.unregisterRemoteTransition(remoteTransition);
-            } catch (RemoteException e) {
-                Log.w(TAG, "Failed call registerRemoteTransition");
-            }
-        }
-    }
-
-    @Override
     public void registerSplitScreenListener(ISplitScreenListener listener) {
-        if (mSystemUiProxy != null) {
+        if (mSplitScreen != null) {
             try {
-                mSystemUiProxy.registerSplitScreenListener(listener);
+                mSplitScreen.registerSplitScreenListener(listener);
             } catch (RemoteException e) {
                 Log.w(TAG, "Failed call registerSplitScreenListener");
             }
         }
     }
 
-    @Override
     public void unregisterSplitScreenListener(ISplitScreenListener listener) {
-        if (mSystemUiProxy != null) {
+        if (mSplitScreen != null) {
             try {
-                mSystemUiProxy.unregisterSplitScreenListener(listener);
+                mSplitScreen.unregisterSplitScreenListener(listener);
             } catch (RemoteException e) {
                 Log.w(TAG, "Failed call unregisterSplitScreenListener");
             }
         }
     }
 
-    @Override
     public void setSideStageVisibility(boolean visible) {
-        if (mSystemUiProxy != null) {
+        if (mSplitScreen != null) {
             try {
-                mSystemUiProxy.setSideStageVisibility(visible);
+                mSplitScreen.setSideStageVisibility(visible);
             } catch (RemoteException e) {
                 Log.w(TAG, "Failed call setSideStageVisibility");
             }
         }
     }
 
-    @Override
     public void exitSplitScreen() {
-        if (mSystemUiProxy != null) {
+        if (mSplitScreen != null) {
             try {
-                mSystemUiProxy.exitSplitScreen();
+                mSplitScreen.exitSplitScreen();
             } catch (RemoteException e) {
                 Log.w(TAG, "Failed call exitSplitScreen");
             }
         }
     }
 
-    @Override
     public void exitSplitScreenOnHide(boolean exitSplitScreenOnHide) {
-        if (mSystemUiProxy != null) {
+        if (mSplitScreen != null) {
             try {
-                mSystemUiProxy.exitSplitScreenOnHide(exitSplitScreenOnHide);
+                mSplitScreen.exitSplitScreenOnHide(exitSplitScreenOnHide);
             } catch (RemoteException e) {
                 Log.w(TAG, "Failed call exitSplitScreen");
             }
         }
     }
 
-    @Override
     public void startTask(int taskId, int stage, int position, Bundle options) {
-        if (mSystemUiProxy != null) {
+        if (mSplitScreen != null) {
             try {
-                mSystemUiProxy.startTask(taskId, stage, position, options);
+                mSplitScreen.startTask(taskId, stage, position, options);
             } catch (RemoteException e) {
                 Log.w(TAG, "Failed call startTask");
             }
         }
     }
 
-    @Override
     public void startShortcut(String packageName, String shortcutId, int stage, int position,
             Bundle options, UserHandle user) {
-        if (mSystemUiProxy != null) {
+        if (mSplitScreen != null) {
             try {
-                mSystemUiProxy.startShortcut(packageName, shortcutId, stage, position, options,
+                mSplitScreen.startShortcut(packageName, shortcutId, stage, position, options,
                         user);
             } catch (RemoteException e) {
                 Log.w(TAG, "Failed call startShortcut");
@@ -516,38 +489,87 @@
         }
     }
 
-    @Override
-    public void startIntent(PendingIntent intent, Intent fillInIntent, int stage,
-            int position, Bundle options) {
-        if (mSystemUiProxy != null) {
+    public void startIntent(PendingIntent intent, Intent fillInIntent, int stage, int position,
+            Bundle options) {
+        if (mSplitScreen != null) {
             try {
-                mSystemUiProxy.startIntent(intent, fillInIntent, stage, position,
-                        options);
+                mSplitScreen.startIntent(intent, fillInIntent, stage, position, options);
             } catch (RemoteException e) {
                 Log.w(TAG, "Failed call startIntent");
             }
         }
     }
 
-    @Override
     public void removeFromSideStage(int taskId) {
-        if (mSystemUiProxy != null) {
+        if (mSplitScreen != null) {
             try {
-                mSystemUiProxy.removeFromSideStage(taskId);
+                mSplitScreen.removeFromSideStage(taskId);
             } catch (RemoteException e) {
                 Log.w(TAG, "Failed call removeFromSideStage");
             }
         }
     }
 
+    //
+    // One handed
+    //
+
+    public void startOneHandedMode() {
+        if (mOneHanded != null) {
+            try {
+                mOneHanded.startOneHanded();
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed call startOneHandedMode", e);
+            }
+        }
+    }
+
+    public void stopOneHandedMode() {
+        if (mOneHanded != null) {
+            try {
+                mOneHanded.stopOneHanded();
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed call stopOneHandedMode", e);
+            }
+        }
+    }
+
+    //
+    // Remote transitions
+    //
+
+    public void registerRemoteTransition(RemoteTransitionCompat remoteTransition) {
+        if (mShellTransitions != null) {
+            try {
+                mShellTransitions.registerRemote(remoteTransition.getFilter(),
+                        remoteTransition.getTransition());
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed call registerRemoteTransition");
+            }
+        }
+    }
+
+    public void unregisterRemoteTransition(RemoteTransitionCompat remoteTransition) {
+        if (mShellTransitions != null) {
+            try {
+                mShellTransitions.unregisterRemote(remoteTransition.getTransition());
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed call registerRemoteTransition");
+            }
+        }
+    }
+
+    //
+    // Starting window
+    //
+
     /**
      * Sets listener to get callbacks when launching a task.
      */
-    @Override
     public void setStartingWindowListener(IStartingWindowListener listener) {
-        if (mSystemUiProxy != null) {
+        if (mStartingWindow != null) {
             try {
-                mSystemUiProxy.setStartingWindowListener(listener);
+                mStartingWindow.setStartingWindowListener(listener);
             } catch (RemoteException e) {
                 Log.w(TAG, "Failed call setStartingWindowListener", e);
             }
diff --git a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
index 65bb0f3..d81f07f 100644
--- a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
+++ b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
@@ -38,7 +38,6 @@
 import com.android.launcher3.model.WellbeingModel;
 import com.android.launcher3.popup.SystemShortcut;
 import com.android.launcher3.popup.SystemShortcut.AppInfo;
-import com.android.launcher3.util.Executors;
 import com.android.launcher3.util.InstantAppResolver;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.TaskThumbnailView;
@@ -54,7 +53,6 @@
 
 import java.util.Collections;
 import java.util.List;
-import java.util.function.Consumer;
 
 /**
  * Represents a system shortcut that can be shown for a recent task.
@@ -281,15 +279,9 @@
 
         @Override
         public void onClick(View view) {
-            Consumer<Boolean> resultCallback = success -> {
-                if (success) {
-                    SystemUiProxy.INSTANCE.get(mTarget).startScreenPinning(
-                            mTaskView.getTask().key.id);
-                } else {
-                    mTaskView.notifyTaskLaunchFailed(TAG);
-                }
-            };
-            mTaskView.launchTask(true, resultCallback, Executors.MAIN_EXECUTOR.getHandler());
+            if (mTaskView.launchTaskAnimated() != null) {
+                SystemUiProxy.INSTANCE.get(mTarget).startScreenPinning(mTaskView.getTask().key.id);
+            }
             dismissTaskMenuView(mTarget);
             mTarget.getStatsLogManager().logger().withItemInfo(mTaskView.getItemInfo())
                     .log(LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_PIN_TAP);
diff --git a/quickstep/src/com/android/quickstep/TaskViewUtils.java b/quickstep/src/com/android/quickstep/TaskViewUtils.java
index 17822e6..2feeffa 100644
--- a/quickstep/src/com/android/quickstep/TaskViewUtils.java
+++ b/quickstep/src/com/android/quickstep/TaskViewUtils.java
@@ -18,7 +18,7 @@
 import static com.android.launcher3.LauncherAnimUtils.VIEW_ALPHA;
 import static com.android.launcher3.LauncherState.BACKGROUND_APP;
 import static com.android.launcher3.LauncherState.NORMAL;
-import static com.android.launcher3.QuickstepAppTransitionManagerImpl.RECENTS_LAUNCH_DURATION;
+import static com.android.launcher3.QuickstepTransitionManager.RECENTS_LAUNCH_DURATION;
 import static com.android.launcher3.Utilities.getDescendantCoordRelativeToAncestor;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.anim.Interpolators.TOUCH_RESPONSE_INTERPOLATOR;
@@ -180,6 +180,7 @@
         boolean parallaxCenterAndAdjacentTask =
                 taskIndex != recentsView.getCurrentPage() && !(dp.isTablet
                         && FeatureFlags.ENABLE_OVERVIEW_GRID.get());
+        float gridProgress = recentsView.getGridProgress();
         float gridTranslationSecondary = recentsView.getGridTranslationSecondary(taskIndex);
         int startScroll = recentsView.getScrollOffset(taskIndex);
 
@@ -197,7 +198,7 @@
             tsv.setPreview(targets.apps[targets.apps.length - 1]);
             tsv.fullScreenProgress.value = 0;
             tsv.recentsViewScale.value = 1;
-            tsv.gridProgress.value = 1;
+            tsv.gridProgress.value = gridProgress;
             tsv.gridTranslationSecondary.value = gridTranslationSecondary;
             tsv.setScroll(startScroll);
 
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index fc805d0..1cb5f5d 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -24,6 +24,11 @@
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.quickstep.GestureState.DEFAULT_STATE;
 import static com.android.quickstep.util.NavigationModeFeatureFlag.LIVE_TILE;
+import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SHELL_ONE_HANDED;
+import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SHELL_PIP;
+import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SHELL_SHELL_TRANSITIONS;
+import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SHELL_SPLIT_SCREEN;
+import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SHELL_STARTING_WINDOW;
 import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SYSUI_PROXY;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_TRACING_ENABLED;
 
@@ -101,6 +106,11 @@
 import com.android.systemui.shared.system.InputConsumerController;
 import com.android.systemui.shared.system.InputMonitorCompat;
 import com.android.systemui.shared.tracing.ProtoTraceable;
+import com.android.wm.shell.onehanded.IOneHanded;
+import com.android.wm.shell.pip.IPip;
+import com.android.wm.shell.splitscreen.ISplitScreen;
+import com.android.wm.shell.startingsurface.IStartingWindow;
+import com.android.wm.shell.transition.IShellTransitions;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
@@ -140,8 +150,18 @@
         public void onInitialize(Bundle bundle) {
             ISystemUiProxy proxy = ISystemUiProxy.Stub.asInterface(
                     bundle.getBinder(KEY_EXTRA_SYSUI_PROXY));
+            IPip pip = IPip.Stub.asInterface(bundle.getBinder(KEY_EXTRA_SHELL_PIP));
+            ISplitScreen splitscreen = ISplitScreen.Stub.asInterface(bundle.getBinder(
+                    KEY_EXTRA_SHELL_SPLIT_SCREEN));
+            IOneHanded onehanded = IOneHanded.Stub.asInterface(
+                    bundle.getBinder(KEY_EXTRA_SHELL_ONE_HANDED));
+            IShellTransitions shellTransitions = IShellTransitions.Stub.asInterface(
+                    bundle.getBinder(KEY_EXTRA_SHELL_SHELL_TRANSITIONS));
+            IStartingWindow startingWindow = IStartingWindow.Stub.asInterface(
+                    bundle.getBinder(KEY_EXTRA_SHELL_STARTING_WINDOW));
             MAIN_EXECUTOR.execute(() -> {
-                SystemUiProxy.INSTANCE.get(TouchInteractionService.this).setProxy(proxy);
+                SystemUiProxy.INSTANCE.get(TouchInteractionService.this).setProxy(proxy, pip,
+                        splitscreen, onehanded, shellTransitions, startingWindow);
                 TouchInteractionService.this.initInputMonitor();
                 preloadOverview(true /* fromInit */);
                 mDeviceState.runOnUserUnlocked(() -> {
@@ -421,7 +441,7 @@
         }
         disposeEventHandlers();
         mDeviceState.destroy();
-        SystemUiProxy.INSTANCE.get(this).setProxy(null);
+        SystemUiProxy.INSTANCE.get(this).clearProxy();
         ProtoTracer.INSTANCE.get(this).stop();
         ProtoTracer.INSTANCE.get(this).remove(this);
 
diff --git a/quickstep/src/com/android/quickstep/util/ImageActionUtils.java b/quickstep/src/com/android/quickstep/util/ImageActionUtils.java
index d022085..0f2d778 100644
--- a/quickstep/src/com/android/quickstep/util/ImageActionUtils.java
+++ b/quickstep/src/com/android/quickstep/util/ImageActionUtils.java
@@ -99,7 +99,14 @@
             .putExtra(Intent.EXTRA_STREAM, uri)
             .putExtra(Intent.EXTRA_SHORTCUT_ID, shortcutInfo.getId())
             .setClipData(clipdata);
-        context.startActivity(intent);
+
+        if (context.getUserId() != appTarget.getUser().getIdentifier()) {
+            intent.prepareToLeaveUser(context.getUserId());
+            intent.fixUris(context.getUserId());
+            context.startActivityAsUser(intent, appTarget.getUser());
+        } else {
+            context.startActivity(intent);
+        }
     }
 
     /**
diff --git a/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java b/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java
index 215f05a..188efad 100644
--- a/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java
+++ b/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java
@@ -23,9 +23,9 @@
 import static android.view.Surface.ROTATION_270;
 import static android.view.Surface.ROTATION_90;
 
-import static com.android.launcher3.util.SettingsCache.ROTATION_SETTING_URI;
 import static com.android.launcher3.states.RotationHelper.ALLOW_ROTATION_PREFERENCE_KEY;
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
+import static com.android.launcher3.util.SettingsCache.ROTATION_SETTING_URI;
 
 import static java.lang.annotation.RetentionPolicy.SOURCE;
 
@@ -47,11 +47,12 @@
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
-import com.android.launcher3.util.SettingsCache;
 import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.touch.PagedOrientationHandler;
+import com.android.launcher3.util.SettingsCache;
 import com.android.launcher3.util.WindowBounds;
 import com.android.quickstep.BaseActivityInterface;
+import com.android.quickstep.views.TaskView;
 
 import java.lang.annotation.Retention;
 import java.util.function.IntConsumer;
@@ -367,8 +368,12 @@
      */
     public float getFullScreenScaleAndPivot(Rect taskView, DeviceProfile dp, PointF outPivot) {
         Rect insets = dp.getInsets();
-        float fullWidth = dp.widthPx - insets.left - insets.right;
-        float fullHeight = dp.heightPx - insets.top - insets.bottom;
+        float fullWidth = dp.widthPx;
+        float fullHeight = dp.heightPx;
+        if (TaskView.CLIP_STATUS_AND_NAV_BARS) {
+            fullWidth -= insets.left + insets.right;
+            fullHeight -= insets.top + insets.bottom;
+        }
 
         if (dp.isMultiWindowMode) {
             WindowBounds bounds = SplitScreenBounds.INSTANCE.getSecondaryWindowBounds(mContext);
diff --git a/quickstep/src/com/android/quickstep/util/RemoteAnimationProvider.java b/quickstep/src/com/android/quickstep/util/RemoteAnimationProvider.java
index 3adb459..5c6da16 100644
--- a/quickstep/src/com/android/quickstep/util/RemoteAnimationProvider.java
+++ b/quickstep/src/com/android/quickstep/util/RemoteAnimationProvider.java
@@ -21,7 +21,6 @@
 import android.os.Handler;
 
 import com.android.launcher3.LauncherAnimationRunner;
-import com.android.launcher3.LauncherAnimationRunner.AnimationResult;
 import com.android.launcher3.WrappedAnimationRunnerImpl;
 import com.android.launcher3.WrappedLauncherAnimationRunner;
 import com.android.systemui.shared.system.ActivityOptionsCompat;
@@ -36,23 +35,10 @@
             RemoteAnimationTargetCompat[] wallpaperTargets);
 
     ActivityOptions toActivityOptions(Handler handler, long duration, Context context) {
-        mAnimationRunner = new WrappedAnimationRunnerImpl() {
-            @Override
-            public Handler getHandler() {
-                return handler;
-            }
-
-            @Override
-            public void onCreateAnimation(int transit,
-                    RemoteAnimationTargetCompat[] appTargets,
-                    RemoteAnimationTargetCompat[] wallpaperTargets,
-                    RemoteAnimationTargetCompat[] nonApps,
-                    AnimationResult result) {
+        mAnimationRunner = (transit, appTargets, wallpaperTargets, nonApps, result) ->
                 result.setAnimation(createWindowAnimation(appTargets, wallpaperTargets), context);
-            }
-        };
         final LauncherAnimationRunner wrapper = new WrappedLauncherAnimationRunner(
-                mAnimationRunner, false /* startAtFrontOfQueue */);
+                handler, mAnimationRunner, false /* startAtFrontOfQueue */);
         return ActivityOptionsCompat.makeRemoteAnimation(
                 new RemoteAnimationAdapterCompat(wrapper, duration, 0));
     }
diff --git a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
index 9537247..df1229b 100644
--- a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
+++ b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
@@ -79,6 +79,7 @@
     private final boolean mIsRecentsRtl;
 
     private final Rect mTaskRect = new Rect();
+    private final Rect mGridRect = new Rect();
     private boolean mDrawsBelowRecents;
     private final PointF mPivot = new PointF();
     private DeviceProfile mDp;
@@ -124,7 +125,7 @@
         Resources resources = context.getResources();
         mIsRecentsRtl = mOrientationState.getOrientationHandler().getRecentsRtlSetting(resources);
         mTaskThumbnailPadding = (int) resources.getDimension(R.dimen.task_thumbnail_top_margin);
-        mRowSpacing = (int) resources.getDimension(R.dimen.recents_row_spacing);
+        mRowSpacing = (int) resources.getDimension(R.dimen.overview_grid_row_spacing);
     }
 
     /**
@@ -266,6 +267,7 @@
             mOrientationStateId = mOrientationState.getStateId();
 
             getFullScreenScale();
+            mSizeStrategy.calculateGridSize(mContext, mDp, mGridRect);
             mThumbnailData.rotation = mOrientationState.getDisplayRotation();
 
             mPositionHelper.updateThumbnailMatrix(
@@ -304,24 +306,34 @@
         mMatrix.postTranslate(insets.left, insets.top);
         mMatrix.postScale(scale, scale);
 
+        // Apply TaskView matrix: gridProgress related properties
         float interpolatedGridProgress = ACCEL_DEACCEL.getInterpolation(gridProgress.value);
-
-        // Apply TaskView matrix: gridProgress
         final int boxLength = (int) Math.max(taskWidth, taskHeight);
-        float availableHeight =
-                mTaskThumbnailPadding + taskHeight + mSizeStrategy.getOverviewActionsHeight(
-                        mContext);
+        float availableHeight = mGridRect.height();
         float rowHeight = (availableHeight - mRowSpacing) / 2;
         float gridScale = rowHeight / (boxLength + mTaskThumbnailPadding);
         scale = Utilities.mapRange(interpolatedGridProgress, 1f, gridScale);
         mMatrix.postScale(scale, scale, mIsRecentsRtl ? 0 : taskWidth, 0);
-        float taskWidthDiff = taskWidth * (1 - gridScale);
-        float taskWidthOffset = mIsRecentsRtl ? taskWidthDiff : -taskWidthDiff;
-        mOrientationState.getOrientationHandler().set(mMatrix, MATRIX_POST_TRANSLATE,
-                Utilities.mapRange(interpolatedGridProgress, 0, taskWidthOffset));
         mOrientationState.getOrientationHandler().setSecondary(mMatrix, MATRIX_POST_TRANSLATE,
                 Utilities.mapRange(interpolatedGridProgress, 0, gridTranslationSecondary.value));
 
+        // Apply TaskView matrix: task rect and grid rect difference
+        float scaledWidth = taskWidth * gridScale;
+        float taskGridHorizontalDiff;
+        if (mIsRecentsRtl) {
+            float taskRight = mTaskRect.left + scaledWidth;
+            taskGridHorizontalDiff = mGridRect.right - taskRight;
+        } else {
+            float taskLeft = mTaskRect.right - scaledWidth;
+            taskGridHorizontalDiff = mGridRect.left - taskLeft;
+        }
+        float taskGridVerticalDiff =
+                mGridRect.top + mTaskThumbnailPadding * gridScale - mTaskRect.top;
+        mOrientationState.getOrientationHandler().set(mMatrix, MATRIX_POST_TRANSLATE,
+                Utilities.mapRange(interpolatedGridProgress, 0, taskGridHorizontalDiff));
+        mOrientationState.getOrientationHandler().setSecondary(mMatrix, MATRIX_POST_TRANSLATE,
+                Utilities.mapRange(interpolatedGridProgress, 0, taskGridVerticalDiff));
+
         // Apply TaskView matrix: translate, scroll
         mMatrix.postTranslate(mTaskRect.left, mTaskRect.top);
         mOrientationState.getOrientationHandler().set(mMatrix, MATRIX_POST_TRANSLATE,
diff --git a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
index c62f3e2..ceb343d 100644
--- a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
@@ -21,7 +21,7 @@
 import static com.android.launcher3.LauncherState.OVERVIEW;
 import static com.android.launcher3.LauncherState.OVERVIEW_MODAL_TASK;
 import static com.android.launcher3.LauncherState.SPRING_LOADED;
-import static com.android.launcher3.QuickstepAppTransitionManagerImpl.ALL_APPS_PROGRESS_OFF_SCREEN;
+import static com.android.launcher3.QuickstepTransitionManager.ALL_APPS_PROGRESS_OFF_SCREEN;
 import static com.android.launcher3.allapps.AllAppsTransitionController.ALL_APPS_PROGRESS;
 import static com.android.quickstep.util.NavigationModeFeatureFlag.LIVE_TILE;
 
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index bdd0a36..261c43f 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -39,6 +39,7 @@
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_DISMISS_SWIPE_UP;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_LAUNCH_SWIPE_DOWN;
 import static com.android.launcher3.statehandlers.DepthController.DEPTH;
+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.SystemUiController.UI_STATE_OVERVIEW;
 import static com.android.quickstep.TaskUtils.checkCurrentOrManagedUserId;
@@ -133,13 +134,13 @@
 import com.android.quickstep.util.TaskViewSimulator;
 import com.android.quickstep.util.TransformParams;
 import com.android.systemui.plugins.ResourceProvider;
-import com.android.systemui.shared.recents.IPinnedStackAnimationListener;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.recents.model.Task.TaskKey;
 import com.android.systemui.shared.recents.model.ThumbnailData;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.PackageManagerWrapper;
 import com.android.systemui.shared.system.TaskStackChangeListener;
+import com.android.wm.shell.pip.IPipAnimationListener;
 
 import java.util.ArrayList;
 import java.util.function.Consumer;
@@ -264,6 +265,7 @@
     protected final TransformParams mLiveTileParams = new TransformParams();
     protected final TaskViewSimulator mLiveTileTaskViewSimulator;
     protected final Rect mLastComputedTaskSize = new Rect();
+    protected final Rect mLastComputedGridSize = new Rect();
     // How much a task that is directly offscreen will be pushed out due to RecentsView scale/pivot.
     protected Float mLastComputedTaskPushOutDistance = null;
     protected boolean mEnableDrawingLiveTile = false;
@@ -376,7 +378,7 @@
         }
     };
 
-    private final PinnedStackAnimationListener mIPinnedStackAnimationListener =
+    private final PinnedStackAnimationListener mIPipAnimationListener =
             new PinnedStackAnimationListener();
 
     // Used to keep track of the last requested task list id, so that we do not request to load the
@@ -471,7 +473,7 @@
         setLayoutDirection(mIsRtl ? View.LAYOUT_DIRECTION_RTL : View.LAYOUT_DIRECTION_LTR);
         mTaskTopMargin = getResources()
                 .getDimensionPixelSize(R.dimen.task_thumbnail_top_margin);
-        mRowSpacing = (int) getResources().getDimension(R.dimen.recents_row_spacing);
+        mRowSpacing = getResources().getDimensionPixelSize(R.dimen.overview_grid_row_spacing);
         mSquaredTouchSlop = squaredTouchSlop(context);
 
         mEmptyIcon = context.getDrawable(R.drawable.ic_empty_recents);
@@ -596,9 +598,9 @@
         mLiveTileParams.setSyncTransactionApplier(mSyncTransactionApplier);
         RecentsModel.INSTANCE.get(getContext()).addThumbnailChangeListener(this);
         mIdp.addOnChangeListener(this);
-        mIPinnedStackAnimationListener.setActivity(mActivity);
+        mIPipAnimationListener.setActivity(mActivity);
         SystemUiProxy.INSTANCE.get(getContext()).setPinnedStackAnimationListener(
-                mIPinnedStackAnimationListener);
+                mIPipAnimationListener);
         mOrientationState.initListeners();
         SplitScreenBounds.INSTANCE.addOnChangeListener(this);
         mTaskOverlayFactory.initListeners();
@@ -617,7 +619,7 @@
         mIdp.removeOnChangeListener(this);
         SystemUiProxy.INSTANCE.get(getContext()).setPinnedStackAnimationListener(null);
         SplitScreenBounds.INSTANCE.removeOnChangeListener(this);
-        mIPinnedStackAnimationListener.setActivity(null);
+        mIPipAnimationListener.setActivity(null);
         mOrientationState.destroyListeners();
         mTaskOverlayFactory.removeListeners();
     }
@@ -1012,6 +1014,10 @@
         setPadding(mTempRect.left - mInsets.left, mTempRect.top - mInsets.top,
                 dp.widthPx - mInsets.right - mTempRect.right,
                 dp.heightPx - mInsets.bottom - mTempRect.bottom);
+
+        mSizeStrategy.calculateGridSize(mActivity, mActivity.getDeviceProfile(),
+                mLastComputedGridSize);
+
         // Force TaskView to update size from thumbnail
         updateTaskSize();
     }
@@ -1467,7 +1473,7 @@
             }
         }
         targetTask.setEndQuickswitchCuj(true);
-        targetTask.launchTask(true);
+        targetTask.launchTaskAnimated();
     }
 
     public void setRunningTaskIconScaledDown(boolean isScaledDown) {
@@ -1521,22 +1527,10 @@
         }
 
         final int boxLength = Math.max(mTaskWidth, mTaskHeight);
-
-        float availableHeight =
-                mTaskTopMargin + mTaskHeight + mSizeStrategy.getOverviewActionsHeight(mContext);
+        float availableHeight = mLastComputedGridSize.height();
         float rowHeight = (availableHeight - mRowSpacing) / 2;
         float gridScale = rowHeight / (boxLength + mTaskTopMargin);
 
-        TaskView firstTask = getTaskViewAt(0);
-        float firstTaskWidthOffset;
-        if (mIsRtl) {
-            // Move the first task to the right edge.
-            firstTaskWidthOffset = mTaskWidth - firstTask.getLayoutParams().width * gridScale;
-        } else {
-            // Move the first task to the left edge.
-            firstTaskWidthOffset = -firstTask.getLayoutParams().width * (1 - gridScale);
-        }
-
         int topRowWidth = 0;
         int bottomRowWidth = 0;
         float topAccumulatedTranslationX = 0;
@@ -1546,13 +1540,22 @@
         for (int i = 0; i < taskCount; i++) {
             TaskView taskView = getTaskViewAt(i);
             taskView.setGridScale(gridScale);
+            gridTranslations[i] = 0;
 
-            float taskWidthDiff = mTaskWidth - taskView.getLayoutParams().width * gridScale;
-            float taskWidthOffset = mIsRtl ? taskWidthDiff : -taskWidthDiff;
-            // Visually we want to move all task by firstTaskWidthOffset, but calculate page scroll
-            // according to right edge (or left in nonRtl) of TaskView.
-            gridTranslations[i] = firstTaskWidthOffset - taskWidthOffset;
-            taskView.setGridOffsetTranslationX(taskWidthOffset);
+            float scaledWidth = taskView.getLayoutParams().width * gridScale;
+            float taskGridHorizontalDiff;
+            if (mIsRtl) {
+                float taskRight = mLastComputedTaskSize.left + scaledWidth;
+                taskGridHorizontalDiff = mLastComputedGridSize.right - taskRight;
+            } else {
+                float taskLeft = mLastComputedTaskSize.right - scaledWidth;
+                taskGridHorizontalDiff = mLastComputedGridSize.left - taskLeft;
+            }
+            gridTranslations[i] -= taskGridHorizontalDiff;
+            taskView.setGridOffsetTranslationX(taskGridHorizontalDiff);
+
+            float taskGridVerticalDiff = mLastComputedGridSize.top + mTaskTopMargin * gridScale
+                    - mLastComputedTaskSize.top;
 
             // Off-set gap due to task scaling.
             float widthDiff = taskView.getLayoutParams().width * (1 - gridScale);
@@ -1567,7 +1570,7 @@
                 topRowWidth += taskView.getLayoutParams().width * gridScale + mPageSpacing;
                 topSet.add(i);
 
-                taskView.setGridTranslationY(0);
+                taskView.setGridTranslationY(taskGridVerticalDiff);
 
                 // Move horizontally into empty space.
                 float widthOffset = 0;
@@ -1578,15 +1581,14 @@
 
                 float gridTranslationX = mIsRtl ? widthOffset : -widthOffset;
                 gridTranslations[i] += gridTranslationX;
-                topAccumulatedTranslationX += gridTranslationX + gridScaleTranslationX;
-                bottomAccumulatedTranslationX += gridScaleTranslationX;
+                topAccumulatedTranslationX += gridTranslationX;
             } else {
                 gridTranslations[i] += bottomAccumulatedTranslationX;
                 bottomRowWidth += taskView.getLayoutParams().width * gridScale + mPageSpacing;
 
                 // Move into bottom row.
                 float heightOffset = (boxLength + mTaskTopMargin) * gridScale + mRowSpacing;
-                taskView.setGridTranslationY(heightOffset);
+                taskView.setGridTranslationY(heightOffset + taskGridVerticalDiff);
 
                 // Move horizontally into empty space.
                 float widthOffset = 0;
@@ -1597,9 +1599,10 @@
 
                 float gridTranslationX = mIsRtl ? widthOffset : -widthOffset;
                 gridTranslations[i] += gridTranslationX;
-                topAccumulatedTranslationX += gridScaleTranslationX;
-                bottomAccumulatedTranslationX += gridTranslationX + gridScaleTranslationX;
+                bottomAccumulatedTranslationX += gridTranslationX;
             }
+            topAccumulatedTranslationX += gridScaleTranslationX;
+            bottomAccumulatedTranslationX += gridScaleTranslationX;
         }
 
         // Use the accumulated translation of the longer row.
@@ -1632,8 +1635,9 @@
                     mIsRtl ? -shortTotalCompensation : shortTotalCompensation;
         }
 
-        float clearAllTotalTranslationX = firstTaskWidthOffset + clearAllAccumulatedTranslation
-                + clearAllShorterRowCompensation + clearAllShortTotalCompensation;
+        float clearAllTotalTranslationX =
+                clearAllAccumulatedTranslation + clearAllShorterRowCompensation
+                        + clearAllShortTotalCompensation;
 
         // We need to maintain first task's grid translation at 0, now shift translation of all
         // the TaskViews to achieve that.
@@ -2494,17 +2498,11 @@
         }
         mPendingAnimation.addEndListener(isSuccess -> {
             if (isSuccess) {
-                Consumer<Boolean> onLaunchResult = (result) -> {
-                    onTaskLaunchAnimationEnd(result);
-                    if (!result) {
-                        tv.notifyTaskLaunchFailed(TAG);
-                    }
-                };
                 if (LIVE_TILE.get()) {
                     finishRecentsAnimation(false /* toRecents */, null);
-                    onLaunchResult.accept(true /* success */);
+                    onTaskLaunchAnimationEnd(true /* success */);
                 } else {
-                    tv.launchTask(false, onLaunchResult, getHandler());
+                    tv.launchTask(this::onTaskLaunchAnimationEnd);
                 }
                 Task task = tv.getTask();
                 if (task != null) {
@@ -2773,6 +2771,15 @@
                 taskView.getGridTranslationY());
     }
 
+    /**
+     * Returns the progress of forming a grid from carousel.
+     *
+     * @return A float from 0 to 1 where 0 is a carousel and 1 is a 2 row grid.
+     */
+    public float getGridProgress() {
+        return mGridProgress;
+    }
+
     public Consumer<MotionEvent> getEventDispatcher(float navbarRotation) {
         float degreesRotated;
         if (navbarRotation == 0) {
@@ -2916,7 +2923,7 @@
     }
 
     private static class PinnedStackAnimationListener<T extends BaseActivity> extends
-            IPinnedStackAnimationListener.Stub {
+            IPipAnimationListener.Stub {
         private T mActivity;
 
         public void setActivity(T activity) {
@@ -2924,10 +2931,12 @@
         }
 
         @Override
-        public void onPinnedStackAnimationStarted() {
-            // Needed for activities that auto-enter PiP, which will not trigger a remote
-            // animation to be created
-            mActivity.clearForceInvisibleFlag(STATE_HANDLER_INVISIBILITY_FLAGS);
+        public void onPipAnimationStarted() {
+            MAIN_EXECUTOR.execute(() -> {
+                // Needed for activities that auto-enter PiP, which will not trigger a remote
+                // animation to be created
+                mActivity.clearForceInvisibleFlag(STATE_HANDLER_INVISIBILITY_FLAGS);
+            });
         }
     }
 }
diff --git a/quickstep/src/com/android/quickstep/views/TaskMenuView.java b/quickstep/src/com/android/quickstep/views/TaskMenuView.java
index 2315147..fe7ece2 100644
--- a/quickstep/src/com/android/quickstep/views/TaskMenuView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskMenuView.java
@@ -23,12 +23,14 @@
 import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
 import android.content.Context;
+import android.graphics.Outline;
 import android.graphics.Rect;
 import android.util.AttributeSet;
 import android.view.Gravity;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup;
+import android.view.ViewOutlineProvider;
 import android.widget.LinearLayout;
 import android.widget.TextView;
 
@@ -41,10 +43,10 @@
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.popup.SystemShortcut;
 import com.android.launcher3.touch.PagedOrientationHandler;
-import com.android.launcher3.util.Themes;
 import com.android.launcher3.views.BaseDragLayer;
 import com.android.quickstep.TaskOverlayFactory;
 import com.android.quickstep.TaskUtils;
+import com.android.quickstep.util.TaskCornerRadius;
 
 /**
  * Contains options for a recent task when long-pressing its icon.
@@ -72,6 +74,7 @@
 
         mActivity = BaseDraggingActivity.fromContext(context);
         mThumbnailTopMargin = getResources().getDimension(R.dimen.task_thumbnail_top_margin);
+        setClipToOutline(true);
     }
 
     @Override
@@ -108,6 +111,17 @@
         return (type & TYPE_TASK_MENU) != 0;
     }
 
+    @Override
+    public ViewOutlineProvider getOutlineProvider() {
+        return new ViewOutlineProvider() {
+            @Override
+            public void getOutline(View view, Outline outline) {
+                outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(),
+                        TaskCornerRadius.get(view.getContext()));
+            }
+        };
+    }
+
     public void setPosition(float x, float y, PagedOrientationHandler pagedOrientationHandler) {
         float adjustedY = y + mThumbnailTopMargin;
         // Changing pivot to make computations easier
@@ -260,7 +274,7 @@
     }
 
     private RoundedRectRevealOutlineProvider createOpenCloseOutlineProvider() {
-        float radius = Themes.getDialogCornerRadius(getContext());
+        float radius = TaskCornerRadius.get(mContext);
         Rect fromRect = new Rect(0, 0, getWidth(), 0);
         Rect toRect = new Rect(0, 0, getWidth(), getHeight());
         return new RoundedRectRevealOutlineProvider(radius, radius, fromRect, toRect);
diff --git a/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java b/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java
index 4c21745..36a5f03 100644
--- a/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java
@@ -212,13 +212,6 @@
         return mDimAlpha;
     }
 
-    public Rect getInsets(Rect fallback) {
-        if (mThumbnailData != null) {
-            return mThumbnailData.insets;
-        }
-        return fallback;
-    }
-
     /**
      * Get the scaled insets that are being used to draw the task view. This is a subsection of
      * the full snapshot.
@@ -230,6 +223,10 @@
             return Insets.NONE;
         }
 
+        if (!TaskView.CLIP_STATUS_AND_NAV_BARS) {
+            return Insets.NONE;
+        }
+
         RectF bitmapRect = new RectF(
                 0, 0,
                 mThumbnailData.thumbnail.getWidth(), mThumbnailData.thumbnail.getHeight());
@@ -459,7 +456,6 @@
         // Contains the portion of the thumbnail that is clipped when fullscreen progress = 0.
         private final RectF mClippedInsets = new RectF();
         private final Matrix mMatrix = new Matrix();
-        private float mClipBottom = -1;
         private boolean mIsOrientationChanged;
 
         public Matrix getMatrix() {
@@ -476,7 +472,8 @@
 
             int thumbnailRotation = thumbnailData.rotation;
             int deltaRotate = getRotationDelta(currentRotation, thumbnailRotation);
-            RectF thumbnailClipHint = new RectF(thumbnailData.insets);
+            RectF thumbnailClipHint = TaskView.CLIP_STATUS_AND_NAV_BARS
+                    ? new RectF(thumbnailData.insets) : new RectF();
 
             float scale = thumbnailData.scale;
             final float thumbnailScale;
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java
index 88545c6..eace0f8 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskView.java
@@ -27,7 +27,7 @@
 import static android.view.Surface.ROTATION_90;
 import static android.widget.Toast.LENGTH_SHORT;
 
-import static com.android.launcher3.QuickstepAppTransitionManagerImpl.RECENTS_LAUNCH_DURATION;
+import static com.android.launcher3.QuickstepTransitionManager.RECENTS_LAUNCH_DURATION;
 import static com.android.launcher3.Utilities.comp;
 import static com.android.launcher3.Utilities.getDescendantCoordRelativeToAncestor;
 import static com.android.launcher3.anim.Interpolators.ACCEL_DEACCEL;
@@ -37,6 +37,7 @@
 import static com.android.launcher3.anim.Interpolators.TOUCH_RESPONSE_INTERPOLATOR;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_ICON_TAP_OR_LONGPRESS;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_LAUNCH_TAP;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 import static com.android.quickstep.util.NavigationModeFeatureFlag.LIVE_TILE;
 
@@ -52,7 +53,6 @@
 import android.graphics.RectF;
 import android.graphics.drawable.Drawable;
 import android.os.Bundle;
-import android.os.Handler;
 import android.util.AttributeSet;
 import android.util.FloatProperty;
 import android.util.Log;
@@ -66,6 +66,8 @@
 import android.widget.FrameLayout;
 import android.widget.Toast;
 
+import androidx.annotation.NonNull;
+
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.R;
@@ -79,7 +81,9 @@
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.touch.PagedOrientationHandler;
+import com.android.launcher3.util.ActivityOptionsWrapper;
 import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.RunnableList;
 import com.android.launcher3.util.TransformingTouchDelegate;
 import com.android.launcher3.util.ViewPool.Reusable;
 import com.android.quickstep.RecentsModel;
@@ -117,6 +121,12 @@
      */
     public static final float MAX_PAGE_SCRIM_ALPHA = 0.4f;
 
+    /**
+     * Should the TaskView display clip off the status and navigation bars in recents. When this
+     * is false the overview shows the whole screen scaled down instead.
+     */
+    public static final boolean CLIP_STATUS_AND_NAV_BARS = false;
+
     private static final float EDGE_SCALE_DOWN_FACTOR_CAROUSEL = 0.03f;
     private static final float EDGE_SCALE_DOWN_FACTOR_GRID = 0.00f;
 
@@ -335,7 +345,7 @@
                 });
                 anim.start();
             } else {
-                launchTask(true /* animate */);
+                launchTaskAnimated();
             }
             mActivity.getStatsLogManager().logger().withItemInfo(getItemInfo())
                     .log(LAUNCHER_TASK_LAUNCH_TAP);
@@ -483,63 +493,62 @@
                 .createPlaybackController();
     }
 
-    public void launchTask(boolean animate) {
-        launchTask(animate, false /* freezeTaskList */);
-    }
-
-    public void launchTask(boolean animate, boolean freezeTaskList) {
-        launchTask(animate, freezeTaskList, (result) -> {
-            if (!result) {
-                notifyTaskLaunchFailed(TAG);
-            }
-        }, getHandler());
-    }
-
-    public void launchTask(boolean animate, Consumer<Boolean> resultCallback,
-            Handler resultCallbackHandler) {
-        launchTask(animate, false /* freezeTaskList */, resultCallback, resultCallbackHandler);
-    }
-
-    public void launchTask(boolean animate, boolean freezeTaskList, Consumer<Boolean> resultCallback,
-            Handler resultCallbackHandler) {
+    /**
+     * Starts the task associated with this view and animates the startup.
+     * @return CompletionStage to indicate the animation completion or null if the launch failed.
+     */
+    public RunnableList launchTaskAnimated() {
         if (mTask != null) {
-            final ActivityOptions opts;
             TestLogging.recordEvent(
                     TestProtocol.SEQUENCE_MAIN, "startActivityFromRecentsAsync", mTask);
-            if (animate) {
-                opts = mActivity.getActivityLaunchOptions(this);
-                if (freezeTaskList) {
-                    ActivityOptionsCompat.setFreezeRecentTasksList(opts);
-                }
-                ActivityManagerWrapper.getInstance().startActivityFromRecentsAsync(mTask.key,
-                        opts, resultCallback, resultCallbackHandler);
+            ActivityOptionsWrapper opts =  mActivity.getActivityLaunchOptions(this);
+            if (ActivityManagerWrapper.getInstance()
+                    .startActivityFromRecents(mTask.key, opts.options)) {
+                return opts.onEndCallback;
             } else {
-                opts = ActivityOptionsCompat.makeCustomAnimation(getContext(), 0, 0, () -> {
-                    if (resultCallback != null) {
-                        // Only post the animation start after the system has indicated that the
-                        // transition has started
-                        resultCallbackHandler.post(() -> resultCallback.accept(true));
-                    }
-                }, resultCallbackHandler);
-                if (freezeTaskList) {
-                    ActivityOptionsCompat.setFreezeRecentTasksList(opts);
-                }
-                UI_HELPER_EXECUTOR.execute(
-                        () -> ActivityManagerWrapper.getInstance().startActivityFromRecentsAsync(
-                                mTask.key,
-                                opts,
-                                (success) -> {
-                                    if (resultCallback != null && !success) {
-                                        // If the call to start activity failed, then post the
-                                        // result
-                                        // immediately, otherwise, wait for the animation start
-                                        // callback
-                                        // from the activity options above
-                                        resultCallbackHandler.post(
-                                                () -> resultCallback.accept(false));
-                                    }
-                                }, resultCallbackHandler));
+                notifyTaskLaunchFailed(TAG);
+                return null;
             }
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Starts the task associated with this view without any animation
+     */
+    public void launchTask(@NonNull Consumer<Boolean> callback) {
+        launchTask(callback, false /* freezeTaskList */);
+    }
+
+    /**
+     * Starts the task associated with this view without any animation
+     */
+    public void launchTask(@NonNull Consumer<Boolean> callback, boolean freezeTaskList) {
+        if (mTask != null) {
+            TestLogging.recordEvent(
+                    TestProtocol.SEQUENCE_MAIN, "startActivityFromRecentsAsync", mTask);
+
+            // Indicate success once the system has indicated that the transition has started
+            ActivityOptions opts = ActivityOptionsCompat.makeCustomAnimation(
+                    getContext(), 0, 0, () -> callback.accept(true), MAIN_EXECUTOR.getHandler());
+            if (freezeTaskList) {
+                ActivityOptionsCompat.setFreezeRecentTasksList(opts);
+            }
+            Task.TaskKey key = mTask.key;
+            UI_HELPER_EXECUTOR.execute(() -> {
+                if (!ActivityManagerWrapper.getInstance().startActivityFromRecents(key, opts)) {
+                    // If the call to start activity failed, then post the result immediately,
+                    // otherwise, wait for the animation start callback from the activity options
+                    // above
+                    MAIN_EXECUTOR.post(() -> {
+                        notifyTaskLaunchFailed(TAG);
+                        callback.accept(false);
+                    });
+                }
+            });
+        } else {
+            callback.accept(false);
         }
     }
 
@@ -617,12 +626,11 @@
         int thumbnailPadding = (int) getResources().getDimension(R.dimen.task_thumbnail_top_margin);
         int taskIconMargin = (int) getResources().getDimension(R.dimen.task_icon_top_margin);
         int taskIconHeight = (int) getResources().getDimension(R.dimen.task_thumbnail_icon_size);
-        int iconTopMargin = taskIconMargin - taskIconHeight + thumbnailPadding;
         LayoutParams iconParams = (LayoutParams) mIconView.getLayoutParams();
         switch (orientationHandler.getRotation()) {
             case ROTATION_90:
                 iconParams.gravity = (isRtl ? START : END) | CENTER_VERTICAL;
-                iconParams.rightMargin = -thumbnailPadding;
+                iconParams.rightMargin = -taskIconHeight - taskIconMargin / 2;
                 iconParams.leftMargin = 0;
                 iconParams.topMargin = snapshotParams.topMargin / 2;
                 break;
@@ -630,11 +638,11 @@
                 iconParams.gravity = BOTTOM | CENTER_HORIZONTAL;
                 iconParams.bottomMargin = -thumbnailPadding;
                 iconParams.leftMargin = iconParams.rightMargin = 0;
-                iconParams.topMargin = iconTopMargin;
+                iconParams.topMargin = taskIconMargin;
                 break;
             case ROTATION_270:
                 iconParams.gravity = (isRtl ? END : START) | CENTER_VERTICAL;
-                iconParams.leftMargin = -thumbnailPadding;
+                iconParams.leftMargin = -taskIconHeight - taskIconMargin / 2;
                 iconParams.rightMargin = 0;
                 iconParams.topMargin = snapshotParams.topMargin / 2;
                 break;
@@ -642,7 +650,7 @@
             default:
                 iconParams.gravity = TOP | CENTER_HORIZONTAL;
                 iconParams.leftMargin = iconParams.rightMargin = 0;
-                iconParams.topMargin = iconTopMargin;
+                iconParams.topMargin = taskIconMargin;
                 break;
         }
         mIconView.setLayoutParams(iconParams);
@@ -769,7 +777,7 @@
         }
         if (view != null) {
             mContextualChipWrapper = view;
-            LayoutParams layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT,
+            LayoutParams layoutParams = new LayoutParams(((View) getParent()).getMeasuredWidth(),
                     LayoutParams.WRAP_CONTENT);
             layoutParams.gravity = BOTTOM | CENTER_HORIZONTAL;
             int expectedChipHeight = getExpectedViewHeight(view);
@@ -1123,7 +1131,7 @@
         return getRecentsView().mOrientationState.getOrientationHandler();
     }
 
-    public void notifyTaskLaunchFailed(String tag) {
+    private void notifyTaskLaunchFailed(String tag) {
         String msg = "Failed to launch task";
         if (mTask != null) {
             msg += " (task=" + mTask.key.baseIntent + " userId=" + mTask.key.userId + ")";
@@ -1188,7 +1196,8 @@
 
             int expectedWidth;
             int expectedHeight;
-            float thumbnailRatio = mTask != null ? mTask.getVisibleThumbnailRatio() : 0f;
+            float thumbnailRatio = mTask != null ? mTask.getVisibleThumbnailRatio(
+                    TaskView.CLIP_STATUS_AND_NAV_BARS) : 0f;
             if (isRunningTask() || thumbnailRatio == 0f) {
                 expectedWidth = taskWidth;
                 expectedHeight = taskHeight + thumbnailPadding;
diff --git a/res/drawable/ic_gm_close_24.xml b/res/drawable/ic_gm_close_24.xml
new file mode 100644
index 0000000..2c9c932
--- /dev/null
+++ b/res/drawable/ic_gm_close_24.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+    <path
+        android:fillColor="?android:attr/textColorTertiary"
+        android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12 19,6.41z"/>
+</vector>
diff --git a/res/layout/widgets_full_sheet.xml b/res/layout/widgets_full_sheet.xml
index 28a8c6f..226c4f7 100644
--- a/res/layout/widgets_full_sheet.xml
+++ b/res/layout/widgets_full_sheet.xml
@@ -15,6 +15,7 @@
 -->
 <com.android.launcher3.widget.picker.WidgetsFullSheet
     xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:orientation="vertical"
@@ -27,6 +28,14 @@
         android:background="?android:attr/colorPrimary"
         android:elevation="4dp">
 
+        <TextView
+            android:id="@+id/no_widgets_text"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:gravity="center"
+            android:visibility="gone"
+            tools:text="No widgets available" />
+
         <!-- Fast scroller popup -->
         <TextView
             android:id="@+id/fast_scroller_popup"
@@ -42,5 +51,13 @@
             android:layout_alignParentEnd="true"
             android:layout_alignParentTop="true"
             android:layout_marginEnd="@dimen/fastscroll_end_margin" />
+
+        <com.android.launcher3.widget.picker.WidgetsRecyclerView
+            android:id="@+id/search_widgets_list_view"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:visibility="gone"
+            android:clipToPadding="false" />
+
     </com.android.launcher3.views.TopRoundedCornerView>
 </com.android.launcher3.widget.picker.WidgetsFullSheet>
\ No newline at end of file
diff --git a/res/layout/widgets_full_sheet_search_and_recommendations.xml b/res/layout/widgets_full_sheet_search_and_recommendations.xml
index 9a6f922..6182255 100644
--- a/res/layout/widgets_full_sheet_search_and_recommendations.xml
+++ b/res/layout/widgets_full_sheet_search_and_recommendations.xml
@@ -34,16 +34,5 @@
         android:textSize="24sp"
         android:layout_marginTop="16dp"
         android:text="@string/widget_button_text"/>
-    <!-- Disable the search bar because it has not been implemented. -->
-    <EditText
-        android:id="@+id/widgets_search_bar"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:visibility="gone"
-        android:layout_marginTop="16dp"
-        android:background="@drawable/bg_widgets_searchbox"
-        android:drawablePadding="8dp"
-        android:drawableStart="@drawable/ic_allapps_search"
-        android:hint="@string/widgets_full_sheet_search_bar_hint"
-        android:padding="12dp" />
+    <include layout="@layout/widgets_search_bar"/>
 </LinearLayout>
diff --git a/res/layout/widgets_list_row_header.xml b/res/layout/widgets_list_row_header.xml
index 041e007..1590286 100644
--- a/res/layout/widgets_list_row_header.xml
+++ b/res/layout/widgets_list_row_header.xml
@@ -19,7 +19,7 @@
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     android:background="?android:attr/selectableItemBackground"
-    android:paddingVertical="20dp"
+    android:paddingVertical="@dimen/widget_list_header_view_vertical_padding"
     android:orientation="horizontal">
 
     <ImageView
@@ -52,6 +52,8 @@
             android:id="@+id/app_subtitle"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
+            android:ellipsize="end"
+            android:maxLines="1"
             tools:text="m widgets, n shortcuts" />
 
     </LinearLayout>
diff --git a/res/layout/widgets_search_bar.xml b/res/layout/widgets_search_bar.xml
new file mode 100644
index 0000000..252637d
--- /dev/null
+++ b/res/layout/widgets_search_bar.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<com.android.launcher3.widget.picker.search.WidgetsSearchBar
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/widgets_search_bar"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="horizontal"
+    android:layout_marginTop="16dp"
+    android:background="@drawable/bg_widgets_searchbox"
+    android:padding="12dp"
+    android:visibility="gone">
+
+    <EditText
+        android:id="@+id/widgets_search_bar_edit_text"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:drawablePadding="8dp"
+        android:drawableStart="@drawable/ic_allapps_search"
+        android:background="@null"
+        android:hint="@string/widgets_full_sheet_search_bar_hint"
+        android:maxLines="1"
+        android:layout_weight="1"
+        android:inputType="text"/>
+
+    <ImageButton
+        android:id="@+id/widgets_search_cancel_button"
+        android:layout_height="wrap_content"
+        android:layout_width="wrap_content"
+        android:src="@drawable/ic_gm_close_24"
+        android:background="?android:selectableItemBackground"
+        android:layout_gravity="center"
+        android:visibility="gone"/>
+</com.android.launcher3.widget.picker.search.WidgetsSearchBar>
\ No newline at end of file
diff --git a/res/values/config.xml b/res/values/config.xml
index 89415b8..65e2ab3 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -61,7 +61,6 @@
     <!-- Various classes overriden by projects/build flavors. -->
     <string name="folder_name_provider_class" translatable="false"></string>
     <string name="stats_log_manager_class" translatable="false"></string>
-    <string name="app_transition_manager_class" translatable="false"></string>
     <string name="instant_app_resolver_class" translatable="false"></string>
     <string name="main_process_initializer_class" translatable="false"></string>
     <string name="app_launch_tracker_class" translatable="false"></string>
@@ -90,6 +89,7 @@
     <string name="wallpaper_picker_package" translatable="false"></string>
     <string name="calendar_component_name" translatable="false"></string>
     <string name="clock_component_name" translatable="false"></string>
+    <string name="local_colors_extraction_class" translatable="false"></string>
 
     <!-- Accessibility actions -->
     <item type="id" name="action_remove" />
@@ -188,4 +188,8 @@
     </string-array>
 
     <string-array name="filtered_components" ></string-array>
+
+    <!-- Name of the class used to generate colors from the wallpaper colors. Must be implementing the LauncherAppWidgetHostView.ColorGenerator interface. -->
+    <string name="color_generator_class" translatable="false"/>
+
 </resources>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index da43758..d135b43 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -108,6 +108,8 @@
     <dimen name="widget_cell_vertical_padding">8dp</dimen>
     <dimen name="widget_cell_horizontal_padding">16dp</dimen>
 
+    <dimen name="widget_list_header_view_vertical_padding">20dp</dimen>
+
     <dimen name="widget_preview_shadow_blur">0.5dp</dimen>
     <dimen name="widget_preview_key_shadow_distance">1dp</dimen>
     <dimen name="widget_preview_corner_radius">2dp</dimen>
diff --git a/res/values/id.xml b/res/values/id.xml
new file mode 100644
index 0000000..39c49bd
--- /dev/null
+++ b/res/values/id.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    Copyright (C) 2020 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.
+-->
+<resources>
+    <item type="id" name="view_type_widgets_list" />
+    <item type="id" name="view_type_widgets_header" />
+    <item type="id" name="view_type_widgets_search_header" />
+</resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 44b5ee7..351182d 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -40,9 +40,9 @@
 
     <!-- Widgets -->
     <!-- Message to tell the user to press and hold on a widget to add it [CHAR_LIMIT=50] -->
-    <string name="long_press_widget_to_add">Touch &amp; hold to pick up a widget.</string>
+    <string name="long_press_widget_to_add">Touch &amp; hold to move a widget.</string>
     <!-- Accessibility spoken hint message in widget picker, which allows user to add a widget. Custom action is the label for additional accessibility actions available in this mode [CHAR_LIMIT=100] -->
-    <string name="long_accessible_way_to_add">Double-tap &amp; hold to pick up a widget or use custom actions.</string>
+    <string name="long_accessible_way_to_add">Double-tap &amp; hold to move a widget or use custom actions.</string>
     <!-- The format string for the dimensions of a widget in the drawer -->
     <!-- There is a special version of this format string for Farsi -->
     <string name="widget_dims_format">%1$d \u00d7 %2$d</string>
@@ -74,6 +74,9 @@
     <!-- Search bar text shown in the popup view showing all available widgets installed on the
          device. [CHAR_LIMIT=50] -->
     <string name="widgets_full_sheet_search_bar_hint">Search</string>
+    <!-- Text shown when there is no widgets shown in the popup view showing all available widgets
+         installed on the device. [CHAR_LIMIT=none] -->
+    <string name="no_widgets_available">No widgets available</string>
 
     <!-- All Apps -->
     <!-- Search bar text in the apps view. [CHAR_LIMIT=50] -->
@@ -101,9 +104,9 @@
 
     <!-- Drag and drop -->
     <!-- Message to tell the user to press and hold on a shortcut to add it [CHAR_LIMIT=50] -->
-    <string name="long_press_shortcut_to_add">Touch &amp; hold to pick up a shortcut.</string>
+    <string name="long_press_shortcut_to_add">Touch &amp; hold to move a shortcut.</string>
     <!-- Accessibility spoken hint message in deep shortcut menu, which allows user to add a shortcut. Custom action is the label for additional accessibility actions available in this mode [CHAR_LIMIT=200] -->
-    <string name="long_accessible_way_to_add_shortcut">Double-tap &amp; hold to pick up a shortcut or use custom actions.</string>
+    <string name="long_accessible_way_to_add_shortcut">Double-tap &amp; hold to move a shortcut or use custom actions.</string>
 
     <skip />
     <!-- Error message when user has filled a home screen -->
diff --git a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsDiffReporterTest.java b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsDiffReporterTest.java
index b972c6f..cc36f63 100644
--- a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsDiffReporterTest.java
+++ b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsDiffReporterTest.java
@@ -221,6 +221,27 @@
         assertThat(currentList).containsExactlyElementsIn(newList);
     }
 
+    @Test
+    public void headersContentsMix_headerWidgetsModified_shouldInvokeCorrectCallbacks() {
+        // GIVEN the current list has app headers [A, B, E content].
+        ArrayList<WidgetsListBaseEntry> currentList = new ArrayList<>(
+                List.of(mHeaderA, mHeaderB, mContentE));
+        // GIVEN the new list has one of the headers widgets list modified.
+        List<WidgetsListBaseEntry> newList = List.of(
+                new WidgetsListHeaderEntry(
+                        mHeaderA.mPkgItem, mHeaderA.mTitleSectionName,
+                        mHeaderA.mWidgets.subList(0, 1)),
+                mHeaderB, mContentE);
+
+        // WHEN computing the list difference.
+        mWidgetsDiffReporter.process(currentList, newList, COMPARATOR);
+
+        // THEN notify "A" has been changed.
+        verify(mAdapter).notifyItemChanged(/* position= */ 0);
+        // THEN the current list contains all elements from the new list.
+        assertThat(currentList).containsExactlyElementsIn(newList);
+    }
+
 
     private WidgetsListHeaderEntry createWidgetsHeaderEntry(String packageName, String appName,
             int numOfWidgets) {
diff --git a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListAdapterTest.java b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListAdapterTest.java
index a7c8d92..e1214ff 100644
--- a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListAdapterTest.java
+++ b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListAdapterTest.java
@@ -26,6 +26,8 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.graphics.Bitmap;
+import android.os.Process;
+import android.os.UserHandle;
 import android.view.LayoutInflater;
 
 import androidx.recyclerview.widget.RecyclerView;
@@ -37,6 +39,7 @@
 import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.model.WidgetItem;
 import com.android.launcher3.model.data.PackageItemInfo;
+import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.widget.model.WidgetsListBaseEntry;
 import com.android.launcher3.widget.model.WidgetsListContentEntry;
@@ -67,6 +70,7 @@
 
     private WidgetsListAdapter mAdapter;
     private InvariantDeviceProfile mTestProfile;
+    private UserHandle mUserHandle;
     private Context mContext;
 
     @Before
@@ -76,6 +80,7 @@
         mTestProfile = new InvariantDeviceProfile();
         mTestProfile.numRows = 5;
         mTestProfile.numColumns = 5;
+        mUserHandle = Process.myUserHandle();
         mAdapter = new WidgetsListAdapter(mContext, mMockLayoutInflater, mMockWidgetCache,
                 mIconCache, null, null);
         mAdapter.registerAdapterDataObserver(mListener);
@@ -126,7 +131,8 @@
         mAdapter.setWidgets(generateSampleMap(3));
 
         // WHEN com.google.test.1 header is expanded.
-        mAdapter.onHeaderClicked(/* isExpanded= */ true, TEST_PACKAGE_PLACEHOLDER + 1);
+        mAdapter.onHeaderClicked(/* showWidgets= */ true,
+                new PackageUserKey(TEST_PACKAGE_PLACEHOLDER + 1, mUserHandle));
 
         // THEN the visible entries list becomes:
         // [com.google.test0, com.google.test1, com.google.test1 content, com.google.test2]
@@ -143,7 +149,8 @@
         // GIVEN test com.google.test1 is expanded.
         // Visible entries in the adapter are:
         // [com.google.test0, com.google.test1, com.google.test1 content]
-        mAdapter.onHeaderClicked(/* isExpanded= */ true, TEST_PACKAGE_PLACEHOLDER + 1);
+        mAdapter.onHeaderClicked(/* showWidgets= */ true,
+                new PackageUserKey(TEST_PACKAGE_PLACEHOLDER + 1, mUserHandle));
         Mockito.reset(mListener);
 
         // WHEN the adapter is updated with the same list of apps but com.google.test1 has 2 widgets
@@ -200,6 +207,30 @@
         verify(mListener).onItemRangeRemoved(/* positionStart= */ 3, /* itemCount= */ 1);
     }
 
+    @Test
+    public void setWidgetsOnSearch_expandedApp_shouldResetExpandedApp() {
+        // GIVEN a list of widgets entries:
+        // [com.google.test0, com.google.test0 content,
+        //  com.google.test1, com.google.test1 content,
+        //  com.google.test2, com.google.test2 content]
+        // The visible widgets entries: [com.google.test0, com.google.test1, com.google.test2].
+        ArrayList<WidgetsListBaseEntry> allEntries = generateSampleMap(2);
+        mAdapter.setWidgetsOnSearch(allEntries);
+        // GIVEN com.google.test.1 header is expanded. The visible entries list becomes:
+        // [com.google.test0, com.google.test1, com.google.test1 content, com.google.test2]
+        mAdapter.onHeaderClicked(/* showWidgets= */ true,
+                new PackageUserKey(TEST_PACKAGE_PLACEHOLDER + 1, mUserHandle));
+        Mockito.reset(mListener);
+
+        // WHEN same widget entries are set again.
+        mAdapter.setWidgetsOnSearch(allEntries);
+
+        // THEN expanded app is reset and the visible entries list becomes:
+        // [com.google.test0, com.google.test1, com.google.test2]
+        verify(mListener).onItemRangeChanged(eq(1), eq(1), isNull());
+        verify(mListener).onItemRangeRemoved(/* positionStart= */ 2, /* itemCount= */ 1);
+    }
+
     /**
      * Generates a list of sample widget entries.
      *
diff --git a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListHeaderViewHolderBinderTest.java b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListHeaderViewHolderBinderTest.java
index 848630e..e8c11da 100644
--- a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListHeaderViewHolderBinderTest.java
+++ b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListHeaderViewHolderBinderTest.java
@@ -18,7 +18,9 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.verify;
 import static org.robolectric.Shadows.shadowOf;
 
 import android.appwidget.AppWidgetProviderInfo;
@@ -26,12 +28,9 @@
 import android.content.Context;
 import android.graphics.Bitmap;
 import android.view.LayoutInflater;
-import android.view.View;
 import android.widget.FrameLayout;
 import android.widget.TextView;
 
-import androidx.annotation.Nullable;
-
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.R;
@@ -41,10 +40,9 @@
 import com.android.launcher3.model.WidgetItem;
 import com.android.launcher3.model.data.PackageItemInfo;
 import com.android.launcher3.testing.TestActivity;
+import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
-import com.android.launcher3.widget.WidgetCell;
 import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
-import com.android.launcher3.widget.picker.WidgetsListHeaderViewHolderBinder.OnHeaderClickListener;
 
 import org.junit.After;
 import org.junit.Before;
@@ -74,12 +72,13 @@
     // testing.
     private ActivityController<TestActivity> mActivityController;
     private TestActivity mTestActivity;
-    private FakeOnHeaderClickListener mFakeOnHeaderClickListener = new FakeOnHeaderClickListener();
 
     @Mock
     private IconCache mIconCache;
     @Mock
     private DeviceProfile mDeviceProfile;
+    @Mock
+    private OnHeaderClickListener mOnHeaderClickListener;
 
     @Before
     public void setUp() {
@@ -99,8 +98,7 @@
         }).when(mIconCache).getTitleNoCache(any());
 
         mViewHolderBinder = new WidgetsListHeaderViewHolderBinder(
-                LayoutInflater.from(mTestActivity),
-                mFakeOnHeaderClickListener);
+                LayoutInflater.from(mTestActivity), mOnHeaderClickListener);
     }
 
     @After
@@ -125,6 +123,23 @@
         assertThat(appSubtitle.getText()).isEqualTo("3 widgets");
     }
 
+    @Test
+    public void bindViewHolder_shouldAttachOnHeaderClickListener() {
+        WidgetsListHeaderHolder viewHolder = mViewHolderBinder.newViewHolder(
+                new FrameLayout(mTestActivity));
+        WidgetsListHeader widgetsListHeader = viewHolder.mWidgetsListHeader;
+        WidgetsListHeaderEntry entry = generateSampleAppHeader(
+                APP_NAME,
+                TEST_PACKAGE,
+                /* numOfWidgets= */ 3);
+
+        mViewHolderBinder.bindViewHolder(viewHolder, entry);
+        widgetsListHeader.callOnClick();
+
+        verify(mOnHeaderClickListener).onHeaderClicked(eq(true),
+                eq(new PackageUserKey(entry.mPkgItem.packageName, entry.mPkgItem.user)));
+    }
+
     private WidgetsListHeaderEntry generateSampleAppHeader(String appName, String packageName,
             int numOfWidgets) {
         PackageItemInfo appInfo = new PackageItemInfo(packageName);
@@ -152,22 +167,4 @@
         }
         return widgetItems;
     }
-
-    private void assertWidgetCellWithLabel(View view, String label) {
-        assertThat(view).isInstanceOf(WidgetCell.class);
-        TextView widgetLabel = (TextView) view.findViewById(R.id.widget_name);
-        assertThat(widgetLabel.getText()).isEqualTo(label);
-    }
-
-    private final class FakeOnHeaderClickListener implements OnHeaderClickListener {
-
-        boolean mShowWidgets = false;
-        @Nullable  String mHeaderClickedPackage = null;
-
-        @Override
-        public void onHeaderClicked(boolean showWidgets, String packageName) {
-            mShowWidgets = showWidgets;
-            mHeaderClickedPackage = packageName;
-        }
-    }
 }
diff --git a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListSearchHeaderViewHolderBinderTest.java b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListSearchHeaderViewHolderBinderTest.java
new file mode 100644
index 0000000..07fbfd2
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListSearchHeaderViewHolderBinderTest.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright (C) 2021 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.widget.picker;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.verify;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.appwidget.AppWidgetProviderInfo;
+import android.content.ComponentName;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.view.LayoutInflater;
+import android.widget.FrameLayout;
+import android.widget.TextView;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.R;
+import com.android.launcher3.icons.BitmapInfo;
+import com.android.launcher3.icons.ComponentWithLabel;
+import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.model.WidgetItem;
+import com.android.launcher3.model.data.PackageItemInfo;
+import com.android.launcher3.testing.TestActivity;
+import com.android.launcher3.util.PackageUserKey;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
+import com.android.launcher3.widget.model.WidgetsListSearchHeaderEntry;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.android.controller.ActivityController;
+import org.robolectric.shadows.ShadowPackageManager;
+import org.robolectric.util.ReflectionHelpers;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RunWith(RobolectricTestRunner.class)
+public final class WidgetsListSearchHeaderViewHolderBinderTest {
+    private static final String TEST_PACKAGE = "com.google.test";
+    private static final String APP_NAME = "Test app";
+
+    private Context mContext;
+    private WidgetsListSearchHeaderViewHolderBinder mViewHolderBinder;
+    private InvariantDeviceProfile mTestProfile;
+    // Replace ActivityController with ActivityScenario, which is the recommended way for activity
+    // testing.
+    private ActivityController<TestActivity> mActivityController;
+    private TestActivity mTestActivity;
+
+    @Mock
+    private IconCache mIconCache;
+    @Mock
+    private DeviceProfile mDeviceProfile;
+    @Mock
+    private OnHeaderClickListener mOnHeaderClickListener;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mContext = RuntimeEnvironment.application;
+        mTestProfile = new InvariantDeviceProfile();
+        mTestProfile.numRows = 5;
+        mTestProfile.numColumns = 5;
+
+        mActivityController = Robolectric.buildActivity(TestActivity.class);
+        mTestActivity = mActivityController.setup().get();
+        mTestActivity.setDeviceProfile(mDeviceProfile);
+
+        doAnswer(invocation -> {
+            ComponentWithLabel componentWithLabel = (ComponentWithLabel) invocation.getArgument(0);
+            return componentWithLabel.getComponent().getShortClassName();
+        }).when(mIconCache).getTitleNoCache(any());
+
+        mViewHolderBinder = new WidgetsListSearchHeaderViewHolderBinder(
+                LayoutInflater.from(mTestActivity), mOnHeaderClickListener);
+    }
+
+    @After
+    public void tearDown() {
+        mActivityController.destroy();
+    }
+
+    @Test
+    public void bindViewHolder_appWith3Widgets_shouldShowTheCorrectAppNameAndSubtitle() {
+        WidgetsListSearchHeaderHolder viewHolder = mViewHolderBinder.newViewHolder(
+                new FrameLayout(mTestActivity));
+        WidgetsListHeader widgetsListHeader = viewHolder.mWidgetsListHeader;
+        WidgetsListSearchHeaderEntry entry = generateSampleSearchHeader(
+                APP_NAME,
+                TEST_PACKAGE,
+                /* numOfWidgets= */ 3);
+        mViewHolderBinder.bindViewHolder(viewHolder, entry);
+
+        TextView appTitle = widgetsListHeader.findViewById(R.id.app_title);
+        TextView appSubtitle = widgetsListHeader.findViewById(R.id.app_subtitle);
+        assertThat(appTitle.getText()).isEqualTo(APP_NAME);
+        assertThat(appSubtitle.getText())
+                .isEqualTo(".SampleWidget0, .SampleWidget1, .SampleWidget2");
+    }
+
+    @Test
+    public void bindViewHolder_shouldAttachOnHeaderClickListener() {
+        WidgetsListSearchHeaderHolder viewHolder = mViewHolderBinder.newViewHolder(
+                new FrameLayout(mTestActivity));
+        WidgetsListHeader widgetsListHeader = viewHolder.mWidgetsListHeader;
+        WidgetsListSearchHeaderEntry entry = generateSampleSearchHeader(
+                APP_NAME,
+                TEST_PACKAGE,
+                /* numOfWidgets= */ 3);
+
+        mViewHolderBinder.bindViewHolder(viewHolder, entry);
+        widgetsListHeader.callOnClick();
+
+        verify(mOnHeaderClickListener).onHeaderClicked(eq(true),
+                eq(new PackageUserKey(entry.mPkgItem.packageName, entry.mPkgItem.user)));
+    }
+
+    private WidgetsListSearchHeaderEntry generateSampleSearchHeader(String appName,
+            String packageName, int numOfWidgets) {
+        PackageItemInfo appInfo = new PackageItemInfo(packageName);
+        appInfo.title = appName;
+        appInfo.bitmap = BitmapInfo.of(Bitmap.createBitmap(10, 10, Bitmap.Config.ALPHA_8), 0);
+
+        return new WidgetsListSearchHeaderEntry(appInfo,
+                /* titleSectionName= */ "",
+                generateWidgetItems(packageName, numOfWidgets));
+    }
+
+    private List<WidgetItem> generateWidgetItems(String packageName, int numOfWidgets) {
+        ShadowPackageManager packageManager = shadowOf(mContext.getPackageManager());
+        ArrayList<WidgetItem> widgetItems = new ArrayList<>();
+        for (int i = 0; i < numOfWidgets; i++) {
+            ComponentName cn = ComponentName.createRelative(packageName, ".SampleWidget" + i);
+            AppWidgetProviderInfo widgetInfo = new AppWidgetProviderInfo();
+            widgetInfo.provider = cn;
+            ReflectionHelpers.setField(widgetInfo, "providerInfo",
+                    packageManager.addReceiverIfNotPresent(cn));
+
+            widgetItems.add(new WidgetItem(
+                    LauncherAppWidgetProviderInfo.fromProviderInfo(mContext, widgetInfo),
+                    mTestProfile, mIconCache));
+        }
+        return widgetItems;
+    }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/widget/picker/search/SimpleWidgetsSearchAlgorithmTest.java b/robolectric_tests/src/com/android/launcher3/widget/picker/search/SimpleWidgetsSearchAlgorithmTest.java
new file mode 100644
index 0000000..c2bf1ae
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/widget/picker/search/SimpleWidgetsSearchAlgorithmTest.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2021 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.widget.picker.search;
+
+import static android.os.Looper.getMainLooper;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.matches;
+import static org.mockito.Mockito.verify;
+import static org.robolectric.Shadows.shadowOf;
+
+import com.android.launcher3.search.SearchCallback;
+import com.android.launcher3.widget.model.WidgetsListBaseEntry;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+
+@RunWith(RobolectricTestRunner.class)
+public class SimpleWidgetsSearchAlgorithmTest {
+
+    private SimpleWidgetsSearchAlgorithm mSimpleWidgetsSearchAlgorithm;
+    @Mock
+    private WidgetsPickerSearchPipeline mSearchPipeline;
+    @Mock
+    private SearchCallback<WidgetsListBaseEntry> mSearchCallback;
+    @Captor
+    private ArgumentCaptor<Consumer<List<WidgetsListBaseEntry>>> mConsumerCaptor;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mSimpleWidgetsSearchAlgorithm = new SimpleWidgetsSearchAlgorithm(mSearchPipeline);
+    }
+
+    @Test
+    public void doSearch_shouldQueryPipeline() {
+        mSimpleWidgetsSearchAlgorithm.doSearch("abc", mSearchCallback);
+
+        verify(mSearchPipeline).query(eq("abc"), any());
+    }
+
+    @Test
+    public void doSearch_shouldInformSearchCallbackOnQueryResult() {
+        ArrayList<WidgetsListBaseEntry> baseEntries = new ArrayList<>();
+
+        mSimpleWidgetsSearchAlgorithm.doSearch("abc", mSearchCallback);
+
+        verify(mSearchPipeline).query(eq("abc"), mConsumerCaptor.capture());
+        mConsumerCaptor.getValue().accept(baseEntries);
+        shadowOf(getMainLooper()).idle();
+        // Verify SearchCallback#onSearchResult receives a query token along with the search
+        // results. The query token is the original query string concatenated with the query
+        // timestamp.
+        verify(mSearchCallback).onSearchResult(matches("abc\t\\d*"), eq(baseEntries));
+    }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/widget/picker/search/SimpleWidgetsSearchPipelineTest.java b/robolectric_tests/src/com/android/launcher3/widget/picker/search/SimpleWidgetsSearchPipelineTest.java
new file mode 100644
index 0000000..17ededd
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/widget/picker/search/SimpleWidgetsSearchPipelineTest.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2021 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.widget.picker.search;
+
+import static android.os.Looper.getMainLooper;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.appwidget.AppWidgetProviderInfo;
+import android.content.ComponentName;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.os.UserHandle;
+
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.icons.BitmapInfo;
+import com.android.launcher3.icons.ComponentWithLabel;
+import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.model.WidgetItem;
+import com.android.launcher3.model.data.PackageItemInfo;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
+import com.android.launcher3.widget.model.WidgetsListContentEntry;
+import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
+import com.android.launcher3.widget.model.WidgetsListSearchHeaderEntry;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.shadows.ShadowPackageManager;
+import org.robolectric.util.ReflectionHelpers;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RunWith(RobolectricTestRunner.class)
+public class SimpleWidgetsSearchPipelineTest {
+    @Mock private IconCache mIconCache;
+
+    private InvariantDeviceProfile mTestProfile;
+    private WidgetsListHeaderEntry mCalendarHeaderEntry;
+    private WidgetsListContentEntry mCalendarContentEntry;
+    private WidgetsListHeaderEntry mCameraHeaderEntry;
+    private WidgetsListContentEntry mCameraContentEntry;
+    private WidgetsListHeaderEntry mClockHeaderEntry;
+    private WidgetsListContentEntry mClockContentEntry;
+    private Context mContext;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        doAnswer(invocation -> {
+            ComponentWithLabel componentWithLabel = (ComponentWithLabel) invocation.getArgument(0);
+            return componentWithLabel.getComponent().getShortClassName();
+        }).when(mIconCache).getTitleNoCache(any());
+        mTestProfile = new InvariantDeviceProfile();
+        mTestProfile.numRows = 5;
+        mTestProfile.numColumns = 5;
+        mContext = RuntimeEnvironment.application;
+
+        mCalendarHeaderEntry =
+                createWidgetsHeaderEntry("com.example.android.Calendar", "Calendar", 2);
+        mCalendarContentEntry =
+                createWidgetsContentEntry("com.example.android.Calendar", "Calendar", 2);
+        mCameraHeaderEntry = createWidgetsHeaderEntry("com.example.android.Camera", "Camera", 11);
+        mCameraContentEntry = createWidgetsContentEntry("com.example.android.Camera", "Camera", 11);
+        mClockHeaderEntry = createWidgetsHeaderEntry("com.example.android.Clock", "Clock", 3);
+        mClockContentEntry = createWidgetsContentEntry("com.example.android.Clock", "Clock", 3);
+    }
+
+    @Test
+    public void query_shouldMatchOnAppName() {
+        SimpleWidgetsSearchPipeline pipeline = new SimpleWidgetsSearchPipeline(
+                List.of(mCalendarHeaderEntry, mCalendarContentEntry, mCameraHeaderEntry,
+                        mCameraContentEntry, mClockHeaderEntry, mClockContentEntry));
+
+        pipeline.query("Ca", results ->
+                assertEquals(results,
+                        List.of(
+                                new WidgetsListSearchHeaderEntry(
+                                        mCalendarHeaderEntry.mPkgItem,
+                                        mCalendarHeaderEntry.mTitleSectionName,
+                                        mCalendarHeaderEntry.mWidgets),
+                                mCalendarContentEntry,
+                                new WidgetsListSearchHeaderEntry(
+                                        mCameraHeaderEntry.mPkgItem,
+                                        mCameraHeaderEntry.mTitleSectionName,
+                                        mCameraHeaderEntry.mWidgets),
+                                mCameraContentEntry)));
+        shadowOf(getMainLooper()).idle();
+    }
+
+    @Test
+    public void query_shouldMatchOnWidgetLabel() {
+        SimpleWidgetsSearchPipeline pipeline = new SimpleWidgetsSearchPipeline(
+                List.of(mCalendarHeaderEntry, mCalendarContentEntry, mCameraHeaderEntry,
+                        mCameraContentEntry));
+
+        pipeline.query("Widget1", results ->
+                assertEquals(results,
+                        List.of(
+                                new WidgetsListSearchHeaderEntry(
+                                        mCalendarHeaderEntry.mPkgItem,
+                                        mCalendarHeaderEntry.mTitleSectionName,
+                                        mCalendarHeaderEntry.mWidgets.subList(1, 2)),
+                                new WidgetsListContentEntry(
+                                        mCalendarHeaderEntry.mPkgItem,
+                                        mCalendarHeaderEntry.mTitleSectionName,
+                                        mCalendarHeaderEntry.mWidgets.subList(1, 2)),
+                                new WidgetsListSearchHeaderEntry(
+                                        mCameraHeaderEntry.mPkgItem,
+                                        mCameraHeaderEntry.mTitleSectionName,
+                                        mCameraHeaderEntry.mWidgets.subList(1, 3)),
+                                new WidgetsListContentEntry(
+                                        mCameraHeaderEntry.mPkgItem,
+                                        mCameraHeaderEntry.mTitleSectionName,
+                                        mCameraHeaderEntry.mWidgets.subList(1, 3)))));
+        shadowOf(getMainLooper()).idle();
+    }
+
+    private WidgetsListHeaderEntry createWidgetsHeaderEntry(String packageName, String appName,
+            int numOfWidgets) {
+        List<WidgetItem> widgetItems = generateWidgetItems(packageName, numOfWidgets);
+        PackageItemInfo pInfo = createPackageItemInfo(packageName, appName,
+                widgetItems.get(0).user);
+
+        return new WidgetsListHeaderEntry(pInfo, /* titleSectionName= */ "", widgetItems);
+    }
+
+    private WidgetsListContentEntry createWidgetsContentEntry(String packageName, String appName,
+            int numOfWidgets) {
+        List<WidgetItem> widgetItems = generateWidgetItems(packageName, numOfWidgets);
+        PackageItemInfo pInfo = createPackageItemInfo(packageName, appName,
+                widgetItems.get(0).user);
+
+        return new WidgetsListContentEntry(pInfo, /* titleSectionName= */ "", widgetItems);
+    }
+
+    private PackageItemInfo createPackageItemInfo(String packageName, String appName,
+            UserHandle userHandle) {
+        PackageItemInfo pInfo = new PackageItemInfo(packageName);
+        pInfo.title = appName;
+        pInfo.user = userHandle;
+        pInfo.bitmap = BitmapInfo.of(Bitmap.createBitmap(10, 10, Bitmap.Config.ALPHA_8), 0);
+        return pInfo;
+    }
+
+    private List<WidgetItem> generateWidgetItems(String packageName, int numOfWidgets) {
+        ShadowPackageManager packageManager = shadowOf(mContext.getPackageManager());
+        ArrayList<WidgetItem> widgetItems = new ArrayList<>();
+        for (int i = 0; i < numOfWidgets; i++) {
+            ComponentName cn = ComponentName.createRelative(packageName, ".SampleWidget" + i);
+            AppWidgetProviderInfo widgetInfo = new AppWidgetProviderInfo();
+            widgetInfo.provider = cn;
+            ReflectionHelpers.setField(widgetInfo, "providerInfo",
+                    packageManager.addReceiverIfNotPresent(cn));
+
+            WidgetItem widgetItem = new WidgetItem(
+                    LauncherAppWidgetProviderInfo.fromProviderInfo(mContext, widgetInfo),
+                    mTestProfile, mIconCache);
+            widgetItems.add(widgetItem);
+        }
+        return widgetItems;
+    }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/widget/picker/search/WidgetsSearchBarControllerTest.java b/robolectric_tests/src/com/android/launcher3/widget/picker/search/WidgetsSearchBarControllerTest.java
new file mode 100644
index 0000000..7fc9650
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/widget/picker/search/WidgetsSearchBarControllerTest.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2021 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.widget.picker.search;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+
+import android.content.Context;
+import android.view.View;
+import android.widget.EditText;
+import android.widget.ImageButton;
+
+import com.android.launcher3.search.SearchAlgorithm;
+import com.android.launcher3.widget.model.WidgetsListBaseEntry;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+import java.util.ArrayList;
+
+@RunWith(RobolectricTestRunner.class)
+public class WidgetsSearchBarControllerTest {
+
+    private WidgetsSearchBarController mController;
+    private Context mContext;
+    private EditText mEditText;
+    private ImageButton mCancelButton;
+    @Mock
+    private SearchModeListener mSearchModeListener;
+    @Mock
+    private SearchAlgorithm<WidgetsListBaseEntry> mSearchAlgorithm;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mContext = RuntimeEnvironment.application;
+        mEditText = new EditText(mContext);
+        mCancelButton = new ImageButton(mContext);
+        mController = new WidgetsSearchBarController(
+                mSearchAlgorithm, mEditText, mCancelButton, mSearchModeListener);
+    }
+
+    @Test
+    public void onSearchResult_shouldInformSearchModeListener() {
+        ArrayList<WidgetsListBaseEntry> entries = new ArrayList<>();
+        mController.onSearchResult("abc", entries);
+
+        verify(mSearchModeListener).onSearchResults(entries);
+    }
+
+    @Test
+    public void afterTextChanged_shouldInformSearchModeListenerToEnterSearch() {
+        mEditText.setText("abc");
+
+        verify(mSearchModeListener).enterSearchMode();
+        verifyNoMoreInteractions(mSearchModeListener);
+    }
+
+    @Test
+    public void afterTextChanged_shouldDoSearch() {
+        mEditText.setText("abc");
+
+        verify(mSearchAlgorithm).doSearch(eq("abc"), any());
+    }
+
+    @Test
+    public void afterTextChanged_shouldShowCancelButton() {
+        mEditText.setText("abc");
+
+        assertEquals(mCancelButton.getVisibility(), View.VISIBLE);
+    }
+
+    @Test
+    public void afterTextChanged_empty_shouldInformSearchModeListenerToExitSearch() {
+        mEditText.setText("");
+
+        verify(mSearchModeListener).exitSearchMode();
+        verifyNoMoreInteractions(mSearchModeListener);
+    }
+
+    @Test
+    public void afterTextChanged_empty_shouldCancelSearch() {
+        mEditText.setText("");
+
+        verify(mSearchAlgorithm).cancel(true);
+        verifyNoMoreInteractions(mSearchAlgorithm);
+    }
+
+    @Test
+    public void afterTextChanged_empty_shouldHideCancelButton() {
+        mEditText.setText("");
+
+        assertEquals(mCancelButton.getVisibility(), View.GONE);
+    }
+
+    @Test
+    public void cancelSearch_shouldInformSearchModeListenerToExitSearch() {
+        mCancelButton.performClick();
+
+        verify(mSearchModeListener).exitSearchMode();
+        verifyNoMoreInteractions(mSearchModeListener);
+    }
+
+    @Test
+    public void cancelSearch_shouldCancelSearch() {
+        mCancelButton.performClick();
+
+        verify(mSearchAlgorithm).cancel(true);
+        verifyNoMoreInteractions(mSearchAlgorithm);
+    }
+
+    @Test
+    public void cancelSearch_shouldClearSearchBar() {
+        mCancelButton.performClick();
+
+        assertEquals(mEditText.getText().toString(), "");
+    }
+}
diff --git a/src/com/android/launcher3/AbstractFloatingView.java b/src/com/android/launcher3/AbstractFloatingView.java
index 95cdbdd..e263c7a 100644
--- a/src/com/android/launcher3/AbstractFloatingView.java
+++ b/src/com/android/launcher3/AbstractFloatingView.java
@@ -98,6 +98,9 @@
     public static final int TYPE_HIDE_BACK_BUTTON = TYPE_ON_BOARD_POPUP | TYPE_DISCOVERY_BOUNCE
             | TYPE_SNACKBAR | TYPE_WIDGET_RESIZE_FRAME | TYPE_LISTENER;
 
+    // When these types of floating views are open, hide the taskbar hotseat and show the real one.
+    public static final int TYPE_REPLACE_TASKBAR_WITH_HOTSEAT = TYPE_FOLDER | TYPE_ACTION_POPUP;
+
     public static final int TYPE_ACCESSIBLE = TYPE_ALL & ~TYPE_DISCOVERY_BOUNCE & ~TYPE_LISTENER
             & ~TYPE_ALL_APPS_EDU;
 
diff --git a/src/com/android/launcher3/BaseActivity.java b/src/com/android/launcher3/BaseActivity.java
index 062ab71..f77f7e8 100644
--- a/src/com/android/launcher3/BaseActivity.java
+++ b/src/com/android/launcher3/BaseActivity.java
@@ -279,7 +279,7 @@
     /**
      * Used to set the override visibility state, used only to handle the transition home with the
      * recents animation.
-     * @see QuickstepAppTransitionManagerImpl#createWallpaperOpenRunner
+     * @see QuickstepTransitionManager#createWallpaperOpenRunner
      */
     public void addForceInvisibleFlag(@InvisibilityFlags int flag) {
         mForceInvisible |= flag;
diff --git a/src/com/android/launcher3/BaseDraggingActivity.java b/src/com/android/launcher3/BaseDraggingActivity.java
index 5bfde15..e38ab74 100644
--- a/src/com/android/launcher3/BaseDraggingActivity.java
+++ b/src/com/android/launcher3/BaseDraggingActivity.java
@@ -27,6 +27,7 @@
 import android.graphics.Insets;
 import android.graphics.Point;
 import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
 import android.os.Bundle;
 import android.os.Process;
 import android.os.StrictMode;
@@ -40,6 +41,7 @@
 import android.view.WindowMetrics;
 import android.widget.Toast;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.LauncherSettings.Favorites;
@@ -52,10 +54,12 @@
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.touch.ItemClickHandler;
 import com.android.launcher3.uioverrides.WallpaperColorInfo;
+import com.android.launcher3.util.ActivityOptionsWrapper;
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.DisplayController.DisplayInfoChangeListener;
 import com.android.launcher3.util.DisplayController.Info;
 import com.android.launcher3.util.PackageManagerHelper;
+import com.android.launcher3.util.RunnableList;
 import com.android.launcher3.util.Themes;
 import com.android.launcher3.util.TraceHelper;
 import com.android.launcher3.util.WindowBounds;
@@ -76,6 +80,7 @@
     protected boolean mIsSafeModeEnabled;
 
     private Runnable mOnStartCallback;
+    private RunnableList mOnResumeCallbacks = new RunnableList();
 
     private int mThemeRes = R.style.AppTheme;
 
@@ -98,6 +103,16 @@
     }
 
     @Override
+    protected void onResume() {
+        super.onResume();
+        mOnResumeCallbacks.executeAllAndClear();
+    }
+
+    public void addOnResumeCallback(Runnable callback) {
+        mOnResumeCallbacks.add(callback);
+    }
+
+    @Override
     public void onExtractedColorsChanged(WallpaperColorInfo wallpaperColorInfo) {
         updateTheme();
     }
@@ -149,20 +164,35 @@
         return new Rect(pos[0], pos[1], pos[0] + v.getWidth(), pos[1] + v.getHeight());
     }
 
-    public final Bundle getActivityLaunchOptionsAsBundle(View v) {
-        ActivityOptions activityOptions = getActivityLaunchOptions(v);
-        return activityOptions == null ? null : activityOptions.toBundle();
+    @NonNull
+    public ActivityOptionsWrapper getActivityLaunchOptions(View v) {
+        int left = 0, top = 0;
+        int width = v.getMeasuredWidth(), height = v.getMeasuredHeight();
+        if (v instanceof BubbleTextView) {
+            // Launch from center of icon, not entire view
+            Drawable icon = ((BubbleTextView) v).getIcon();
+            if (icon != null) {
+                Rect bounds = icon.getBounds();
+                left = (width - bounds.width()) / 2;
+                top = v.getPaddingTop();
+                width = bounds.width();
+                height = bounds.height();
+            }
+        }
+        ActivityOptions options =
+                ActivityOptions.makeClipRevealAnimation(v, left, top, width, height);
+        RunnableList callback = new RunnableList();
+        addOnResumeCallback(callback::executeAllAndDestroy);
+        return new ActivityOptionsWrapper(options, callback);
     }
 
-    public abstract ActivityOptions getActivityLaunchOptions(View v);
-
     public boolean startActivitySafely(View v, Intent intent, @Nullable ItemInfo item) {
         if (mIsSafeModeEnabled && !PackageManagerHelper.isSystemApp(this, intent)) {
             Toast.makeText(this, R.string.safemode_shortcut_error, Toast.LENGTH_SHORT).show();
             return false;
         }
 
-        Bundle optsBundle = (v != null) ? getActivityLaunchOptionsAsBundle(v) : null;
+        Bundle optsBundle = (v != null) ? getActivityLaunchOptions(v).toBundle() : null;
         UserHandle user = item == null ? null : item.user;
 
         // Prepare intent
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index e0be6de..2b58fb6 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -50,7 +50,6 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.UiThread;
 
-import com.android.launcher3.Launcher.OnResumeCallback;
 import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.dot.DotInfo;
@@ -81,7 +80,7 @@
  * because we want to make the bubble taller than the text and TextView's clip is
  * too aggressive.
  */
-public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, OnResumeCallback,
+public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver,
         IconLabelDotView, DraggableView, Reorderable {
 
     private static final int DISPLAY_WORKSPACE = 0;
@@ -431,13 +430,6 @@
         }
     }
 
-    @Override
-    public void onLauncherResume() {
-        // Reset the pressed state of icon that was locked in the press state while activity
-        // was launching
-        setStayPressed(false);
-    }
-
     void clearPressedBackground() {
         setPressed(false);
         setStayPressed(false);
diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index f2dd60e..fb7a99f 100644
--- a/src/com/android/launcher3/DeviceProfile.java
+++ b/src/com/android/launcher3/DeviceProfile.java
@@ -472,7 +472,7 @@
         final boolean isVerticalLayout = isVerticalBarLayout();
         float invIconSizeDp = isVerticalLayout ? inv.landscapeIconSize : inv.iconSize;
         iconSizePx = Math.max(1, pxFromDp(invIconSizeDp, mInfo.metrics, scale));
-        iconTextSizePx = pxFromDp(inv.iconTextSize, mInfo.metrics, scale);
+        iconTextSizePx = (int) (Utilities.pxFromSp(inv.iconTextSize, mInfo.metrics) * scale);
         iconDrawablePaddingPx = (int) (iconDrawablePaddingOriginalPx * scale);
 
         setCellLayoutBorderSpacing((int) (cellLayoutBorderSpacingOriginalPx * scale));
@@ -708,10 +708,11 @@
                     mInsets.top + availableHeightPx);
         } else {
             // Folders should only appear below the drop target bar and above the hotseat
+            int hotseatTop = isTaskbarPresent ? taskbarSize : hotseatBarSizePx;
             return new Rect(mInsets.left + edgeMarginPx,
                     mInsets.top + dropTargetBarSizePx + edgeMarginPx,
                     mInsets.left + availableWidthPx - edgeMarginPx,
-                    mInsets.top + availableHeightPx - hotseatBarSizePx
+                    mInsets.top + availableHeightPx - hotseatTop
                             - workspacePageIndicatorHeight - edgeMarginPx);
         }
     }
diff --git a/src/com/android/launcher3/Hotseat.java b/src/com/android/launcher3/Hotseat.java
index b2112ad..e5b75c1 100644
--- a/src/com/android/launcher3/Hotseat.java
+++ b/src/com/android/launcher3/Hotseat.java
@@ -29,6 +29,7 @@
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.util.MultiValueAlpha;
 
 import java.util.function.Consumer;
 
@@ -37,6 +38,10 @@
  */
 public class Hotseat extends CellLayout implements Insettable {
 
+    private static final int ALPHA_INDEX_STATE = 0;
+    private static final int ALPHA_INDEX_REPLACE_TASKBAR = 1;
+    private static final int NUM_ALPHA_CHANNELS = 2;
+
     @ViewDebug.ExportedProperty(category = "launcher")
     private boolean mHasVerticalHotseat;
     private Workspace mWorkspace;
@@ -44,6 +49,8 @@
     @Nullable
     private Consumer<Boolean> mOnVisibilityAggregatedCallback;
 
+    private final MultiValueAlpha mMultiValueAlpha;
+
     public Hotseat(Context context) {
         this(context, null);
     }
@@ -54,6 +61,8 @@
 
     public Hotseat(Context context, AttributeSet attrs, int defStyle) {
         super(context, attrs, defStyle);
+        mMultiValueAlpha = new MultiValueAlpha(this, NUM_ALPHA_CHANNELS, MultiValueAlpha.Mode.MAX);
+        mMultiValueAlpha.setUpdateVisibility(true);
     }
 
     /**
@@ -174,4 +183,12 @@
     public View getFirstItemMatch(Workspace.ItemOperator itemOperator) {
         return mWorkspace.getFirstMatch(new CellLayout[] { this }, itemOperator);
     }
+
+    public MultiValueAlpha.AlphaProperty getStateAlpha() {
+        return mMultiValueAlpha.getProperty(ALPHA_INDEX_STATE);
+    }
+
+    public MultiValueAlpha.AlphaProperty getReplaceTaskbarAlpha() {
+        return mMultiValueAlpha.getProperty(ALPHA_INDEX_REPLACE_TASKBAR);
+    }
 }
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 80c80d7..1546ee3 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -61,7 +61,6 @@
 import android.animation.ObjectAnimator;
 import android.animation.ValueAnimator;
 import android.annotation.TargetApi;
-import android.app.ActivityOptions;
 import android.appwidget.AppWidgetHostView;
 import android.appwidget.AppWidgetManager;
 import android.content.ActivityNotFoundException;
@@ -276,7 +275,6 @@
 
     private static final int THEME_CROSS_FADE_ANIMATION_DURATION = 375;
 
-    private LauncherAppTransitionManager mAppTransitionManager;
     private Configuration mOldConfig;
 
     private LiveSearchManager mLiveSearchManager;
@@ -312,8 +310,6 @@
     @Thunk
     boolean mWorkspaceLoading = true;
 
-    private ArrayList<OnResumeCallback> mOnResumeCallbacks = new ArrayList<>();
-
     // Used to notify when an activity launch has been deferred because launcher is not yet resumed
     // TODO: See if we can remove this later
     private Runnable mOnDeferredActivityLaunchCallback;
@@ -419,9 +415,6 @@
         crossFadeWithPreviousAppearance();
         mPopupDataProvider = new PopupDataProvider(this::updateNotificationDots);
 
-        mAppTransitionManager = LauncherAppTransitionManager.newInstance(this);
-        mAppTransitionManager.registerRemoteAnimations();
-
         boolean internalStateHandled = ACTIVITY_TRACKER.handleCreate(this);
         if (internalStateHandled) {
             if (savedInstanceState != null) {
@@ -1090,15 +1083,6 @@
                 TraceHelper.FLAG_UI_EVENT);
         super.onResume();
 
-        if (!mOnResumeCallbacks.isEmpty()) {
-            final ArrayList<OnResumeCallback> resumeCallbacks = new ArrayList<>(mOnResumeCallbacks);
-            mOnResumeCallbacks.clear();
-            for (int i = resumeCallbacks.size() - 1; i >= 0; i--) {
-                resumeCallbacks.get(i).onLauncherResume();
-            }
-            resumeCallbacks.clear();
-        }
-
         if (mDeferOverlayCallbacks) {
             scheduleDeferredCheck();
         } else {
@@ -1609,8 +1593,6 @@
         LauncherAppState.getIDP(this).removeOnChangeListener(this);
 
         mOverlayManager.onActivityDestroyed(this);
-        mAppTransitionManager.unregisterRemoteAnimations();
-        mAppTransitionManager.unregisterRemoteTransitions();
         mUserChangedCallbackCloseable.close();
         mLiveSearchManager.stop();
     }
@@ -1936,16 +1918,6 @@
 
     @TargetApi(Build.VERSION_CODES.M)
     @Override
-    public ActivityOptions getActivityLaunchOptions(View v) {
-        return mAppTransitionManager.getActivityLaunchOptions(this, v);
-    }
-
-    public LauncherAppTransitionManager getAppTransitionManager() {
-        return mAppTransitionManager;
-    }
-
-    @TargetApi(Build.VERSION_CODES.M)
-    @Override
     protected boolean onErrorStartingShortcut(Intent intent, ItemInfo info) {
         // Due to legacy reasons, direct call shortcuts require Launchers to have the
         // corresponding permission. Show the appropriate permission prompt if that
@@ -1994,7 +1966,7 @@
             // state when we return to launcher.
             BubbleTextView btv = (BubbleTextView) v;
             btv.setStayPressed(true);
-            addOnResumeCallback(btv);
+            addOnResumeCallback(() -> btv.setStayPressed(false));
         }
         return success;
     }
@@ -2038,10 +2010,6 @@
         return result;
     }
 
-    public void addOnResumeCallback(OnResumeCallback callback) {
-        mOnResumeCallbacks.add(callback);
-    }
-
     /**
      * Persistant callback which notifies when an activity launch is deferred because the activity
      * was not yet resumed.
@@ -2815,15 +2783,6 @@
         return (T) activityContext;
     }
 
-
-    /**
-     * Callback for listening for onResume
-     */
-    public interface OnResumeCallback {
-
-        void onLauncherResume();
-    }
-
     /**
      * Cross-fades the launcher's updated appearance with its previous appearance.
      *
@@ -2866,6 +2825,10 @@
         return false;
     }
 
+    public boolean supportsAdaptiveIconAnimation(View clickedView) {
+        return false;
+    }
+
     public DragOptions getDefaultWorkspaceDragOptions() {
         return new DragOptions();
     }
diff --git a/src/com/android/launcher3/LauncherAppTransitionManager.java b/src/com/android/launcher3/LauncherAppTransitionManager.java
deleted file mode 100644
index 0fa441a..0000000
--- a/src/com/android/launcher3/LauncherAppTransitionManager.java
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
- * Copyright (C) 2018 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 android.app.ActivityOptions;
-import android.content.Context;
-import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
-import android.view.View;
-
-import com.android.launcher3.util.ResourceBasedOverride;
-
-/**
- * Manages the opening and closing app transitions from Launcher.
- */
-public class LauncherAppTransitionManager implements ResourceBasedOverride {
-
-    public static LauncherAppTransitionManager newInstance(Context context) {
-        return Overrides.getObject(LauncherAppTransitionManager.class,
-                context, R.string.app_transition_manager_class);
-    }
-
-    public ActivityOptions getActivityLaunchOptions(Launcher launcher, View v) {
-        int left = 0, top = 0;
-        int width = v.getMeasuredWidth(), height = v.getMeasuredHeight();
-        if (v instanceof BubbleTextView) {
-            // Launch from center of icon, not entire view
-            Drawable icon = ((BubbleTextView) v).getIcon();
-            if (icon != null) {
-                Rect bounds = icon.getBounds();
-                left = (width - bounds.width()) / 2;
-                top = v.getPaddingTop();
-                width = bounds.width();
-                height = bounds.height();
-            }
-        }
-        return ActivityOptions.makeClipRevealAnimation(v, left, top, width, height);
-    }
-
-    public boolean supportsAdaptiveIconAnimation(View clickedView) {
-        return false;
-    }
-
-    /**
-     * Registers remote animations for certain system transitions.
-     */
-    public void registerRemoteAnimations() {
-        // Do nothing
-    }
-
-    /**
-     * Unregisters all remote animations.
-     */
-    public void unregisterRemoteAnimations() {
-        // Do nothing
-    }
-
-    /**
-     * Registers remote transitions for certain system transitions.
-     */
-    public void registerRemoteTransitions() {
-        // Do nothing
-    }
-
-    /**
-     * Unregisters all remote transitions.
-     */
-    public void unregisterRemoteTransitions() {
-        // Do nothing
-    }
-}
diff --git a/src/com/android/launcher3/SecondaryDropTarget.java b/src/com/android/launcher3/SecondaryDropTarget.java
index 4fd87cb..8bc5ad0 100644
--- a/src/com/android/launcher3/SecondaryDropTarget.java
+++ b/src/com/android/launcher3/SecondaryDropTarget.java
@@ -33,7 +33,6 @@
 import android.view.View;
 import android.widget.Toast;
 
-import com.android.launcher3.Launcher.OnResumeCallback;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.dragndrop.DragOptions;
 import com.android.launcher3.logging.FileLog;
@@ -228,7 +227,7 @@
             DeferredOnComplete deferred = (DeferredOnComplete) d.dragSource;
             if (target != null) {
                 deferred.mPackageName = target.getPackageName();
-                mLauncher.addOnResumeCallback(deferred);
+                mLauncher.addOnResumeCallback(deferred::onLauncherResume);
             } else {
                 deferred.sendFailure();
             }
@@ -311,7 +310,7 @@
      * A wrapper around {@link DragSource} which delays the {@link #onDropCompleted} action until
      * {@link #onLauncherResume}
      */
-    private class DeferredOnComplete implements DragSource, OnResumeCallback {
+    private class DeferredOnComplete implements DragSource {
 
         private final DragSource mOriginal;
         private final Context mContext;
@@ -330,7 +329,6 @@
             mDragObject = d;
         }
 
-        @Override
         public void onLauncherResume() {
             // We use MATCH_UNINSTALLED_PACKAGES as the app can be on SD card as well.
             if (new PackageManagerHelper(mContext).getApplicationInfo(mPackageName,
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index 87fb6fb..0cc965c 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -186,7 +186,6 @@
     @Thunk final Launcher mLauncher;
     @Thunk DragController mDragController;
 
-    private final Rect mTempRect = new Rect();
     private final int[] mTempXY = new int[2];
     private final float[] mTempFXY = new float[2];
     @Thunk float[] mDragViewVisualCenter = new float[2];
@@ -367,10 +366,19 @@
     }
 
     public float getWallpaperOffsetForCenterPage() {
-        int pageScroll = getScrollForPage(getPageNearestToCenterOfScreen());
+        return getWallpaperOffsetForPage(getPageNearestToCenterOfScreen());
+    }
+
+    private float getWallpaperOffsetForPage(int page) {
+        int pageScroll = getScrollForPage(page);
         return mWallpaperOffset.wallpaperOffsetForScroll(pageScroll);
     }
 
+    /** Returns the number of pages used for the wallpaper parallax. */
+    public int getNumPagesForWallpaperParallax() {
+        return mWallpaperOffset.getNumPagesForWallpaperParallax();
+    }
+
     public Rect estimateItemPosition(CellLayout cl, int hCell, int vCell, int hSpan, int vSpan) {
         Rect r = new Rect();
         cl.cellToRect(hCell, vCell, hSpan, vSpan, r);
diff --git a/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java b/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java
index 660eeab..d6d2f73 100644
--- a/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java
+++ b/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java
@@ -55,6 +55,7 @@
 import com.android.launcher3.graphics.WorkspaceDragScrim;
 import com.android.launcher3.states.StateAnimationConfig;
 import com.android.launcher3.util.DynamicResource;
+import com.android.launcher3.util.MultiValueAlpha;
 import com.android.systemui.plugins.ResourceProvider;
 
 /**
@@ -143,8 +144,8 @@
             }
 
             float hotseatIconsAlpha = (elements & HOTSEAT_ICONS) != 0 ? 1 : 0;
-            propertySetter.setViewAlpha(hotseat, hotseatIconsAlpha,
-                    config.getInterpolator(ANIM_HOTSEAT_FADE, fadeInterpolator));
+            propertySetter.setFloat(hotseat.getStateAlpha(), MultiValueAlpha.VALUE,
+                    hotseatIconsAlpha, config.getInterpolator(ANIM_HOTSEAT_FADE, fadeInterpolator));
             float workspacePageIndicatorAlpha = (elements & WORKSPACE_PAGE_INDICATOR) != 0 ? 1 : 0;
             propertySetter.setViewAlpha(mLauncher.getWorkspace().getPageIndicator(),
                     workspacePageIndicatorAlpha, fadeInterpolator);
diff --git a/src/com/android/launcher3/allapps/AllAppsContainerView.java b/src/com/android/launcher3/allapps/AllAppsContainerView.java
index edd9a9f..9ede94c 100644
--- a/src/com/android/launcher3/allapps/AllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsContainerView.java
@@ -201,9 +201,9 @@
         }
         if (!mAH[AdapterHolder.MAIN].appsList.hasFilter()) {
             rebindAdapters(hasWorkApps);
-        }
-        if (hasWorkApps) {
-            resetWorkProfile();
+            if (hasWorkApps) {
+                resetWorkProfile();
+            }
         }
     }
 
diff --git a/src/com/android/launcher3/allapps/search/AppsSearchPipeline.java b/src/com/android/launcher3/allapps/search/AppsSearchPipeline.java
index f9fb22e..34895ed 100644
--- a/src/com/android/launcher3/allapps/search/AppsSearchPipeline.java
+++ b/src/com/android/launcher3/allapps/search/AppsSearchPipeline.java
@@ -25,6 +25,7 @@
 import com.android.launcher3.model.BaseModelUpdateTask;
 import com.android.launcher3.model.BgDataModel;
 import com.android.launcher3.model.data.AppInfo;
+import com.android.launcher3.search.StringMatcherUtility;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -67,10 +68,10 @@
         // apps that don't match all of the words in the query.
         final String queryTextLower = query.toLowerCase();
         final ArrayList<AppInfo> result = new ArrayList<>();
-        DefaultAppSearchAlgorithm.StringMatcher matcher =
-                DefaultAppSearchAlgorithm.StringMatcher.getInstance();
+        StringMatcherUtility.StringMatcher matcher =
+                StringMatcherUtility.StringMatcher.getInstance();
         for (AppInfo info : apps) {
-            if (DefaultAppSearchAlgorithm.matches(info, queryTextLower, matcher)) {
+            if (StringMatcherUtility.matches(queryTextLower, info.title.toString(), matcher)) {
                 result.add(info);
             }
         }
diff --git a/src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithm.java b/src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithm.java
index 4e213b0..a386ef8 100644
--- a/src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithm.java
+++ b/src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithm.java
@@ -20,12 +20,9 @@
 
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.allapps.AllAppsGridAdapter.AdapterItem;
-import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.search.SearchAlgorithm;
 import com.android.launcher3.search.SearchCallback;
 
-import java.text.Collator;
-
 /**
  * The default search implementation.
  */
@@ -54,132 +51,4 @@
                         () -> callback.onSearchResult(query, results)),
                 null);
     }
-
-    public static boolean matches(AppInfo info, String query, StringMatcher matcher) {
-        int queryLength = query.length();
-
-        String title = info.title.toString();
-        int titleLength = title.length();
-
-        if (titleLength < queryLength || queryLength <= 0) {
-            return false;
-        }
-
-        if (requestSimpleFuzzySearch(query)) {
-            return title.toLowerCase().contains(query);
-        }
-
-        int lastType;
-        int thisType = Character.UNASSIGNED;
-        int nextType = Character.getType(title.codePointAt(0));
-
-        int end = titleLength - queryLength;
-        for (int i = 0; i <= end; i++) {
-            lastType = thisType;
-            thisType = nextType;
-            nextType = i < (titleLength - 1) ?
-                    Character.getType(title.codePointAt(i + 1)) : Character.UNASSIGNED;
-            if (isBreak(thisType, lastType, nextType) &&
-                    matcher.matches(query, title.substring(i, i + queryLength))) {
-                return true;
-            }
-        }
-        return false;
-    }
-
-    /**
-     * Returns true if the current point should be a break point. Following cases
-     * are considered as break points:
-     *      1) Any non space character after a space character
-     *      2) Any digit after a non-digit character
-     *      3) Any capital character after a digit or small character
-     *      4) Any capital character before a small character
-     */
-    private static boolean isBreak(int thisType, int prevType, int nextType) {
-        switch (prevType) {
-            case Character.UNASSIGNED:
-            case Character.SPACE_SEPARATOR:
-            case Character.LINE_SEPARATOR:
-            case Character.PARAGRAPH_SEPARATOR:
-                return true;
-        }
-        switch (thisType) {
-            case Character.UPPERCASE_LETTER:
-                if (nextType == Character.UPPERCASE_LETTER) {
-                    return true;
-                }
-                // Follow through
-            case Character.TITLECASE_LETTER:
-                // Break point if previous was not a upper case
-                return prevType != Character.UPPERCASE_LETTER;
-            case Character.LOWERCASE_LETTER:
-                // Break point if previous was not a letter.
-                return prevType > Character.OTHER_LETTER || prevType <= Character.UNASSIGNED;
-            case Character.DECIMAL_DIGIT_NUMBER:
-            case Character.LETTER_NUMBER:
-            case Character.OTHER_NUMBER:
-                // Break point if previous was not a number
-                return !(prevType == Character.DECIMAL_DIGIT_NUMBER
-                        || prevType == Character.LETTER_NUMBER
-                        || prevType == Character.OTHER_NUMBER);
-            case Character.MATH_SYMBOL:
-            case Character.CURRENCY_SYMBOL:
-            case Character.OTHER_PUNCTUATION:
-            case Character.DASH_PUNCTUATION:
-                // Always a break point for a symbol
-                return true;
-            default:
-                return  false;
-        }
-    }
-
-    public static class StringMatcher {
-
-        private static final char MAX_UNICODE = '\uFFFF';
-
-        private final Collator mCollator;
-
-        StringMatcher() {
-            // On android N and above, Collator uses ICU implementation which has a much better
-            // support for non-latin locales.
-            mCollator = Collator.getInstance();
-            mCollator.setStrength(Collator.PRIMARY);
-            mCollator.setDecomposition(Collator.CANONICAL_DECOMPOSITION);
-        }
-
-        /**
-         * Returns true if {@param query} is a prefix of {@param target}
-         */
-        public boolean matches(String query, String target) {
-            switch (mCollator.compare(query, target)) {
-                case 0:
-                    return true;
-                case -1:
-                    // The target string can contain a modifier which would make it larger than
-                    // the query string (even though the length is same). If the query becomes
-                    // larger after appending a unicode character, it was originally a prefix of
-                    // the target string and hence should match.
-                    return mCollator.compare(query + MAX_UNICODE, target) > -1;
-                default:
-                    return false;
-            }
-        }
-
-        public static StringMatcher getInstance() {
-            return new StringMatcher();
-        }
-    }
-
-    private static boolean requestSimpleFuzzySearch(String s) {
-        for (int i = 0; i < s.length(); ) {
-            int codepoint = s.codePointAt(i);
-            i += Character.charCount(codepoint);
-            switch (Character.UnicodeScript.of(codepoint)) {
-                case HAN:
-                    //Character.UnicodeScript.HAN: use String.contains to match
-                    return true;
-            }
-        }
-        return false;
-    }
 }
diff --git a/src/com/android/launcher3/allapps/search/DefaultSearchAdapterProvider.java b/src/com/android/launcher3/allapps/search/DefaultSearchAdapterProvider.java
index 330ec68..e268f56 100644
--- a/src/com/android/launcher3/allapps/search/DefaultSearchAdapterProvider.java
+++ b/src/com/android/launcher3/allapps/search/DefaultSearchAdapterProvider.java
@@ -15,6 +15,7 @@
  */
 package com.android.launcher3.allapps.search;
 
+import android.net.Uri;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
@@ -37,6 +38,11 @@
     }
 
     @Override
+    public void onSliceStatusUpdate(Uri sliceUri) {
+
+    }
+
+    @Override
     public boolean isSearchView(int viewType) {
         return false;
     }
diff --git a/src/com/android/launcher3/allapps/search/LiveSearchManager.java b/src/com/android/launcher3/allapps/search/LiveSearchManager.java
index 76099a6..adb882a 100644
--- a/src/com/android/launcher3/allapps/search/LiveSearchManager.java
+++ b/src/com/android/launcher3/allapps/search/LiveSearchManager.java
@@ -42,6 +42,7 @@
 import androidx.slice.SliceViewManager;
 import androidx.slice.SliceViewManager.SliceCallback;
 
+import com.android.launcher3.Alarm;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.statemanager.StateManager.StateListener;
@@ -52,6 +53,7 @@
 
 import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.function.Consumer;
 
 /**
  * Manages Lifecycle for Live search results
@@ -60,6 +62,7 @@
 
     private static final String TAG = "LiveSearchManager";
 
+    private static final long SLICE_TIMEOUT_MS = 50;
     public static final int SEARCH_APPWIDGET_HOST_ID = 2048;
 
     private final Launcher mLauncher;
@@ -144,16 +147,50 @@
     /**
      * Adds a new observer for the provided uri and returns a callback to cancel this observer
      */
-    public SafeCloseable addObserver(Uri uri, Observer<Slice> listener) {
+    public SafeCloseable addObserver(Uri uri, Observer<Slice> listener,
+            Consumer<Uri> timeoutConsumer) {
         SliceLifeCycle slc = mUriSliceMap.get(uri);
         if (slc == null) {
             slc = new SliceLifeCycle(uri, mLauncher);
             mUriSliceMap.put(uri, slc);
         }
-        slc.addListener(listener);
+        if (slc.mLastValue != null) {
+            listener.onChanged(slc.mLastValue);
+        }
+
+        // Use a listener wrapper to handle error timeout.
+        Observer<Slice> listenerWrapper = new Observer<Slice>() {
+            final Alarm mErrorTimeout = new Alarm();
+            {
+                mErrorTimeout.setOnAlarmListener(alarm -> {
+                    alarm.cancelAlarm();
+                    timeoutConsumer.accept(uri);
+                });
+                mErrorTimeout.setAlarm(SLICE_TIMEOUT_MS);
+            }
+
+            @Override
+            public void onChanged(Slice slice) {
+                if (slice == null) {
+                    return;
+                }
+
+                if (mErrorTimeout.alarmPending()) {
+                    mErrorTimeout.cancelAlarm();
+                }
+
+                if (mUriSliceMap.get(uri) != null) {
+                    mUriSliceMap.get(uri).mLastValue = slice;
+                }
+
+                listener.onChanged(slice);
+            }
+        };
+
+        slc.addListener(listenerWrapper);
 
         final SliceLifeCycle sliceLifeCycle = slc;
-        return () -> sliceLifeCycle.removeListener(listener);
+        return () -> sliceLifeCycle.removeListener(listenerWrapper);
     }
 
     static class SearchWidgetHost extends AppWidgetHost {
@@ -184,6 +221,8 @@
         private boolean mDestroyed = false;
         private boolean mWasListening = false;
 
+        Slice mLastValue;
+
         SliceLifeCycle(Uri uri, Launcher launcher) {
             mUri = uri;
             mLauncher = launcher;
diff --git a/src/com/android/launcher3/allapps/search/SearchAdapterProvider.java b/src/com/android/launcher3/allapps/search/SearchAdapterProvider.java
index fdacd3d..0864090 100644
--- a/src/com/android/launcher3/allapps/search/SearchAdapterProvider.java
+++ b/src/com/android/launcher3/allapps/search/SearchAdapterProvider.java
@@ -16,6 +16,7 @@
 
 package com.android.launcher3.allapps.search;
 
+import android.net.Uri;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
@@ -40,6 +41,11 @@
     public abstract void onBindView(AllAppsGridAdapter.ViewHolder holder, int position);
 
     /**
+     * Called from LiveSearchManager to notify slice status updates.
+     */
+    public abstract void onSliceStatusUpdate(Uri sliceUri);
+
+    /**
      * Returns whether or not viewType can be handled by searchProvider
      */
     public abstract boolean isSearchView(int viewType);
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index e406e9b..3657e89 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -198,10 +198,10 @@
             "ENABLE_APP_PREDICTIONS_WHILE_VISIBLE", true, "Allows app "
             + "predictions to be updated while they are visible to the user.");
 
-    public static final BooleanFlag ENABLE_TASKBAR = new DeviceFlag(
+    public static final BooleanFlag ENABLE_TASKBAR = getDebugFlag(
             "ENABLE_TASKBAR", false, "Allows a system Taskbar to be shown on larger devices.");
 
-    public static final BooleanFlag ENABLE_OVERVIEW_GRID = new DeviceFlag(
+    public static final BooleanFlag ENABLE_OVERVIEW_GRID = getDebugFlag(
             "ENABLE_OVERVIEW_GRID", false, "Uses grid overview layout. "
             + "Only applicable on large screen devices.");
 
diff --git a/src/com/android/launcher3/folder/FolderAnimationManager.java b/src/com/android/launcher3/folder/FolderAnimationManager.java
index feb528c..ee6ea99 100644
--- a/src/com/android/launcher3/folder/FolderAnimationManager.java
+++ b/src/com/android/launcher3/folder/FolderAnimationManager.java
@@ -40,7 +40,6 @@
 import com.android.launcher3.BubbleTextView;
 import com.android.launcher3.CellLayout;
 import com.android.launcher3.R;
-import com.android.launcher3.ResourceUtils;
 import com.android.launcher3.ShortcutAndWidgetContainer;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.PropertyResetListener;
@@ -177,7 +176,7 @@
                 Math.round((totalOffsetX + initialSize)),
                 Math.round((paddingOffsetY + initialSize)));
         Rect endRect = new Rect(0, 0, lp.width, lp.height);
-        float finalRadius = ResourceUtils.pxFromDp(2, mContext.getResources().getDisplayMetrics());
+        float finalRadius = mFolderBackground.getCornerRadius();
 
         // Create the animators.
         AnimatorSet a = new AnimatorSet();
diff --git a/src/com/android/launcher3/popup/ArrowPopup.java b/src/com/android/launcher3/popup/ArrowPopup.java
index 5a34d2a..15915e5 100644
--- a/src/com/android/launcher3/popup/ArrowPopup.java
+++ b/src/com/android/launcher3/popup/ArrowPopup.java
@@ -164,7 +164,9 @@
             reverseOrder(viewsToFlip);
         }
         onInflationComplete(reverseOrder);
-        addArrow();
+        if (shouldAddArrow()) {
+            addArrow();
+        }
         animateOpen();
     }
 
@@ -174,7 +176,9 @@
     protected void show() {
         setupForDisplay();
         onInflationComplete(false);
-        addArrow();
+        if (shouldAddArrow()) {
+            addArrow();
+        }
         animateOpen();
     }
 
@@ -233,6 +237,13 @@
     }
 
     /**
+     * Returns whether or not we should add the arrow.
+     */
+    protected boolean shouldAddArrow() {
+        return true;
+    }
+
+    /**
      * Provide the location of the target object relative to the dragLayer.
      */
     protected abstract void getTargetObjectLocation(Rect outPos);
@@ -392,13 +403,18 @@
         return getChildCount() > 0 ? getChildAt(0) : this;
     }
 
+    private int getArrowDuration() {
+        return shouldAddArrow()
+                ? getResources().getInteger(R.integer.config_popupArrowOpenCloseDuration)
+                : 0;
+    }
     private void animateOpen() {
         setVisibility(View.VISIBLE);
 
         final AnimatorSet openAnim = new AnimatorSet();
         final Resources res = getResources();
         final long revealDuration = (long) res.getInteger(R.integer.config_popupOpenCloseDuration);
-        final long arrowDuration = res.getInteger(R.integer.config_popupArrowOpenCloseDuration);
+        final long arrowDuration = getArrowDuration();
         final TimeInterpolator revealInterpolator = ACCEL_DEACCEL;
 
         // Rectangular reveal.
@@ -460,7 +476,7 @@
         final Resources res = getResources();
         final TimeInterpolator revealInterpolator = ACCEL_DEACCEL;
         final long revealDuration = res.getInteger(R.integer.config_popupOpenCloseDuration);
-        final long arrowDuration = res.getInteger(R.integer.config_popupArrowOpenCloseDuration);
+        final long arrowDuration = getArrowDuration();
 
         // Hide the arrow
         Animator scaleArrow = ObjectAnimator.ofFloat(mArrow, LauncherAnimUtils.SCALE_PROPERTY, 0)
diff --git a/src/com/android/launcher3/search/SearchAlgorithm.java b/src/com/android/launcher3/search/SearchAlgorithm.java
index 1665354..a1720c7 100644
--- a/src/com/android/launcher3/search/SearchAlgorithm.java
+++ b/src/com/android/launcher3/search/SearchAlgorithm.java
@@ -31,4 +31,9 @@
      * Cancels any active request.
      */
     void cancel(boolean interruptActiveRequests);
+
+    /**
+     * Cleans up after search is no longer needed.
+     */
+    default void destroy() {};
 }
diff --git a/src/com/android/launcher3/search/StringMatcherUtility.java b/src/com/android/launcher3/search/StringMatcherUtility.java
new file mode 100644
index 0000000..acab52b
--- /dev/null
+++ b/src/com/android/launcher3/search/StringMatcherUtility.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2021 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.search;
+
+import java.text.Collator;
+
+/**
+ * Utilities for matching query string to target string.
+ */
+public class StringMatcherUtility {
+
+    /**
+     * Returns {@code true} is {@code query} is a prefix substring of a complete word/phrase in
+     * {@code target}.
+     */
+    public static boolean matches(String query, String target, StringMatcher matcher) {
+        int queryLength = query.length();
+
+        int targetLength = target.length();
+
+        if (targetLength < queryLength || queryLength <= 0) {
+            return false;
+        }
+
+        if (requestSimpleFuzzySearch(query)) {
+            return target.toLowerCase().contains(query);
+        }
+
+        int lastType;
+        int thisType = Character.UNASSIGNED;
+        int nextType = Character.getType(target.codePointAt(0));
+
+        int end = targetLength - queryLength;
+        for (int i = 0; i <= end; i++) {
+            lastType = thisType;
+            thisType = nextType;
+            nextType = i < (targetLength - 1)
+                    ? Character.getType(target.codePointAt(i + 1)) : Character.UNASSIGNED;
+            if (isBreak(thisType, lastType, nextType)
+                    && matcher.matches(query, target.substring(i, i + queryLength))) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Returns true if the current point should be a break point. Following cases
+     * are considered as break points:
+     *      1) Any non space character after a space character
+     *      2) Any digit after a non-digit character
+     *      3) Any capital character after a digit or small character
+     *      4) Any capital character before a small character
+     */
+    private static boolean isBreak(int thisType, int prevType, int nextType) {
+        switch (prevType) {
+            case Character.UNASSIGNED:
+            case Character.SPACE_SEPARATOR:
+            case Character.LINE_SEPARATOR:
+            case Character.PARAGRAPH_SEPARATOR:
+                return true;
+        }
+        switch (thisType) {
+            case Character.UPPERCASE_LETTER:
+                if (nextType == Character.UPPERCASE_LETTER) {
+                    return true;
+                }
+                // Follow through
+            case Character.TITLECASE_LETTER:
+                // Break point if previous was not a upper case
+                return prevType != Character.UPPERCASE_LETTER;
+            case Character.LOWERCASE_LETTER:
+                // Break point if previous was not a letter.
+                return prevType > Character.OTHER_LETTER || prevType <= Character.UNASSIGNED;
+            case Character.DECIMAL_DIGIT_NUMBER:
+            case Character.LETTER_NUMBER:
+            case Character.OTHER_NUMBER:
+                // Break point if previous was not a number
+                return !(prevType == Character.DECIMAL_DIGIT_NUMBER
+                        || prevType == Character.LETTER_NUMBER
+                        || prevType == Character.OTHER_NUMBER);
+            case Character.MATH_SYMBOL:
+            case Character.CURRENCY_SYMBOL:
+            case Character.OTHER_PUNCTUATION:
+            case Character.DASH_PUNCTUATION:
+                // Always a break point for a symbol
+                return true;
+            default:
+                return  false;
+        }
+    }
+
+    /**
+     * Performs locale sensitive string comparison using {@link Collator}.
+     */
+    public static class StringMatcher {
+
+        private static final char MAX_UNICODE = '\uFFFF';
+
+        private final Collator mCollator;
+
+        StringMatcher() {
+            // On android N and above, Collator uses ICU implementation which has a much better
+            // support for non-latin locales.
+            mCollator = Collator.getInstance();
+            mCollator.setStrength(Collator.PRIMARY);
+            mCollator.setDecomposition(Collator.CANONICAL_DECOMPOSITION);
+        }
+
+        /**
+         * Returns true if {@param query} is a prefix of {@param target}
+         */
+        public boolean matches(String query, String target) {
+            switch (mCollator.compare(query, target)) {
+                case 0:
+                    return true;
+                case -1:
+                    // The target string can contain a modifier which would make it larger than
+                    // the query string (even though the length is same). If the query becomes
+                    // larger after appending a unicode character, it was originally a prefix of
+                    // the target string and hence should match.
+                    return mCollator.compare(query + MAX_UNICODE, target) > -1;
+                default:
+                    return false;
+            }
+        }
+
+        public static StringMatcher getInstance() {
+            return new StringMatcher();
+        }
+    }
+
+    /**
+     * Matching optimization to search in Chinese.
+     */
+    private static boolean requestSimpleFuzzySearch(String s) {
+        for (int i = 0; i < s.length(); ) {
+            int codepoint = s.codePointAt(i);
+            i += Character.charCount(codepoint);
+            switch (Character.UnicodeScript.of(codepoint)) {
+                case HAN:
+                    //Character.UnicodeScript.HAN: use String.contains to match
+                    return true;
+            }
+        }
+        return false;
+    }
+}
diff --git a/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java b/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java
index 0e8d4ae..f5e74b7 100644
--- a/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java
+++ b/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java
@@ -17,7 +17,6 @@
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
-import android.app.ActivityOptions;
 import android.content.Intent;
 import android.os.Bundle;
 import android.view.View;
@@ -168,11 +167,6 @@
     }
 
     @Override
-    public ActivityOptions getActivityLaunchOptions(View v) {
-        return null;
-    }
-
-    @Override
     protected void reapplyUi() { }
 
     @Override
diff --git a/src/com/android/launcher3/settings/DeveloperOptionsFragment.java b/src/com/android/launcher3/settings/DeveloperOptionsFragment.java
index c9fb956..8d676c9 100644
--- a/src/com/android/launcher3/settings/DeveloperOptionsFragment.java
+++ b/src/com/android/launcher3/settings/DeveloperOptionsFragment.java
@@ -88,7 +88,6 @@
     private PreferenceCategory mPluginsCategory;
     private FlagTogglerPrefUi mFlagTogglerPrefUi;
 
-
     @Override
     public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
         IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
diff --git a/src/com/android/launcher3/touch/ItemClickHandler.java b/src/com/android/launcher3/touch/ItemClickHandler.java
index 61bd30a..2e54904 100644
--- a/src/com/android/launcher3/touch/ItemClickHandler.java
+++ b/src/com/android/launcher3/touch/ItemClickHandler.java
@@ -180,7 +180,7 @@
                 LauncherApps launcherApps = launcher.getSystemService(LauncherApps.class);
                 try {
                     launcherApps.startPackageInstallerSessionDetailsActivity(sessionInfo, null,
-                            launcher.getActivityLaunchOptionsAsBundle(v));
+                            launcher.getActivityLaunchOptions(v).toBundle());
                     return;
                 } catch (Exception e) {
                     Log.e(TAG, "Unable to launch market intent for package=" + packageName, e);
@@ -304,7 +304,7 @@
                 intent.setPackage(null);
             }
         }
-        if (v != null && launcher.getAppTransitionManager().supportsAdaptiveIconAnimation(v)) {
+        if (v != null && launcher.supportsAdaptiveIconAnimation(v)) {
             // Preload the icon to reduce latency b/w swapping the floating view with the original.
             FloatingIconView.fetchIcon(launcher, v, item, true /* isOpening */);
         }
diff --git a/src/com/android/launcher3/util/ActivityOptionsWrapper.java b/src/com/android/launcher3/util/ActivityOptionsWrapper.java
new file mode 100644
index 0000000..99cc1f7
--- /dev/null
+++ b/src/com/android/launcher3/util/ActivityOptionsWrapper.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2021 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 android.app.ActivityOptions;
+import android.os.Bundle;
+
+/**
+ * A wrapper around {@link ActivityOptions} to allow custom functionality in launcher
+ */
+public class ActivityOptionsWrapper {
+
+    public final ActivityOptions options;
+    public final RunnableList onEndCallback;
+
+    public ActivityOptionsWrapper(ActivityOptions options, RunnableList onEndCallback) {
+        this.options = options;
+        this.onEndCallback = onEndCallback;
+    }
+
+    /**
+     * @see {@link ActivityOptions#toBundle()}
+     */
+    public Bundle toBundle() {
+        return options.toBundle();
+    }
+}
diff --git a/src/com/android/launcher3/util/MultiValueAlpha.java b/src/com/android/launcher3/util/MultiValueAlpha.java
index 5be9529..c79b1f6 100644
--- a/src/com/android/launcher3/util/MultiValueAlpha.java
+++ b/src/com/android/launcher3/util/MultiValueAlpha.java
@@ -42,16 +42,49 @@
                 }
             };
 
+    /**
+     * Determines how each alpha should factor into the final alpha.
+     */
+    public enum Mode {
+        BLEND(1f) {
+            @Override
+            public float calculateNewAlpha(float currentAlpha, float otherAlpha) {
+                return currentAlpha * otherAlpha;
+            }
+        },
+
+        MAX(0f) {
+            @Override
+            public float calculateNewAlpha(float currentAlpha, float otherAlpha) {
+                return Math.max(currentAlpha, otherAlpha);
+            }
+        };
+
+        Mode(float startAlpha) {
+            mStartAlpha = startAlpha;
+        }
+
+        protected final float mStartAlpha;
+        protected abstract float calculateNewAlpha(float currentAlpha, float otherAlpha);
+    }
+
     private final View mView;
     private final AlphaProperty[] mMyProperties;
+    private final Mode mMode;
 
     private int mValidMask;
     // Whether we should change from INVISIBLE to VISIBLE and vice versa at low alpha values.
     private boolean mUpdateVisibility;
 
     public MultiValueAlpha(View view, int size) {
+        this(view, size, Mode.BLEND);
+    }
+
+    public MultiValueAlpha(View view, int size, Mode mode) {
         mView = view;
         mMyProperties = new AlphaProperty[size];
+        mMode = mode;
+        mView.setAlpha(mMode.mStartAlpha);
 
         mValidMask = 0;
         for (int i = 0; i < size; i++) {
@@ -79,9 +112,9 @@
 
         private final int mMyMask;
 
-        private float mValue = 1;
+        private float mValue = mMode.mStartAlpha;
         // Factor of all other alpha channels, only valid if mMyMask is present in mValidMask.
-        private float mOthers = 1;
+        private float mOthers = mMode.mStartAlpha;
 
         AlphaProperty(int myMask) {
             mMyMask = myMask;
@@ -94,10 +127,10 @@
 
             if ((mValidMask & mMyMask) == 0) {
                 // Our cache value is not correct, recompute it.
-                mOthers = 1;
+                mOthers = mMode.mStartAlpha;
                 for (AlphaProperty prop : mMyProperties) {
                     if (prop != this) {
-                        mOthers *= prop.mValue;
+                        mOthers = mMode.calculateNewAlpha(mOthers, prop.mValue);
                     }
                 }
             }
@@ -107,7 +140,7 @@
             mValidMask = mMyMask;
             mValue = value;
 
-            mView.setAlpha(mOthers * mValue);
+            mView.setAlpha(mMode.calculateNewAlpha(mOthers, mValue));
             if (mUpdateVisibility) {
                 AlphaUpdateListener.updateVisibility(mView);
             }
diff --git a/src/com/android/launcher3/util/RunnableList.java b/src/com/android/launcher3/util/RunnableList.java
new file mode 100644
index 0000000..55add14
--- /dev/null
+++ b/src/com/android/launcher3/util/RunnableList.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2021 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 java.util.ArrayList;
+
+/**
+ * Utility class to hold a list of runnable
+ */
+public class RunnableList {
+
+    private ArrayList<Runnable> mList = null;
+    private boolean mDestroyed = false;
+
+    /**
+     * Ads a runnable to this list
+     */
+    public void add(Runnable runnable) {
+        if (mDestroyed) {
+            runnable.run();
+            return;
+        }
+        if (mList == null) {
+            mList = new ArrayList<>();
+        }
+        mList.add(runnable);
+    }
+
+    /**
+     * Destroys the list, executing any pending callbacks. All new callbacks are
+     * immediately executed
+     */
+    public void executeAllAndDestroy() {
+        mDestroyed = true;
+        executeAllAndClear();
+    }
+
+    /**
+     * Executes all previously added runnable and clears the list
+     */
+    public void executeAllAndClear() {
+        if (mList != null) {
+            ArrayList<Runnable> list = mList;
+            mList = null;
+            int count = list.size();
+            for (int i = 0; i < count; i++) {
+                list.get(i).run();
+            }
+        }
+    }
+}
diff --git a/src/com/android/launcher3/util/WallpaperOffsetInterpolator.java b/src/com/android/launcher3/util/WallpaperOffsetInterpolator.java
index 2ad80cf..b8554e4 100644
--- a/src/com/android/launcher3/util/WallpaperOffsetInterpolator.java
+++ b/src/com/android/launcher3/util/WallpaperOffsetInterpolator.java
@@ -145,14 +145,17 @@
         msg.sendToTarget();
     }
 
-    private void updateOffset() {
-        int numPagesForWallpaperParallax;
+    /** Returns the number of pages used for the wallpaper parallax. */
+    public int getNumPagesForWallpaperParallax() {
         if (mWallpaperIsLiveWallpaper) {
-            numPagesForWallpaperParallax = mNumScreens;
+            return mNumScreens;
         } else {
-            numPagesForWallpaperParallax = Math.max(MIN_PARALLAX_PAGE_SPAN, mNumScreens);
+            return Math.max(MIN_PARALLAX_PAGE_SPAN, mNumScreens);
         }
-        Message.obtain(mHandler, MSG_SET_NUM_PARALLAX, numPagesForWallpaperParallax, 0,
+    }
+
+    private void updateOffset() {
+        Message.obtain(mHandler, MSG_SET_NUM_PARALLAX, getNumPagesForWallpaperParallax(), 0,
                 mWindowToken).sendToTarget();
     }
 
diff --git a/src/com/android/launcher3/views/OptionsPopupView.java b/src/com/android/launcher3/views/OptionsPopupView.java
index 899dcf7..1a114f3 100644
--- a/src/com/android/launcher3/views/OptionsPopupView.java
+++ b/src/com/android/launcher3/views/OptionsPopupView.java
@@ -114,6 +114,11 @@
     }
 
     @Override
+    protected boolean shouldAddArrow() {
+        return false;
+    }
+
+    @Override
     protected void getTargetObjectLocation(Rect outPos) {
         mTargetRect.roundOut(outPos);
     }
diff --git a/src/com/android/launcher3/widget/BaseWidgetSheet.java b/src/com/android/launcher3/widget/BaseWidgetSheet.java
index 03c58bb..4fe631a 100644
--- a/src/com/android/launcher3/widget/BaseWidgetSheet.java
+++ b/src/com/android/launcher3/widget/BaseWidgetSheet.java
@@ -74,7 +74,18 @@
 
     @Override
     public final void onClick(View v) {
-        mWidgetInstructionToast = showWidgetToast(getContext(), mWidgetInstructionToast);
+        Object tag = null;
+        if (v instanceof WidgetCell) {
+            tag = v.getTag();
+        } else if (v.getParent() instanceof WidgetCell) {
+            tag = ((WidgetCell) v.getParent()).getTag();
+        }
+        if (tag instanceof PendingAddShortcutInfo) {
+            mWidgetInstructionToast = showShortcutToast(getContext(), mWidgetInstructionToast);
+        } else {
+            mWidgetInstructionToast = showWidgetToast(getContext(), mWidgetInstructionToast);
+        }
+
     }
 
     @Override
@@ -158,4 +169,21 @@
         toast.show();
         return toast;
     }
+
+    /**
+     * Show shortcut tap toast prompting user to drag instead.
+     */
+    private static Toast showShortcutToast(Context context, Toast toast) {
+        // Let the user know that they have to long press to add a widget
+        if (toast != null) {
+            toast.cancel();
+        }
+
+        CharSequence msg = Utilities.wrapForTts(
+                context.getText(R.string.long_press_shortcut_to_add),
+                context.getString(R.string.long_accessible_way_to_add_shortcut));
+        toast = Toast.makeText(context, msg, Toast.LENGTH_SHORT);
+        toast.show();
+        return toast;
+    }
 }
diff --git a/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java b/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
index 3285c18..df01295 100644
--- a/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
+++ b/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
@@ -16,12 +16,17 @@
 
 package com.android.launcher3.widget;
 
+import android.app.WallpaperManager;
 import android.appwidget.AppWidgetProviderInfo;
 import android.content.Context;
 import android.content.res.Configuration;
+import android.graphics.Rect;
+import android.graphics.RectF;
 import android.os.Handler;
 import android.os.SystemClock;
+import android.util.Log;
 import android.util.SparseBooleanArray;
+import android.util.SparseIntArray;
 import android.view.LayoutInflater;
 import android.view.MotionEvent;
 import android.view.View;
@@ -32,10 +37,13 @@
 import android.widget.Advanceable;
 import android.widget.RemoteViews;
 
+import androidx.annotation.Nullable;
+
 import com.android.launcher3.CheckLongPressHelper;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.Workspace;
 import com.android.launcher3.dragndrop.DragLayer;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
@@ -43,11 +51,16 @@
 import com.android.launcher3.util.Themes;
 import com.android.launcher3.views.BaseDragLayer.TouchCompleteListener;
 
+import java.util.List;
+
 /**
  * {@inheritDoc}
  */
 public class LauncherAppWidgetHostView extends NavigableAppWidgetHostView
-        implements TouchCompleteListener, View.OnLongClickListener {
+        implements TouchCompleteListener, View.OnLongClickListener,
+        LocalColorExtractor.Listener {
+
+    private static final String LOG_TAG = "LauncherAppWidgetHostView";
 
     // Related to the auto-advancing of widgets
     private static final long ADVANCE_INTERVAL = 20000;
@@ -60,18 +73,29 @@
 
     private final CheckLongPressHelper mLongPressHelper;
     protected final Launcher mLauncher;
+    private final Workspace mWorkspace;
+    private final WallpaperManager mWallpaperManager;
 
     @ViewDebug.ExportedProperty(category = "launcher")
     private boolean mReinflateOnConfigChange;
 
+    // Maintain the color manager.
+    private final LocalColorExtractor mColorExtractor;
+
     private boolean mIsScrollable;
     private boolean mIsAttachedToWindow;
     private boolean mIsAutoAdvanceRegistered;
     private Runnable mAutoAdvanceRunnable;
+    private RectF mLastLocationRegistered = null;
+    // Used to store the widget size during onLayout.
+    private final Rect mCurrentWidgetSize = new Rect();
+    private final RectF mTempRectF = new RectF();
+    private final boolean mIsRtl;
 
     public LauncherAppWidgetHostView(Context context) {
         super(context);
         mLauncher = Launcher.getLauncher(context);
+        mWorkspace = mLauncher.getWorkspace();
         mLongPressHelper = new CheckLongPressHelper(this, this);
         mInflater = LayoutInflater.from(context);
         setAccessibilityDelegate(mLauncher.getAccessibilityDelegate());
@@ -81,6 +105,19 @@
         if (Utilities.ATLEAST_Q && Themes.getAttrBoolean(mLauncher, R.attr.isWorkspaceDarkText)) {
             setOnLightBackground(true);
         }
+        mIsRtl = Utilities.isRtl(context.getResources());
+        mWallpaperManager = WallpaperManager.getInstance(getContext());
+        mColorExtractor = LocalColorExtractor.newInstance(getContext());
+        mColorExtractor.setListener(this);
+    }
+
+    @Override
+    public void setColorResources(@Nullable SparseIntArray colors) {
+        if (colors == null) {
+            resetColorResources();
+        } else {
+            super.setColorResources(colors);
+        }
     }
 
     @Override
@@ -167,6 +204,7 @@
         // state is updated. So isAttachedToWindow() will return true until next frame.
         mIsAttachedToWindow = false;
         checkIfAutoAdvance();
+        mColorExtractor.removeLocations();
     }
 
     @Override
@@ -213,6 +251,78 @@
         }
 
         mIsScrollable = checkScrollableRecursively(this);
+
+        mCurrentWidgetSize.left = left;
+        mCurrentWidgetSize.top = top;
+        mCurrentWidgetSize.right = right;
+        mCurrentWidgetSize.bottom = bottom;
+        updateColorExtraction(mCurrentWidgetSize);
+    }
+
+    private void updateColorExtraction(Rect widgetLocation) {
+        // If the widget hasn't been measured and laid out, we cannot do this.
+        if (widgetLocation.isEmpty()) {
+            return;
+        }
+        LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) getTag();
+        if (info != null) {
+            int screenWidth = mLauncher.getDeviceProfile().widthPx;
+            int screenHeight = mLauncher.getDeviceProfile().heightPx;
+            int numScreens = mWorkspace.getNumPagesForWallpaperParallax();
+            int screenId = mIsRtl ? numScreens - info.screenId : info.screenId;
+            float relativeScreenWidth = 1f / numScreens;
+            float absoluteTop = widgetLocation.top;
+            float absoluteBottom = widgetLocation.bottom;
+            for (View v = (View) getParent();
+                    v != null && v.getId() != R.id.launcher;
+                    v = (View) v.getParent()) {
+                absoluteBottom += v.getTop();
+                absoluteTop += v.getTop();
+            }
+            float xOffset = 0;
+            View parentView = (View) getParent();
+            // The layout depends on the orientation.
+            if (getResources().getConfiguration().orientation
+                    == Configuration.ORIENTATION_LANDSCAPE) {
+                xOffset = screenHeight - mWorkspace.getPaddingRight()
+                        - parentView.getWidth();
+            } else {
+                xOffset = mWorkspace.getPaddingLeft() + parentView.getPaddingLeft();
+            }
+            // This is the position of the widget relative to the wallpaper, as expected by the
+            // local color extraction of the WallpaperManager.
+            // The coordinate system is such that, on the horizontal axis, each screen has a
+            // distinct range on the [0,1] segment. So if there are 3 screens, they will have the
+            // ranges [0, 1/3], [1/3, 2/3] and [2/3, 1]. The position on the subrange should be
+            // the position of the widget relative to the screen. For the vertical axis, this is
+            // simply the location of the widget relative to the screen.
+            mTempRectF.left = ((widgetLocation.left + xOffset) / screenWidth + screenId)
+                    * relativeScreenWidth;
+            mTempRectF.right = ((widgetLocation.right + xOffset) / screenWidth + screenId)
+                    * relativeScreenWidth;
+            mTempRectF.top = absoluteTop / screenHeight;
+            mTempRectF.bottom = absoluteBottom / screenHeight;
+            if (mTempRectF.left < 0 || mTempRectF.right > 1 || mTempRectF.top < 0
+                    || mTempRectF.bottom > 1) {
+                Log.e(LOG_TAG, "   Error, invalid relative position");
+                return;
+            }
+            if (!mTempRectF.equals(mLastLocationRegistered)) {
+                if (mLastLocationRegistered != null) {
+                    mColorExtractor.removeLocations();
+                }
+                mLastLocationRegistered = new RectF(mTempRectF);
+                mColorExtractor.addLocation(List.of(mLastLocationRegistered));
+            }
+        } else {
+            mColorExtractor.removeLocations();
+        }
+    }
+
+    @Override
+    public void onColorsChanged(RectF rectF, SparseIntArray colors) {
+        // setColorResources will reapply the view, which must happen in the UI thread.
+        post(() -> setColorResources(colors));
     }
 
     @Override
@@ -225,6 +335,14 @@
     protected void onWindowVisibilityChanged(int visibility) {
         super.onWindowVisibilityChanged(visibility);
         maybeRegisterAutoAdvance();
+
+        if (visibility == View.VISIBLE) {
+            if (mLastLocationRegistered != null) {
+                mColorExtractor.addLocation(List.of(mLastLocationRegistered));
+            }
+        } else {
+            mColorExtractor.removeLocations();
+        }
     }
 
     private void checkIfAutoAdvance() {
diff --git a/src/com/android/launcher3/widget/LocalColorExtractor.java b/src/com/android/launcher3/widget/LocalColorExtractor.java
new file mode 100644
index 0000000..097158b
--- /dev/null
+++ b/src/com/android/launcher3/widget/LocalColorExtractor.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2009 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.widget;
+
+import android.appwidget.AppWidgetHostView;
+import android.content.Context;
+import android.graphics.RectF;
+import android.util.SparseIntArray;
+
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.R;
+import com.android.launcher3.util.ResourceBasedOverride;
+
+import java.util.List;
+
+/** Extracts the colors we need from the wallpaper at given locations. */
+public class LocalColorExtractor implements ResourceBasedOverride {
+
+    /** Listener for color changes on a screen location. */
+    public interface Listener {
+        /**
+         * Method called when the colors on a registered location has changed.
+         *
+         * {@code extractedColors} maps the color resources {@code android.R.colors.system_*} to
+         * their value, in a format that can be passed directly to
+         * {@link AppWidgetHostView#setColorResources(SparseIntArray)}.
+         */
+        void onColorsChanged(RectF rect, SparseIntArray extractedColors);
+    }
+
+    static LocalColorExtractor newInstance(Context context) {
+        return Overrides.getObject(LocalColorExtractor.class, context.getApplicationContext(),
+                R.string.local_colors_extraction_class);
+    }
+
+    /** Sets the object that will receive the color changes. */
+    public void setListener(@Nullable Listener listener) {
+        // no-op
+    }
+
+    /** Adds a list of locations to track with this listener. */
+    public void addLocation(List<RectF> locations) {
+        // no-op
+    }
+
+    /** Stops tracking any locations. */
+    public void removeLocations() {
+        // no-op
+    }
+}
diff --git a/src/com/android/launcher3/widget/WidgetAddFlowHandler.java b/src/com/android/launcher3/widget/WidgetAddFlowHandler.java
index 1ac5a33..9313266 100644
--- a/src/com/android/launcher3/widget/WidgetAddFlowHandler.java
+++ b/src/com/android/launcher3/widget/WidgetAddFlowHandler.java
@@ -15,6 +15,9 @@
  */
 package com.android.launcher3.widget;
 
+import static android.appwidget.AppWidgetProviderInfo.WIDGET_FEATURE_CONFIGURATION_OPTIONAL;
+import static android.appwidget.AppWidgetProviderInfo.WIDGET_FEATURE_RECONFIGURABLE;
+
 import android.appwidget.AppWidgetProviderInfo;
 import android.content.Context;
 import android.os.Parcel;
@@ -78,8 +81,22 @@
         return true;
     }
 
+    /**
+     * Checks whether the widget needs configuration.
+     *
+     * A widget needs configuration if (1) it has a configuration activity and (2)
+     * it's configuration is not optional.
+     *
+     * @return true if the widget needs configuration, false otherwise.
+     */
     public boolean needsConfigure() {
-        return mProviderInfo.configure != null;
+        int featureFlags = mProviderInfo.widgetFeatures;
+        // A widget's configuration is optional only if it's configuration is marked as optional AND
+        // it can be reconfigured later.
+        boolean configurationOptional = (featureFlags & WIDGET_FEATURE_CONFIGURATION_OPTIONAL) != 0
+                && (featureFlags & WIDGET_FEATURE_RECONFIGURABLE) != 0;
+
+        return mProviderInfo.configure != null && !configurationOptional;
     }
 
     public LauncherAppWidgetProviderInfo getProviderInfo(Context context) {
diff --git a/src/com/android/launcher3/widget/model/WidgetsListBaseEntry.java b/src/com/android/launcher3/widget/model/WidgetsListBaseEntry.java
index 09517e1..73bae6f 100644
--- a/src/com/android/launcher3/widget/model/WidgetsListBaseEntry.java
+++ b/src/com/android/launcher3/widget/model/WidgetsListBaseEntry.java
@@ -20,10 +20,14 @@
 
 import androidx.annotation.IntDef;
 
+import com.android.launcher3.model.WidgetItem;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.PackageItemInfo;
+import com.android.launcher3.widget.WidgetItemComparator;
 
 import java.lang.annotation.Retention;
+import java.util.List;
+import java.util.stream.Collectors;
 
 /** Holder class to store the package information of an entry shown in the widgets list. */
 public abstract class WidgetsListBaseEntry {
@@ -35,9 +39,14 @@
      */
     public final String mTitleSectionName;
 
-    public WidgetsListBaseEntry(PackageItemInfo pkgItem, String titleSectionName) {
+    public final List<WidgetItem> mWidgets;
+
+    public WidgetsListBaseEntry(PackageItemInfo pkgItem, String titleSectionName,
+            List<WidgetItem> items) {
         mPkgItem = pkgItem;
         mTitleSectionName = titleSectionName;
+        this.mWidgets =
+                items.stream().sorted(new WidgetItemComparator()).collect(Collectors.toList());
     }
 
     /**
@@ -51,10 +60,11 @@
     public abstract int getRank();
 
     @Retention(SOURCE)
-    @IntDef({RANK_WIDGETS_LIST_HEADER, RANK_WIDGETS_LIST_CONTENT})
+    @IntDef({RANK_WIDGETS_LIST_HEADER, RANK_WIDGETS_LIST_SEARCH_HEADER, RANK_WIDGETS_LIST_CONTENT})
     public @interface Rank {
     }
 
     public static final int RANK_WIDGETS_LIST_HEADER = 1;
-    public static final int RANK_WIDGETS_LIST_CONTENT = 2;
+    public static final int RANK_WIDGETS_LIST_SEARCH_HEADER = 2;
+    public static final int RANK_WIDGETS_LIST_CONTENT = 3;
 }
diff --git a/src/com/android/launcher3/widget/model/WidgetsListContentEntry.java b/src/com/android/launcher3/widget/model/WidgetsListContentEntry.java
index b0cb8c7..0328cf6 100644
--- a/src/com/android/launcher3/widget/model/WidgetsListContentEntry.java
+++ b/src/com/android/launcher3/widget/model/WidgetsListContentEntry.java
@@ -17,10 +17,8 @@
 
 import com.android.launcher3.model.WidgetItem;
 import com.android.launcher3.model.data.PackageItemInfo;
-import com.android.launcher3.widget.WidgetItemComparator;
 
 import java.util.List;
-import java.util.stream.Collectors;
 
 /**
  * Holder class to store all the information related to a list of widgets from the same app which is
@@ -28,18 +26,14 @@
  */
 public final class WidgetsListContentEntry extends WidgetsListBaseEntry {
 
-    public final List<WidgetItem> mWidgets;
-
     public WidgetsListContentEntry(PackageItemInfo pkgItem, String titleSectionName,
             List<WidgetItem> items) {
-        super(pkgItem, titleSectionName);
-        this.mWidgets =
-                items.stream().sorted(new WidgetItemComparator()).collect(Collectors.toList());
+        super(pkgItem, titleSectionName, items);
     }
 
     @Override
     public String toString() {
-        return mPkgItem.packageName + ":" + mWidgets.size();
+        return "Content:" + mPkgItem.packageName + ":" + mWidgets.size();
     }
 
     @Override
@@ -47,4 +41,12 @@
     public int getRank() {
         return RANK_WIDGETS_LIST_CONTENT;
     }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (!(obj instanceof WidgetsListContentEntry)) return false;
+        WidgetsListContentEntry otherEntry = (WidgetsListContentEntry) obj;
+        return mWidgets.equals(otherEntry.mWidgets) && mPkgItem.equals(otherEntry.mPkgItem)
+                && mTitleSectionName.equals(otherEntry.mTitleSectionName);
+    }
 }
diff --git a/src/com/android/launcher3/widget/model/WidgetsListHeaderEntry.java b/src/com/android/launcher3/widget/model/WidgetsListHeaderEntry.java
index 6899647..1fdc399 100644
--- a/src/com/android/launcher3/widget/model/WidgetsListHeaderEntry.java
+++ b/src/com/android/launcher3/widget/model/WidgetsListHeaderEntry.java
@@ -18,7 +18,7 @@
 import com.android.launcher3.model.WidgetItem;
 import com.android.launcher3.model.data.PackageItemInfo;
 
-import java.util.Collection;
+import java.util.List;
 
 /** An information holder for an app which has widgets or/and shortcuts. */
 public final class WidgetsListHeaderEntry extends WidgetsListBaseEntry {
@@ -30,8 +30,8 @@
     private boolean mHasEntryUpdated = false;
 
     public WidgetsListHeaderEntry(PackageItemInfo pkgItem, String titleSectionName,
-            Collection<WidgetItem> items) {
-        super(pkgItem, titleSectionName);
+            List<WidgetItem> items) {
+        super(pkgItem, titleSectionName, items);
         widgetsCount = (int) items.stream().filter(item -> item.widgetInfo != null).count();
         shortcutsCount = Math.max(0, items.size() - widgetsCount);
     }
@@ -57,8 +57,21 @@
     }
 
     @Override
+    public String toString() {
+        return "Header:" + mPkgItem.packageName + ":" + mWidgets.size();
+    }
+
+    @Override
     @Rank
     public int getRank() {
         return RANK_WIDGETS_LIST_HEADER;
     }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (!(obj instanceof WidgetsListHeaderEntry)) return false;
+        WidgetsListHeaderEntry otherEntry = (WidgetsListHeaderEntry) obj;
+        return mWidgets.equals(otherEntry.mWidgets) && mPkgItem.equals(otherEntry.mPkgItem)
+                && mTitleSectionName.equals(otherEntry.mTitleSectionName);
+    }
 }
diff --git a/src/com/android/launcher3/widget/model/WidgetsListSearchHeaderEntry.java b/src/com/android/launcher3/widget/model/WidgetsListSearchHeaderEntry.java
new file mode 100644
index 0000000..2aec3f8
--- /dev/null
+++ b/src/com/android/launcher3/widget/model/WidgetsListSearchHeaderEntry.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2021 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.widget.model;
+
+import com.android.launcher3.model.WidgetItem;
+import com.android.launcher3.model.data.PackageItemInfo;
+
+import java.util.List;
+
+/** An information holder for an app which has widgets or/and shortcuts, to be shown in search. */
+public final class WidgetsListSearchHeaderEntry extends WidgetsListBaseEntry {
+
+    private boolean mIsWidgetListShown = false;
+    private boolean mHasEntryUpdated = false;
+
+    public WidgetsListSearchHeaderEntry(PackageItemInfo pkgItem, String titleSectionName,
+            List<WidgetItem> items) {
+        super(pkgItem, titleSectionName, items);
+    }
+
+    /** Sets if the widgets list associated with this header is shown. */
+    public void setIsWidgetListShown(boolean isWidgetListShown) {
+        if (mIsWidgetListShown != isWidgetListShown) {
+            this.mIsWidgetListShown = isWidgetListShown;
+            mHasEntryUpdated = true;
+        } else {
+            mHasEntryUpdated = false;
+        }
+    }
+
+    /** Returns {@code true} if the widgets list associated with this header is shown. */
+    public boolean isWidgetListShown() {
+        return mIsWidgetListShown;
+    }
+
+    /** Returns {@code true} if this entry has been updated due to user interactions. */
+    public boolean hasEntryUpdated() {
+        return mHasEntryUpdated;
+    }
+
+    @Override
+    public String toString() {
+        return "SearchHeader:" + mPkgItem.packageName + ":" + mWidgets.size();
+    }
+
+    @Override
+    @Rank
+    public int getRank() {
+        return RANK_WIDGETS_LIST_SEARCH_HEADER;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (!(obj instanceof WidgetsListSearchHeaderEntry)) return false;
+        WidgetsListSearchHeaderEntry otherEntry = (WidgetsListSearchHeaderEntry) obj;
+        return mWidgets.equals(otherEntry.mWidgets) && mPkgItem.equals(otherEntry.mPkgItem)
+                && mTitleSectionName.equals(otherEntry.mTitleSectionName);
+    }
+}
diff --git a/src/com/android/launcher3/widget/picker/OnHeaderClickListener.java b/src/com/android/launcher3/widget/picker/OnHeaderClickListener.java
new file mode 100644
index 0000000..7372751
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/OnHeaderClickListener.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2021 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.widget.picker;
+
+import com.android.launcher3.util.PackageUserKey;
+
+/**
+ * A listener to be invoked when a header is clicked.
+ */
+public interface OnHeaderClickListener {
+    /**
+     * Calls when a header is clicked to show / hide widgets for a package.
+     */
+    void onHeaderClicked(boolean showWidgets, PackageUserKey packageUserKey);
+}
diff --git a/src/com/android/launcher3/widget/picker/SearchAndRecommendationsScrollController.java b/src/com/android/launcher3/widget/picker/SearchAndRecommendationsScrollController.java
index 95fa05f..7eb5b83 100644
--- a/src/com/android/launcher3/widget/picker/SearchAndRecommendationsScrollController.java
+++ b/src/com/android/launcher3/widget/picker/SearchAndRecommendationsScrollController.java
@@ -34,6 +34,7 @@
     private final boolean mHasWorkProfile;
     private final SearchAndRecommendationViewHolder mViewHolder;
     private final WidgetsRecyclerView mPrimaryRecyclerView;
+    private final WidgetsRecyclerView mSearchRecyclerView;
 
     // The following are only non null if mHasWorkProfile is true.
     @Nullable private final WidgetsRecyclerView mWorkRecyclerView;
@@ -48,12 +49,14 @@
             SearchAndRecommendationViewHolder viewHolder,
             WidgetsRecyclerView primaryRecyclerView,
             @Nullable WidgetsRecyclerView workRecyclerView,
+            WidgetsRecyclerView searchRecyclerView,
             @Nullable View personalWorkTabsView,
             @Nullable PersonalWorkPagedView primaryWorkViewPager) {
         mHasWorkProfile = hasWorkProfile;
         mViewHolder = viewHolder;
         mPrimaryRecyclerView = primaryRecyclerView;
         mWorkRecyclerView = workRecyclerView;
+        mSearchRecyclerView = searchRecyclerView;
         mPrimaryWorkTabsView = personalWorkTabsView;
         mPrimaryWorkViewPager = primaryWorkViewPager;
         mCurrentRecyclerView = mPrimaryRecyclerView;
@@ -149,6 +152,11 @@
                     mPrimaryRecyclerView.getPaddingRight(),
                     mPrimaryRecyclerView.getPaddingBottom());
         }
+        mSearchRecyclerView.setPadding(
+                mSearchRecyclerView.getPaddingLeft(),
+                topContainerHeight,
+                mSearchRecyclerView.getPaddingRight(),
+                mSearchRecyclerView.getPaddingBottom());
     }
 
     /**
diff --git a/src/com/android/launcher3/widget/picker/WidgetsDiffReporter.java b/src/com/android/launcher3/widget/picker/WidgetsDiffReporter.java
index dbd1bdf..2366609 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsDiffReporter.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsDiffReporter.java
@@ -25,6 +25,7 @@
 import com.android.launcher3.widget.model.WidgetsListBaseEntry;
 import com.android.launcher3.widget.model.WidgetsListContentEntry;
 import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
+import com.android.launcher3.widget.model.WidgetsListSearchHeaderEntry;
 import com.android.launcher3.widget.picker.WidgetsListAdapter.WidgetListBaseRowEntryComparator;
 
 import java.util.ArrayList;
@@ -113,7 +114,7 @@
                 // or did the header view changed due to user interactions?
                 // or did the widget size and desc, span, etc change?
                 if (!isSamePackageItemInfo(orgRowEntry.mPkgItem, newRowEntry.mPkgItem)
-                        || hasHeaderUpdated(newRowEntry)
+                        || hasHeaderUpdated(orgRowEntry, newRowEntry)
                         || hasWidgetsListChanged(orgRowEntry, newRowEntry)) {
                     index = currentEntries.indexOf(orgRowEntry);
                     currentEntries.set(index, newRowEntry);
@@ -174,12 +175,16 @@
      * Returns {@code true} if {@code newRow} is {@link WidgetsListHeaderEntry} and its content has
      * been changed due to user interactions.
      */
-    private boolean hasHeaderUpdated(WidgetsListBaseEntry newRow) {
-        if (!(newRow instanceof WidgetsListHeaderEntry)) {
-            return false;
+    private boolean hasHeaderUpdated(WidgetsListBaseEntry curRow, WidgetsListBaseEntry newRow) {
+        if (newRow instanceof WidgetsListHeaderEntry && curRow instanceof WidgetsListHeaderEntry) {
+            return ((WidgetsListHeaderEntry) newRow).hasEntryUpdated() || !curRow.equals(newRow);
         }
-        WidgetsListHeaderEntry newRowEntry = (WidgetsListHeaderEntry) newRow;
-        return newRowEntry.hasEntryUpdated();
+        if (newRow instanceof WidgetsListSearchHeaderEntry
+                && curRow instanceof WidgetsListSearchHeaderEntry) {
+            return ((WidgetsListSearchHeaderEntry) newRow).hasEntryUpdated()
+                    || !curRow.equals(newRow);
+        }
+        return false;
     }
 
     private boolean isSamePackageItemInfo(PackageItemInfo curInfo, PackageItemInfo newInfo) {
diff --git a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
index bf9b849..6b3c71a 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
@@ -34,7 +34,6 @@
 import android.view.View;
 import android.view.animation.AnimationUtils;
 import android.view.animation.Interpolator;
-import android.widget.EditText;
 import android.widget.TextView;
 
 import androidx.annotation.Nullable;
@@ -53,6 +52,8 @@
 import com.android.launcher3.widget.BaseWidgetSheet;
 import com.android.launcher3.widget.LauncherAppWidgetHost.ProviderChangedListener;
 import com.android.launcher3.widget.model.WidgetsListBaseEntry;
+import com.android.launcher3.widget.picker.search.SearchModeListener;
+import com.android.launcher3.widget.picker.search.WidgetsSearchBar;
 import com.android.launcher3.workprofile.PersonalWorkPagedView;
 import com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip.OnActivePageChangedListener;
 
@@ -64,7 +65,7 @@
  */
 public class WidgetsFullSheet extends BaseWidgetSheet
         implements Insettable, ProviderChangedListener, OnActivePageChangedListener,
-        WidgetsRecyclerView.HeaderViewDimensionsProvider {
+        WidgetsRecyclerView.HeaderViewDimensionsProvider, SearchModeListener {
 
     private static final long DEFAULT_OPEN_DURATION = 267;
     private static final long FADE_IN_DURATION = 150;
@@ -81,7 +82,9 @@
 
     @Nullable private PersonalWorkPagedView mViewPager;
     private int mInitialTabsHeight = 0;
+    private boolean mIsInSearchMode;
     private View mTabsView;
+    private TextView mNoWidgetsView;
     private SearchAndRecommendationViewHolder mSearchAndRecommendationViewHolder;
     private SearchAndRecommendationsScrollController mSearchAndRecommendationsScrollController;
 
@@ -90,6 +93,7 @@
         mHasWorkProfile = context.getSystemService(LauncherApps.class).getProfiles().size() > 1;
         mAdapters.put(AdapterHolder.PRIMARY, new AdapterHolder(AdapterHolder.PRIMARY));
         mAdapters.put(AdapterHolder.WORK, new AdapterHolder(AdapterHolder.WORK));
+        mAdapters.put(AdapterHolder.SEARCH, new AdapterHolder(AdapterHolder.SEARCH));
     }
 
     public WidgetsFullSheet(Context context, AttributeSet attrs) {
@@ -137,33 +141,59 @@
                 mSearchAndRecommendationViewHolder,
                 findViewById(R.id.primary_widgets_list_view),
                 mHasWorkProfile ? findViewById(R.id.work_widgets_list_view) : null,
+                findViewById(R.id.search_widgets_list_view),
                 mTabsView,
                 mViewPager);
         fastScroller.setOnFastScrollChangeListener(mSearchAndRecommendationsScrollController);
 
+        mNoWidgetsView = findViewById(R.id.no_widgets_text);
+
         onWidgetsBound();
+
+        mSearchAndRecommendationViewHolder.mSearchBar.initialize(
+                mLauncher.getPopupDataProvider().getAllWidgets(), /* searchModeListener= */ this);
     }
 
     @Override
     public void onActivePageChanged(int currentActivePage) {
+        AdapterHolder currentAdapterHolder = mAdapters.get(currentActivePage);
         WidgetsRecyclerView currentRecyclerView =
                 mAdapters.get(currentActivePage).mWidgetsRecyclerView;
-        currentRecyclerView.bindFastScrollbar();
-        mSearchAndRecommendationsScrollController.setCurrentRecyclerView(currentRecyclerView);
 
+        updateNoWidgetsView(currentAdapterHolder);
+
+        attachScrollbarToRecyclerView(currentRecyclerView);
+    }
+
+    private void attachScrollbarToRecyclerView(WidgetsRecyclerView recyclerView) {
+        recyclerView.bindFastScrollbar();
+        mSearchAndRecommendationsScrollController.setCurrentRecyclerView(recyclerView);
         reset();
     }
 
+    private void updateNoWidgetsView(AdapterHolder adapterHolder) {
+        boolean isWidgetAvailable = adapterHolder.mWidgetsListAdapter.getItemCount() > 0;
+        adapterHolder.mWidgetsRecyclerView.setVisibility(isWidgetAvailable ? VISIBLE : GONE);
+
+        // Always resets the text in case this is updated by search.
+        mNoWidgetsView.setText(R.string.no_widgets_available);
+        mNoWidgetsView.setVisibility(isWidgetAvailable ? GONE : VISIBLE);
+    }
+
     private void reset() {
         mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView.scrollToTop();
         if (mHasWorkProfile) {
             mAdapters.get(AdapterHolder.WORK).mWidgetsRecyclerView.scrollToTop();
         }
+        mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView.scrollToTop();
         mSearchAndRecommendationsScrollController.reset();
     }
 
     @VisibleForTesting
     public WidgetsRecyclerView getRecyclerView() {
+        if (mIsInSearchMode) {
+            return mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView;
+        }
         if (!mHasWorkProfile || mViewPager.getCurrentPage() == AdapterHolder.PRIMARY) {
             return mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView;
         }
@@ -275,7 +305,11 @@
 
         AdapterHolder primaryUserAdapterHolder = mAdapters.get(AdapterHolder.PRIMARY);
         primaryUserAdapterHolder.setup(findViewById(R.id.primary_widgets_list_view));
+        AdapterHolder searchAdapterHolder = mAdapters.get(AdapterHolder.SEARCH);
+        searchAdapterHolder.setup(findViewById(R.id.search_widgets_list_view));
         primaryUserAdapterHolder.mWidgetsListAdapter.setWidgets(allWidgets);
+        updateNoWidgetsView(primaryUserAdapterHolder);
+
         if (mHasWorkProfile) {
             AdapterHolder workUserAdapterHolder = mAdapters.get(AdapterHolder.WORK);
             workUserAdapterHolder.setup(findViewById(R.id.work_widgets_list_view));
@@ -284,6 +318,40 @@
         }
     }
 
+    @Override
+    public void enterSearchMode() {
+        if (mIsInSearchMode) return;
+        setViewVisibilityBasedOnSearch(/*isInSearchMode= */ true);
+        attachScrollbarToRecyclerView(mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView);
+    }
+
+    @Override
+    public void exitSearchMode() {
+        setViewVisibilityBasedOnSearch(/*isInSearchMode=*/ false);
+        if (mHasWorkProfile) {
+            mViewPager.snapToPage(AdapterHolder.PRIMARY);
+        }
+        attachScrollbarToRecyclerView(mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView);
+    }
+
+    @Override
+    public void onSearchResults(List<WidgetsListBaseEntry> entries) {
+        mAdapters.get(AdapterHolder.SEARCH).mWidgetsListAdapter.setWidgetsOnSearch(entries);
+    }
+
+    private void setViewVisibilityBasedOnSearch(boolean isInSearchMode) {
+        mIsInSearchMode = isInSearchMode;
+        if (mHasWorkProfile) {
+            mViewPager.setVisibility(isInSearchMode ? GONE : VISIBLE);
+            mTabsView.setVisibility(isInSearchMode ? GONE : VISIBLE);
+        } else {
+            mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView
+                    .setVisibility(isInSearchMode ? GONE : VISIBLE);
+        }
+        mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView
+                .setVisibility(mIsInSearchMode ? VISIBLE : GONE);
+    }
+
     private void open(boolean animate) {
         if (animate) {
             if (getPopupContainer().getInsets().bottom > 0) {
@@ -369,14 +437,16 @@
 
     @Override
     public int getHeaderViewHeight() {
-        // No need to check work profile here because mInitialTabHeight is always 0 if there is no
-        // work profile.
-        return mInitialTabsHeight
-                + measureHeightWithVerticalMargins(mSearchAndRecommendationViewHolder.mContainer);
+        return measureHeightWithVerticalMargins(mSearchAndRecommendationViewHolder.mCollapseHandle)
+                + measureHeightWithVerticalMargins(mSearchAndRecommendationViewHolder.mHeaderTitle)
+                + measureHeightWithVerticalMargins(mSearchAndRecommendationViewHolder.mSearchBar);
     }
 
     /** private the height, in pixel, + the vertical margins of a given view. */
     private static int measureHeightWithVerticalMargins(View view) {
+        if (view.getVisibility() != VISIBLE) {
+            return 0;
+        }
         MarginLayoutParams marginLayoutParams = (MarginLayoutParams) view.getLayoutParams();
         return view.getMeasuredHeight() + marginLayoutParams.bottomMargin
                 + marginLayoutParams.topMargin;
@@ -386,6 +456,7 @@
     private final class AdapterHolder {
         static final int PRIMARY = 0;
         static final int WORK = 1;
+        static final int SEARCH = 2;
 
         private final int mAdapterType;
         private final WidgetsListAdapter mWidgetsListAdapter;
@@ -404,8 +475,16 @@
                     apps.getIconCache(),
                     /* iconClickListener= */ WidgetsFullSheet.this,
                     /* iconLongClickListener= */ WidgetsFullSheet.this);
-            mWidgetsListAdapter.setFilter(
-                    mAdapterType == PRIMARY ? mPrimaryWidgetsFilter : mWorkWidgetsFilter);
+            switch (mAdapterType) {
+                case PRIMARY:
+                    mWidgetsListAdapter.setFilter(mPrimaryWidgetsFilter);
+                    break;
+                case WORK:
+                    mWidgetsListAdapter.setFilter(mWorkWidgetsFilter);
+                    break;
+                default:
+                    break;
+            }
         }
 
         void setup(WidgetsRecyclerView recyclerView) {
@@ -421,7 +500,7 @@
     final class SearchAndRecommendationViewHolder {
         final View mContainer;
         final View mCollapseHandle;
-        final EditText mSearchBar;
+        final WidgetsSearchBar mSearchBar;
         final TextView mHeaderTitle;
 
         SearchAndRecommendationViewHolder(View searchAndRecommendationContainer) {
diff --git a/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java b/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java
index 8b49d1e..9009eb1 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java
@@ -34,11 +34,12 @@
 import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.recyclerview.ViewHolderBinder;
 import com.android.launcher3.util.LabelComparator;
+import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.widget.WidgetCell;
 import com.android.launcher3.widget.model.WidgetsListBaseEntry;
 import com.android.launcher3.widget.model.WidgetsListContentEntry;
 import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
-import com.android.launcher3.widget.picker.WidgetsListHeaderViewHolderBinder.OnHeaderClickListener;
+import com.android.launcher3.widget.model.WidgetsListSearchHeaderEntry;
 
 import java.util.ArrayList;
 import java.util.Comparator;
@@ -62,8 +63,9 @@
     private static final boolean DEBUG = false;
 
     /** Uniquely identifies widgets list view type within the app. */
-    private static final int VIEW_TYPE_WIDGETS_LIST = R.layout.widgets_list_row_view;
-    private static final int VIEW_TYPE_WIDGETS_HEADER = R.layout.widgets_list_row_header;
+    private static final int VIEW_TYPE_WIDGETS_LIST = R.id.view_type_widgets_list;
+    private static final int VIEW_TYPE_WIDGETS_HEADER = R.id.view_type_widgets_header;
+    private static final int VIEW_TYPE_WIDGETS_SEARCH_HEADER = R.id.view_type_widgets_search_header;
 
     private final WidgetsDiffReporter mDiffReporter;
     private final SparseArray<ViewHolderBinder> mViewHolderBinders = new SparseArray<>();
@@ -73,11 +75,13 @@
 
     private List<WidgetsListBaseEntry> mAllEntries = new ArrayList<>();
     private ArrayList<WidgetsListBaseEntry> mVisibleEntries = new ArrayList<>();
-    @Nullable private String mWidgetsContentVisiblePackage = null;
+    @Nullable private PackageUserKey mWidgetsContentVisiblePackageUserKey = null;
 
     private Predicate<WidgetsListBaseEntry> mHeaderAndSelectedContentFilter = entry ->
             entry instanceof WidgetsListHeaderEntry
-                    || entry.mPkgItem.packageName.equals(mWidgetsContentVisiblePackage);
+                    || entry instanceof WidgetsListSearchHeaderEntry
+                    || new PackageUserKey(entry.mPkgItem.packageName, entry.mPkgItem.user)
+                    .equals(mWidgetsContentVisiblePackageUserKey);
     @Nullable private Predicate<WidgetsListBaseEntry> mFilter = null;
 
     public WidgetsListAdapter(Context context, LayoutInflater layoutInflater,
@@ -87,8 +91,14 @@
         mWidgetsListTableViewHolderBinder = new WidgetsListTableViewHolderBinder(context,
                 layoutInflater, iconClickListener, iconLongClickListener, widgetPreviewLoader);
         mViewHolderBinders.put(VIEW_TYPE_WIDGETS_LIST, mWidgetsListTableViewHolderBinder);
-        mViewHolderBinders.put(VIEW_TYPE_WIDGETS_HEADER,
-                new WidgetsListHeaderViewHolderBinder(layoutInflater, this::onHeaderClicked));
+        mViewHolderBinders.put(
+                VIEW_TYPE_WIDGETS_HEADER,
+                new WidgetsListHeaderViewHolderBinder(
+                        layoutInflater, /*onHeaderClickListener=*/this));
+        mViewHolderBinders.put(
+                VIEW_TYPE_WIDGETS_SEARCH_HEADER,
+                new WidgetsListSearchHeaderViewHolderBinder(
+                        layoutInflater, /*onHeaderClickListener=*/ this));
     }
 
     public void setFilter(Predicate<WidgetsListBaseEntry> filter) {
@@ -122,23 +132,40 @@
         return mVisibleEntries.size();
     }
 
+    /** Returns all items that will be drawn in a recycler view. */
+    public List<WidgetsListBaseEntry> getItems() {
+        return mVisibleEntries;
+    }
+
     /** Gets the section name for {@link com.android.launcher3.views.RecyclerViewFastScroller}. */
     public String getSectionName(int pos) {
         return mVisibleEntries.get(pos).mTitleSectionName;
     }
 
-    /** Updates the widget list. */
+    /** Updates the widget list based on {@code tempEntries}. */
     public void setWidgets(List<WidgetsListBaseEntry> tempEntries) {
         mAllEntries = tempEntries.stream().sorted(mRowComparator)
                 .collect(Collectors.toList());
         updateVisibleEntries();
     }
 
+    /** Updates the widget list based on {@code searchResults}. */
+    public void setWidgetsOnSearch(List<WidgetsListBaseEntry> searchResults) {
+        // Forget the expanded package every time widget list is refreshed in search mode.
+        mWidgetsContentVisiblePackageUserKey = null;
+        setWidgets(searchResults);
+    }
+
     private void updateVisibleEntries() {
         mAllEntries.forEach(entry -> {
             if (entry instanceof WidgetsListHeaderEntry) {
                 ((WidgetsListHeaderEntry) entry).setIsWidgetListShown(
-                        entry.mPkgItem.packageName.equals(mWidgetsContentVisiblePackage));
+                        new PackageUserKey(entry.mPkgItem.packageName, entry.mPkgItem.user)
+                                .equals(mWidgetsContentVisiblePackageUserKey));
+            } else if (entry instanceof WidgetsListSearchHeaderEntry) {
+                ((WidgetsListSearchHeaderEntry) entry).setIsWidgetListShown(
+                        new PackageUserKey(entry.mPkgItem.packageName, entry.mPkgItem.user)
+                                .equals(mWidgetsContentVisiblePackageUserKey));
             }
         });
         List<WidgetsListBaseEntry> newVisibleEntries = mAllEntries.stream()
@@ -189,17 +216,19 @@
             return VIEW_TYPE_WIDGETS_LIST;
         } else if (entry instanceof WidgetsListHeaderEntry) {
             return VIEW_TYPE_WIDGETS_HEADER;
+        } else if (entry instanceof WidgetsListSearchHeaderEntry) {
+            return VIEW_TYPE_WIDGETS_SEARCH_HEADER;
         }
         throw new UnsupportedOperationException("ViewHolderBinder not found for " + entry);
     }
 
     @Override
-    public void onHeaderClicked(boolean showWidgets, String expandedPackage) {
+    public void onHeaderClicked(boolean showWidgets, PackageUserKey packageUserKey) {
         if (showWidgets) {
-            mWidgetsContentVisiblePackage = expandedPackage;
+            mWidgetsContentVisiblePackageUserKey = packageUserKey;
             updateVisibleEntries();
-        } else if (expandedPackage.equals(mWidgetsContentVisiblePackage)) {
-            mWidgetsContentVisiblePackage = null;
+        } else if (packageUserKey.equals(mWidgetsContentVisiblePackageUserKey)) {
+            mWidgetsContentVisiblePackageUserKey = null;
             updateVisibleEntries();
         }
     }
diff --git a/src/com/android/launcher3/widget/picker/WidgetsListHeader.java b/src/com/android/launcher3/widget/picker/WidgetsListHeader.java
index 070a9aa..119d094 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsListHeader.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsListHeader.java
@@ -41,6 +41,9 @@
 import com.android.launcher3.model.data.PackageItemInfo;
 import com.android.launcher3.views.ActivityContext;
 import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
+import com.android.launcher3.widget.model.WidgetsListSearchHeaderEntry;
+
+import java.util.stream.Collectors;
 
 /**
  * A UI represents a header of an app shown in the full widgets tray.
@@ -173,7 +176,7 @@
                     shortcutsCount);
         } else if (entry.widgetsCount > 0) {
             subtitle = resources.getQuantityString(R.plurals.widgets_count,
-                     entry.widgetsCount, entry.widgetsCount);
+                    entry.widgetsCount, entry.widgetsCount);
         } else {
             subtitle = resources.getQuantityString(R.plurals.shortcuts_count,
                     entry.shortcutsCount, entry.shortcutsCount);
@@ -182,6 +185,32 @@
         mSubtitle.setVisibility(VISIBLE);
     }
 
+    /** Apply app icon, labels and tag using a generic {@link WidgetsListSearchHeaderEntry}. */
+    @UiThread
+    public void applyFromItemInfoWithIcon(WidgetsListSearchHeaderEntry entry) {
+        applyIconAndLabel(entry);
+    }
+
+    @UiThread
+    private void applyIconAndLabel(WidgetsListSearchHeaderEntry entry) {
+        PackageItemInfo info = entry.mPkgItem;
+        setIcon(info);
+        setTitles(entry);
+        setExpanded(entry.isWidgetListShown());
+
+        super.setTag(info);
+
+        verifyHighRes();
+    }
+
+    private void setTitles(WidgetsListSearchHeaderEntry entry) {
+        mTitle.setText(entry.mPkgItem.title);
+
+        mSubtitle.setText(entry.mWidgets.stream()
+                .map(item -> item.label).sorted().collect(Collectors.joining(", ")));
+        mSubtitle.setVisibility(VISIBLE);
+    }
+
     @Override
     public void reapplyItemInfo(ItemInfoWithIcon info) {
         if (getTag() == info) {
diff --git a/src/com/android/launcher3/widget/picker/WidgetsListHeaderViewHolderBinder.java b/src/com/android/launcher3/widget/picker/WidgetsListHeaderViewHolderBinder.java
index ed53e6f..fcefe3a 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsListHeaderViewHolderBinder.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsListHeaderViewHolderBinder.java
@@ -20,6 +20,7 @@
 
 import com.android.launcher3.R;
 import com.android.launcher3.recyclerview.ViewHolderBinder;
+import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
 
 /**
@@ -50,12 +51,9 @@
         widgetsListHeader.applyFromItemInfoWithIcon(data);
         widgetsListHeader.setExpanded(data.isWidgetListShown());
         widgetsListHeader.setOnExpandChangeListener(isExpanded ->
-                mOnHeaderClickListener.onHeaderClicked(isExpanded, data.mPkgItem.packageName));
-    }
-
-    /** A listener to be invoked when {@link WidgetsListHeader} is clicked. */
-    public interface OnHeaderClickListener {
-        /** Calls when {@link WidgetsListHeader} is clicked to show / hide widgets for a package. */
-        void onHeaderClicked(boolean showWidgets, String packageName);
+                mOnHeaderClickListener.onHeaderClicked(
+                        isExpanded,
+                        new PackageUserKey(data.mPkgItem.packageName, data.mPkgItem.user)
+                ));
     }
 }
diff --git a/src/com/android/launcher3/widget/picker/WidgetsListSearchHeaderHolder.java b/src/com/android/launcher3/widget/picker/WidgetsListSearchHeaderHolder.java
new file mode 100644
index 0000000..9562af3
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/WidgetsListSearchHeaderHolder.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2021 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.widget.picker;
+
+import androidx.recyclerview.widget.RecyclerView.ViewHolder;
+
+/**
+ * A {@link ViewHolder} for {@link WidgetsListHeader} of an app, which renders the app icon, the app
+ * name, label and a button for showing / hiding widgets.
+ */
+public final class WidgetsListSearchHeaderHolder extends ViewHolder {
+    final WidgetsListHeader mWidgetsListHeader;
+
+    public WidgetsListSearchHeaderHolder(WidgetsListHeader view) {
+        super(view);
+
+        mWidgetsListHeader = view;
+    }
+}
diff --git a/src/com/android/launcher3/widget/picker/WidgetsListSearchHeaderViewHolderBinder.java b/src/com/android/launcher3/widget/picker/WidgetsListSearchHeaderViewHolderBinder.java
new file mode 100644
index 0000000..83c7948
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/WidgetsListSearchHeaderViewHolderBinder.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2021 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.widget.picker;
+
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+
+import com.android.launcher3.R;
+import com.android.launcher3.recyclerview.ViewHolderBinder;
+import com.android.launcher3.util.PackageUserKey;
+import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
+import com.android.launcher3.widget.model.WidgetsListSearchHeaderEntry;
+
+/**
+ * Binds data from {@link WidgetsListHeaderEntry} to UI elements in {@link WidgetsListHeaderHolder}.
+ */
+public final class WidgetsListSearchHeaderViewHolderBinder implements
+        ViewHolderBinder<WidgetsListSearchHeaderEntry, WidgetsListSearchHeaderHolder> {
+    private final LayoutInflater mLayoutInflater;
+    private final OnHeaderClickListener mOnHeaderClickListener;
+
+    public WidgetsListSearchHeaderViewHolderBinder(LayoutInflater layoutInflater,
+            OnHeaderClickListener onHeaderClickListener) {
+        mLayoutInflater = layoutInflater;
+        mOnHeaderClickListener = onHeaderClickListener;
+    }
+
+    @Override
+    public WidgetsListSearchHeaderHolder newViewHolder(ViewGroup parent) {
+        WidgetsListHeader header = (WidgetsListHeader) mLayoutInflater.inflate(
+                R.layout.widgets_list_row_header, parent, false);
+
+        return new WidgetsListSearchHeaderHolder(header);
+    }
+
+    @Override
+    public void bindViewHolder(WidgetsListSearchHeaderHolder viewHolder,
+            WidgetsListSearchHeaderEntry data) {
+        WidgetsListHeader widgetsListHeader = viewHolder.mWidgetsListHeader;
+        widgetsListHeader.applyFromItemInfoWithIcon(data);
+        widgetsListHeader.setExpanded(data.isWidgetListShown());
+        widgetsListHeader.setOnExpandChangeListener(isExpanded ->
+                mOnHeaderClickListener.onHeaderClicked(isExpanded,
+                        new PackageUserKey(data.mPkgItem.packageName, data.mPkgItem.user)));
+    }
+}
diff --git a/src/com/android/launcher3/widget/picker/WidgetsRecyclerView.java b/src/com/android/launcher3/widget/picker/WidgetsRecyclerView.java
index d65a809..9ab6424 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsRecyclerView.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsRecyclerView.java
@@ -21,13 +21,19 @@
 import android.util.AttributeSet;
 import android.view.MotionEvent;
 import android.view.View;
+import android.widget.TableLayout;
 
 import androidx.recyclerview.widget.LinearLayoutManager;
 import androidx.recyclerview.widget.RecyclerView;
 import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener;
 
 import com.android.launcher3.BaseRecyclerView;
+import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.R;
+import com.android.launcher3.views.ActivityContext;
+import com.android.launcher3.widget.model.WidgetsListBaseEntry;
+import com.android.launcher3.widget.model.WidgetsListContentEntry;
+import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
 
 /**
  * The widgets recycler view.
@@ -39,8 +45,10 @@
     private final int mScrollbarTop;
 
     private final Point mFastScrollerOffset = new Point();
+    private final int mEstimatedWidgetListHeaderHeight;
     private boolean mTouchDownOnScroller;
     private HeaderViewDimensionsProvider mHeaderViewDimensionsProvider;
+    private int mLastVisibleWidgetContentTableHeight = 0;
 
     public WidgetsRecyclerView(Context context) {
         this(context, null);
@@ -55,6 +63,12 @@
         super(context, attrs, defStyleAttr);
         mScrollbarTop = getResources().getDimensionPixelSize(R.dimen.dynamic_grid_edge_margin);
         addOnItemTouchListener(this);
+
+        ActivityContext activity = ActivityContext.lookupContext(getContext());
+        DeviceProfile grid = activity.getDeviceProfile();
+        mEstimatedWidgetListHeaderHeight = grid.iconSizePx
+                + 2 * context.getResources().getDimensionPixelSize(
+                        R.dimen.widget_list_header_view_vertical_padding);
     }
 
     @Override
@@ -123,21 +137,32 @@
 
         View child = getChildAt(0);
         int rowIndex = getChildPosition(child);
-        int y = (child.getMeasuredHeight() * rowIndex);
+        for (int i = 0; i < getChildCount(); i++) {
+            View view = getChildAt(i);
+            if (view instanceof TableLayout) {
+                // This assumes there is ever only one content shown in this recycler view.
+                mLastVisibleWidgetContentTableHeight = view.getMeasuredHeight();
+            }
+        }
+
+        int scrollPosition = getItemsHeight(rowIndex);
         int offset = getLayoutManager().getDecoratedTop(child);
 
-        return getPaddingTop() + y - offset;
+        return getPaddingTop() + scrollPosition - offset;
     }
 
     /**
-     * Returns the available scroll height:
-     *   AvailableScrollHeight = Total height of the all items - last page height
+     * Returns the available scroll height, in pixel.
+     *
+     * <p>If the recycler view can't be scrolled, returns 0.
      */
     @Override
     protected int getAvailableScrollHeight() {
-        View child = getChildAt(0);
-        return child.getMeasuredHeight() * mAdapter.getItemCount() + getScrollBarTop()
-                + getPaddingBottom() - mScrollbar.getHeight();
+        // AvailableScrollHeight = Total height of the all items - first page height
+        int firstPageHeight = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();
+        int totalHeightOfAllItems = getItemsHeight(/* untilIndex= */ mAdapter.getItemCount());
+        int availableScrollHeight = totalHeightOfAllItems - firstPageHeight;
+        return Math.max(0, availableScrollHeight);
     }
 
     private boolean isModelNotReady() {
@@ -181,6 +206,31 @@
     }
 
     /**
+     * Returns the sum of the height, in pixels, of this list adapter's items from index 0 until
+     * {@code untilIndex}.
+     *
+     * <p>If the untilIndex is larger than the total number of items in this adapter, returns the
+     * sum of all items' height.
+     */
+    private int getItemsHeight(int untilIndex) {
+        if (untilIndex > mAdapter.getItems().size()) {
+            untilIndex = mAdapter.getItems().size();
+        }
+        int totalItemsHeight = 0;
+        for (int i = 0; i < untilIndex; i++) {
+            WidgetsListBaseEntry entry = mAdapter.getItems().get(i);
+            if (entry instanceof WidgetsListHeaderEntry) {
+                totalItemsHeight += mEstimatedWidgetListHeaderHeight;
+            } else if (entry instanceof WidgetsListContentEntry) {
+                totalItemsHeight += mLastVisibleWidgetContentTableHeight;
+            } else {
+                throw new UnsupportedOperationException("Can't estimate height for " + entry);
+            }
+        }
+        return totalItemsHeight;
+    }
+
+    /**
      * Provides dimensions of the header view that is shown at the top of a
      * {@link WidgetsRecyclerView}.
      */
diff --git a/src/com/android/launcher3/widget/picker/search/SearchModeListener.java b/src/com/android/launcher3/widget/picker/search/SearchModeListener.java
new file mode 100644
index 0000000..cee7d67
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/search/SearchModeListener.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2021 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.widget.picker.search;
+
+import com.android.launcher3.widget.model.WidgetsListBaseEntry;
+
+import java.util.List;
+
+/**
+ * A listener to help with widgets picker search.
+ */
+public interface SearchModeListener {
+    /**
+     * Notifies the subscriber when user enters widget picker search mode.
+     */
+    void enterSearchMode();
+
+    /**
+     * Notifies the subscriber when user exits widget picker search mode.
+     */
+    void exitSearchMode();
+
+    /**
+     * Notifies the subscriber with search results.
+     */
+    void onSearchResults(List<WidgetsListBaseEntry> entries);
+}
diff --git a/src/com/android/launcher3/widget/picker/search/SimpleWidgetsSearchAlgorithm.java b/src/com/android/launcher3/widget/picker/search/SimpleWidgetsSearchAlgorithm.java
new file mode 100644
index 0000000..15d2454
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/search/SimpleWidgetsSearchAlgorithm.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2021 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.widget.picker.search;
+
+import android.os.Handler;
+import android.util.Log;
+
+import com.android.launcher3.search.SearchAlgorithm;
+import com.android.launcher3.search.SearchCallback;
+import com.android.launcher3.widget.model.WidgetsListBaseEntry;
+
+import java.util.ArrayList;
+
+/**
+ * Implementation of {@link SearchAlgorithm} that posts a task to query on the main thread.
+ */
+public final class SimpleWidgetsSearchAlgorithm implements SearchAlgorithm<WidgetsListBaseEntry> {
+
+    private static final boolean DEBUG = false;
+    private static final String TAG = "SimpleWidgetsSearchAlgo";
+    private static final String DELIM = "\t";
+
+    private final Handler mResultHandler;
+    private final WidgetsPickerSearchPipeline mSearchPipeline;
+
+    public SimpleWidgetsSearchAlgorithm(WidgetsPickerSearchPipeline searchPipeline) {
+        mResultHandler = new Handler();
+        mSearchPipeline = searchPipeline;
+    }
+
+    @Override
+    public void doSearch(String query, SearchCallback<WidgetsListBaseEntry> callback) {
+        long startTime = System.currentTimeMillis();
+        String queryToken = query + DELIM + startTime;
+        if (DEBUG) {
+            Log.d(TAG, "doSearch queryToken:" + queryToken);
+        }
+        mSearchPipeline.query(query,
+                results -> mResultHandler.post(
+                        () -> callback.onSearchResult(queryToken, new ArrayList(results))));
+    }
+
+    @Override
+    public void cancel(boolean interruptActiveRequests) {
+        if (interruptActiveRequests) {
+            mResultHandler.removeCallbacksAndMessages(/*token= */null);
+        }
+    }
+}
diff --git a/src/com/android/launcher3/widget/picker/search/SimpleWidgetsSearchPipeline.java b/src/com/android/launcher3/widget/picker/search/SimpleWidgetsSearchPipeline.java
new file mode 100644
index 0000000..5222e8e
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/search/SimpleWidgetsSearchPipeline.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2021 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.widget.picker.search;
+
+import static com.android.launcher3.search.StringMatcherUtility.matches;
+
+import com.android.launcher3.model.WidgetItem;
+import com.android.launcher3.search.StringMatcherUtility.StringMatcher;
+import com.android.launcher3.widget.model.WidgetsListBaseEntry;
+import com.android.launcher3.widget.model.WidgetsListContentEntry;
+import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
+import com.android.launcher3.widget.model.WidgetsListSearchHeaderEntry;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+
+/**
+ * Implementation of {@link WidgetsPickerSearchPipeline} that performs search by prefix matching on
+ * app names and widget labels.
+ */
+public final class SimpleWidgetsSearchPipeline implements WidgetsPickerSearchPipeline {
+
+    private final List<WidgetsListBaseEntry> mAllEntries;
+
+    public SimpleWidgetsSearchPipeline(List<WidgetsListBaseEntry> allEntries) {
+        mAllEntries = allEntries;
+    }
+
+    @Override
+    public void query(String input, Consumer<List<WidgetsListBaseEntry>> callback) {
+        ArrayList<WidgetsListBaseEntry> results = new ArrayList<>();
+        mAllEntries.stream().filter(entry -> entry instanceof WidgetsListHeaderEntry)
+                .forEach(headerEntry -> {
+                    List<WidgetItem> matchedWidgetItems = filterWidgetItems(
+                            input, headerEntry.mPkgItem.title.toString(), headerEntry.mWidgets);
+                    if (matchedWidgetItems.size() > 0) {
+                        results.add(new WidgetsListSearchHeaderEntry(headerEntry.mPkgItem,
+                                headerEntry.mTitleSectionName, matchedWidgetItems));
+                        results.add(new WidgetsListContentEntry(headerEntry.mPkgItem,
+                                headerEntry.mTitleSectionName, matchedWidgetItems));
+                    }
+                });
+        callback.accept(results);
+    }
+
+    private List<WidgetItem> filterWidgetItems(String query, String packageTitle,
+            List<WidgetItem> items) {
+        StringMatcher matcher = StringMatcher.getInstance();
+        if (matches(query, packageTitle, matcher)) {
+            return items;
+        }
+        return items.stream()
+                .filter(item -> matches(query, item.label, matcher))
+                .collect(Collectors.toList());
+    }
+}
diff --git a/src/com/android/launcher3/widget/picker/search/WidgetsSearchBar.java b/src/com/android/launcher3/widget/picker/search/WidgetsSearchBar.java
new file mode 100644
index 0000000..d8e9733
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/search/WidgetsSearchBar.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2021 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.widget.picker.search;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.EditText;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.R;
+import com.android.launcher3.search.SearchAlgorithm;
+import com.android.launcher3.widget.model.WidgetsListBaseEntry;
+
+import java.util.List;
+
+/**
+ * View for a search bar with an edit text with a cancel button.
+ */
+public class WidgetsSearchBar extends LinearLayout {
+    private WidgetsSearchBarController mController;
+    private EditText mEditText;
+    private ImageButton mCancelButton;
+
+    public WidgetsSearchBar(Context context) {
+        this(context, null, 0);
+    }
+
+    public WidgetsSearchBar(@NonNull Context context,
+            @Nullable AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public WidgetsSearchBar(@NonNull Context context, @Nullable AttributeSet attrs,
+            int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+    }
+
+    /**
+     * Attaches a controller to the search bar which interacts with {@code searchModeListener}.
+     */
+    public void initialize(List<WidgetsListBaseEntry> allWidgets,
+            SearchModeListener searchModeListener) {
+        SearchAlgorithm<WidgetsListBaseEntry> algo =
+                new SimpleWidgetsSearchAlgorithm(new SimpleWidgetsSearchPipeline(allWidgets));
+        mController = new WidgetsSearchBarController(
+                algo, mEditText, mCancelButton, searchModeListener);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mEditText = findViewById(R.id.widgets_search_bar_edit_text);
+        mCancelButton = findViewById(R.id.widgets_search_cancel_button);
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        mController.onDestroy();
+    }
+}
diff --git a/src/com/android/launcher3/widget/picker/search/WidgetsSearchBarController.java b/src/com/android/launcher3/widget/picker/search/WidgetsSearchBarController.java
new file mode 100644
index 0000000..6c37484
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/search/WidgetsSearchBarController.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2021 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.widget.picker.search;
+
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
+
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.util.Log;
+import android.widget.EditText;
+import android.widget.ImageButton;
+
+import com.android.launcher3.search.SearchAlgorithm;
+import com.android.launcher3.search.SearchCallback;
+import com.android.launcher3.widget.model.WidgetsListBaseEntry;
+
+import java.util.ArrayList;
+
+/**
+ * Controller for a search bar with an edit text and a cancel button.
+ */
+public class WidgetsSearchBarController implements TextWatcher,
+        SearchCallback<WidgetsListBaseEntry> {
+    private static final String TAG = "WidgetsSearchBarController";
+    private static final boolean DEBUG = false;
+
+    protected SearchAlgorithm<WidgetsListBaseEntry> mSearchAlgorithm;
+    protected EditText mInput;
+    protected ImageButton mCancelButton;
+    protected SearchModeListener mSearchModeListener;
+    protected String mQuery;
+
+    public WidgetsSearchBarController(
+            SearchAlgorithm<WidgetsListBaseEntry> algo, EditText editText, ImageButton cancelButton,
+            SearchModeListener searchModeListener) {
+        mSearchAlgorithm = algo;
+        mInput = editText;
+        mInput.addTextChangedListener(this);
+        mCancelButton = cancelButton;
+        mCancelButton.setOnClickListener(v -> clearSearchResult());
+        mSearchModeListener = searchModeListener;
+    }
+
+    @Override
+    public void afterTextChanged(final Editable s) {
+        mQuery = s.toString();
+        if (mQuery.isEmpty()) {
+            mSearchAlgorithm.cancel(/* interruptActiveRequests= */ true);
+            mSearchModeListener.exitSearchMode();
+            mCancelButton.setVisibility(GONE);
+        } else {
+            mSearchAlgorithm.cancel(/* interruptActiveRequests= */ false);
+            mSearchModeListener.enterSearchMode();
+            mSearchAlgorithm.doSearch(mQuery, this);
+            mCancelButton.setVisibility(VISIBLE);
+        }
+    }
+
+    @Override
+    public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
+        // Do nothing.
+    }
+
+    @Override
+    public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
+        // Do nothing.
+    }
+
+    @Override
+    public void onSearchResult(String query, ArrayList<WidgetsListBaseEntry> items) {
+        if (DEBUG) {
+            Log.d(TAG, "onSearchResult query: " + query + " items: " + items);
+        }
+        mSearchModeListener.onSearchResults(items);
+    }
+
+    @Override
+    public void onAppendSearchResult(String query, ArrayList<WidgetsListBaseEntry> items) {
+        // Not needed.
+    }
+
+    @Override
+    public void clearSearchResult() {
+        mSearchAlgorithm.cancel(/* interruptActiveRequests= */ true);
+        mInput.getText().clear();
+        mInput.clearFocus();
+        mSearchModeListener.exitSearchMode();
+    }
+
+    /**
+     * Cleans up after search is no longer needed.
+     */
+    public void onDestroy() {
+        mSearchAlgorithm.destroy();
+    }
+}
diff --git a/src_plugins/com/android/systemui/plugins/BcSmartspaceDataPlugin.java b/src_plugins/com/android/systemui/plugins/BcSmartspaceDataPlugin.java
deleted file mode 100644
index f8a9a04..0000000
--- a/src_plugins/com/android/systemui/plugins/BcSmartspaceDataPlugin.java
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * Copyright (C) 2021 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.systemui.plugins;
-
-import android.os.Parcelable;
-
-import com.android.systemui.plugins.annotations.ProvidesInterface;
-
-import java.util.List;
-
-/**
- * Interface to provide SmartspaceTargets to BcSmartspace.
- */
-@ProvidesInterface(action = BcSmartspaceDataPlugin.ACTION, version = BcSmartspaceDataPlugin.VERSION)
-public interface BcSmartspaceDataPlugin extends Plugin {
-    String ACTION = "com.android.systemui.action.PLUGIN_BC_SMARTSPACE_DATA";
-    int VERSION = 1;
-
-    /** Register a listener to get Smartspace data. */
-    void registerListener(SmartspaceTargetListener listener);
-
-    /** Unregister a listener. */
-    void unregisterListener(SmartspaceTargetListener listener);
-
-    /** Provides Smartspace data to registered listeners. */
-    interface SmartspaceTargetListener {
-        /** Each Parcelable is a SmartspaceTarget that represents a card. */
-        void onSmartspaceTargetsUpdated(List<? extends Parcelable> targets);
-    }
-}
diff --git a/tests/AndroidManifest-common.xml b/tests/AndroidManifest-common.xml
index 744dee0..7f6c8f8 100644
--- a/tests/AndroidManifest-common.xml
+++ b/tests/AndroidManifest-common.xml
@@ -60,6 +60,17 @@
                        android:resource="@xml/appwidget_with_config"/>
         </receiver>
 
+        <receiver
+            android:name="com.android.launcher3.testcomponent.AppWidgetDynamicColors"
+            android:exported="true"
+            android:label="Dynamic Colors">
+            <intent-filter>
+                <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
+            </intent-filter>
+            <meta-data android:name="android.appwidget.provider"
+                android:resource="@xml/appwidget_dynamic_colors"/>
+        </receiver>
+
         <activity
             android:name="com.android.launcher3.testcomponent.WidgetConfigActivity"
             android:exported="true">
diff --git a/tests/Launcher3Tests.xml b/tests/Launcher3Tests.xml
new file mode 100644
index 0000000..3fff622
--- /dev/null
+++ b/tests/Launcher3Tests.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 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.
+-->
+<!-- This test config file is auto-generated. -->
+<configuration description="Runs Launcher3 tests.">
+    <option name="test-suite-tag" value="apct" />
+    <option name="test-suite-tag" value="apct-instrumentation" />
+
+    <target_preparer class="com.android.tradefed.targetprep.DeviceSetup">
+        <option name="set-test-harness" value="true" />
+        <option name="run-command" value="am force-stop com.android.launcher3" />
+        <option name="run-command" value="pm uninstall com.google.android.apps.nexuslauncher" />
+        <option name="run-command" value="pm uninstall com.google.android.apps.nexuslauncher.out_of_proc_tests" />
+        <option name="run-command" value="pm uninstall com.google.android.apps.nexuslauncher.tests" />
+        <option name="run-command" value="pm disable com.google.android.googlequicksearchbox" />
+
+        <option name="run-command" value="input keyevent 82" />
+        <option name="run-command" value="settings delete secure assistant" />
+        <option name="run-command" value="settings put global airplane_mode_on 1" />
+        <option name="run-command" value="am broadcast -a android.intent.action.AIRPLANE_MODE" />
+    </target_preparer>
+
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="test-file-name" value="Launcher3Tests.apk" />
+        <option name="test-file-name" value="Launcher3.apk" />
+    </target_preparer>
+
+    <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
+        <option name="directory-keys" value="/data/user/0/com.android.launcher3/files" />
+        <option name="collect-on-run-ended-only" value="true" />
+    </metrics_collector>
+
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="com.android.launcher3.tests" />
+        <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+    </test>
+</configuration>
diff --git a/tests/res/layout/test_layout_appwidget_dynamic_colors.xml b/tests/res/layout/test_layout_appwidget_dynamic_colors.xml
new file mode 100644
index 0000000..c5ab030
--- /dev/null
+++ b/tests/res/layout/test_layout_appwidget_dynamic_colors.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="vertical"
+    android:background="?android:attr/colorBackground"
+    android:padding="8dp"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+    <LinearLayout
+        android:orientation = "horizontal"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content">
+        <TextView
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:text="prim"/>
+        <ImageView
+            android:layout_width="24dp"
+            android:layout_height="24dp"
+            android:background="@android:color/system_primary_500"/>
+    </LinearLayout>
+    <LinearLayout
+        android:orientation = "horizontal"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content">
+        <TextView
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:text="second"/>
+        <ImageView
+            android:layout_width="24dp"
+            android:layout_height="24dp"
+            android:background="@android:color/system_secondary_500"/>
+    </LinearLayout>
+    <LinearLayout
+        android:orientation = "horizontal"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content">
+        <TextView
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:text="neutral"/>
+        <ImageView
+            android:layout_width="24dp"
+            android:layout_height="24dp"
+            android:background="@android:color/system_neutral_500"/>
+    </LinearLayout>
+
+    </LinearLayout>
\ No newline at end of file
diff --git a/tests/res/xml/appwidget_dynamic_colors.xml b/tests/res/xml/appwidget_dynamic_colors.xml
new file mode 100644
index 0000000..f6b9a04
--- /dev/null
+++ b/tests/res/xml/appwidget_dynamic_colors.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<appwidget-provider
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:minWidth="1dp"
+    android:minHeight="1dp"
+    android:updatePeriodMillis="0"
+    android:initialLayout="@layout/test_layout_appwidget_dynamic_colors"
+    android:resizeMode="horizontal|vertical"
+    android:widgetCategory="home_screen">
+</appwidget-provider>
\ No newline at end of file
diff --git a/tests/src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithmTest.java b/tests/src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithmTest.java
deleted file mode 100644
index 39709a9..0000000
--- a/tests/src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithmTest.java
+++ /dev/null
@@ -1,98 +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.allapps.search;
-
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-
-import android.content.ComponentName;
-
-import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
-
-import com.android.launcher3.model.data.AppInfo;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-/**
- * Unit tests for {@link DefaultAppSearchAlgorithm}
- */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class DefaultAppSearchAlgorithmTest {
-    private static final DefaultAppSearchAlgorithm.StringMatcher MATCHER =
-            DefaultAppSearchAlgorithm.StringMatcher.getInstance();
-
-    @Test
-    public void testMatches() {
-        assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("white cow"), "cow", MATCHER));
-        assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("whiteCow"), "cow", MATCHER));
-        assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("whiteCOW"), "cow", MATCHER));
-        assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("whitecowCOW"), "cow", MATCHER));
-        assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("white2cow"), "cow", MATCHER));
-
-        assertFalse(DefaultAppSearchAlgorithm.matches(getInfo("whitecow"), "cow", MATCHER));
-        assertFalse(DefaultAppSearchAlgorithm.matches(getInfo("whitEcow"), "cow", MATCHER));
-
-        assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("whitecowCow"), "cow", MATCHER));
-        assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("whitecow cow"), "cow", MATCHER));
-        assertFalse(DefaultAppSearchAlgorithm.matches(getInfo("whitecowcow"), "cow", MATCHER));
-        assertFalse(DefaultAppSearchAlgorithm.matches(getInfo("whit ecowcow"), "cow", MATCHER));
-
-        assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("cats&dogs"), "dog", MATCHER));
-        assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("cats&Dogs"), "dog", MATCHER));
-        assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("cats&Dogs"), "&", MATCHER));
-
-        assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("2+43"), "43", MATCHER));
-        assertFalse(DefaultAppSearchAlgorithm.matches(getInfo("2+43"), "3", MATCHER));
-
-        assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("Q"), "q", MATCHER));
-        assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("  Q"), "q", MATCHER));
-
-        // match lower case words
-        assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("elephant"), "e", MATCHER));
-
-        assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("电子邮件"), "电", MATCHER));
-        assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("电子邮件"), "电子", MATCHER));
-        assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("电子邮件"), "子", MATCHER));
-        assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("电子邮件"), "邮件", MATCHER));
-
-        assertFalse(DefaultAppSearchAlgorithm.matches(getInfo("Bot"), "ba", MATCHER));
-        assertFalse(DefaultAppSearchAlgorithm.matches(getInfo("bot"), "ba", MATCHER));
-    }
-
-    @Test
-    public void testMatchesVN() {
-        assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("다운로드"), "다", MATCHER));
-        assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("드라이브"), "드", MATCHER));
-        assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("다운로드 드라이브"), "ㄷ", MATCHER));
-        assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("운로 드라이브"), "ㄷ", MATCHER));
-        assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("abc"), "åbç", MATCHER));
-        assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("Alpha"), "ål", MATCHER));
-
-        assertFalse(DefaultAppSearchAlgorithm.matches(getInfo("다운로드 드라이브"), "ㄷㄷ", MATCHER));
-        assertFalse(DefaultAppSearchAlgorithm.matches(getInfo("로드라이브"), "ㄷ", MATCHER));
-        assertFalse(DefaultAppSearchAlgorithm.matches(getInfo("abc"), "åç", MATCHER));
-    }
-
-    private AppInfo getInfo(String title) {
-        AppInfo info = new AppInfo();
-        info.title = title;
-        info.componentName = new ComponentName("Test", title);
-        return info;
-    }
-}
diff --git a/tests/src/com/android/launcher3/search/StringMatcherUtilityTest.java b/tests/src/com/android/launcher3/search/StringMatcherUtilityTest.java
new file mode 100644
index 0000000..413f404
--- /dev/null
+++ b/tests/src/com/android/launcher3/search/StringMatcherUtilityTest.java
@@ -0,0 +1,96 @@
+/*
+ * 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.search;
+
+import static com.android.launcher3.search.StringMatcherUtility.matches;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.launcher3.search.StringMatcherUtility.StringMatcher;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Unit tests for {@link StringMatcherUtility}
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class StringMatcherUtilityTest {
+    private static final StringMatcher MATCHER =
+            StringMatcher.getInstance();
+
+    @Test
+    public void testMatches() {
+        assertTrue(matches("white ", "white cow", MATCHER));
+        assertTrue(matches("white c", "white cow", MATCHER));
+        assertTrue(matches("cow", "white cow", MATCHER));
+        assertTrue(matches("cow", "whiteCow", MATCHER));
+        assertTrue(matches("cow", "whiteCOW", MATCHER));
+        assertTrue(matches("cow", "whitecowCOW", MATCHER));
+        assertTrue(matches("cow", "white2cow", MATCHER));
+
+        assertFalse(matches("cow", "whitecow", MATCHER));
+        assertFalse(matches("cow", "whitEcow", MATCHER));
+
+        assertTrue(matches("cow", "whitecowCow", MATCHER));
+        assertTrue(matches("cow", "whitecow cow", MATCHER));
+        assertFalse(matches("cow", "whitecowcow", MATCHER));
+        assertFalse(matches("cow", "whit ecowcow", MATCHER));
+
+        assertTrue(matches("dog", "cats&dogs", MATCHER));
+        assertTrue(matches("dog", "cats&Dogs", MATCHER));
+        assertTrue(matches("&", "cats&Dogs", MATCHER));
+
+        assertTrue(matches("43", "2+43", MATCHER));
+        assertFalse(matches("3", "2+43", MATCHER));
+
+        assertTrue(matches("q", "Q", MATCHER));
+        assertTrue(matches("q", "  Q", MATCHER));
+
+        // match lower case words
+        assertTrue(matches("e", "elephant", MATCHER));
+        assertTrue(matches("eL", "Elephant", MATCHER));
+
+        assertTrue(matches("电", "电子邮件", MATCHER));
+        assertTrue(matches("电子", "电子邮件", MATCHER));
+        assertTrue(matches("子", "电子邮件", MATCHER));
+        assertTrue(matches("邮件", "电子邮件", MATCHER));
+
+        assertFalse(matches("ba", "Bot", MATCHER));
+        assertFalse(matches("ba", "bot", MATCHER));
+        assertFalse(matches("phant", "elephant", MATCHER));
+        assertFalse(matches("elephants", "elephant", MATCHER));
+    }
+
+    @Test
+    public void testMatchesVN() {
+        assertTrue(matches("다", "다운로드", MATCHER));
+        assertTrue(matches("드", "드라이브", MATCHER));
+        assertTrue(matches("ㄷ", "다운로드 드라이브", MATCHER));
+        assertTrue(matches("ㄷ", "운로 드라이브", MATCHER));
+        assertTrue(matches("åbç", "abc", MATCHER));
+        assertTrue(matches("ål", "Alpha", MATCHER));
+
+        assertFalse(matches("ㄷㄷ", "다운로드 드라이브", MATCHER));
+        assertFalse(matches("ㄷ", "로드라이브", MATCHER));
+        assertFalse(matches("åç", "abc", MATCHER));
+    }
+}
diff --git a/tests/src/com/android/launcher3/testcomponent/AppWidgetDynamicColors.java b/tests/src/com/android/launcher3/testcomponent/AppWidgetDynamicColors.java
new file mode 100644
index 0000000..5fb3454
--- /dev/null
+++ b/tests/src/com/android/launcher3/testcomponent/AppWidgetDynamicColors.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2013 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.testcomponent;
+
+import android.appwidget.AppWidgetProvider;
+
+/**
+ * A simple app widget showing a primary, secondary and neutral color.
+ */
+public class AppWidgetDynamicColors extends AppWidgetProvider {
+}
diff --git a/tests/src/com/android/launcher3/ui/WorkTabTest.java b/tests/src/com/android/launcher3/ui/WorkTabTest.java
index a6c7c03..8f4381b 100644
--- a/tests/src/com/android/launcher3/ui/WorkTabTest.java
+++ b/tests/src/com/android/launcher3/ui/WorkTabTest.java
@@ -18,7 +18,6 @@
 import static com.android.launcher3.LauncherState.ALL_APPS;
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.allapps.AllAppsStore.DEFER_UPDATES_TEST;
-import static com.android.launcher3.tapl.LauncherInstrumentation.LONG_WAIT_TIME_MS;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
@@ -37,6 +36,7 @@
 import com.android.launcher3.allapps.AllAppsPagedView;
 import com.android.launcher3.allapps.WorkModeSwitch;
 import com.android.launcher3.dragndrop.DragLayer;
+import com.android.launcher3.tapl.LauncherInstrumentation;
 import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.views.WorkEduView;
 
@@ -147,7 +147,7 @@
                 return true;
             }
             return false;
-        }, LONG_WAIT_TIME_MS);
+        }, LauncherInstrumentation.WAIT_TIME_MS);
 
         executeOnLauncher(launcher -> launcher.getStateManager().goToState(ALL_APPS));
         WorkEduView workEduView = getEduView();
diff --git a/tests/tapl/com/android/launcher3/tapl/AllApps.java b/tests/tapl/com/android/launcher3/tapl/AllApps.java
index b6c17df..e32250e 100644
--- a/tests/tapl/com/android/launcher3/tapl/AllApps.java
+++ b/tests/tapl/com/android/launcher3/tapl/AllApps.java
@@ -27,7 +27,6 @@
 import androidx.test.uiautomator.Direction;
 import androidx.test.uiautomator.UiObject2;
 
-import com.android.launcher3.ResourceUtils;
 import com.android.launcher3.testing.TestProtocol;
 
 import java.util.stream.Collectors;
@@ -108,26 +107,24 @@
                     "apps_list_view");
             final UiObject2 searchBox = getSearchBox(allAppsContainer);
 
-            int bottomGestureMargin = ResourceUtils.getNavbarSize(
-                    ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE, mLauncher.getResources()) + 1;
-            int deviceHeight = mLauncher.getDevice().getDisplayHeight();
-            int displayBottom = deviceHeight - bottomGestureMargin;
+            int deviceHeight = mLauncher.getRealDisplaySize().y;
+            int bottomGestureStartOnScreen = mLauncher.getBottomGestureStartOnScreen();
             final BySelector appIconSelector = AppIcon.getAppIconSelector(appName, mLauncher);
             if (!hasClickableIcon(allAppsContainer, appListRecycler, appIconSelector,
-                    displayBottom)) {
+                    bottomGestureStartOnScreen)) {
                 scrollBackToBeginning();
                 int attempts = 0;
                 int scroll = getAllAppsScroll();
                 try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer("scrolled")) {
                     while (!hasClickableIcon(allAppsContainer, appListRecycler, appIconSelector,
-                            displayBottom)) {
+                            bottomGestureStartOnScreen)) {
                         mLauncher.scrollToLastVisibleRow(
                                 allAppsContainer,
                                 mLauncher.getObjectsInContainer(allAppsContainer, "icon")
                                         .stream()
                                         .filter(icon ->
-                                                        mLauncher.getVisibleBounds(icon).bottom
-                                                        <= displayBottom)
+                                                mLauncher.getVisibleBounds(icon).top
+                                                        < bottomGestureStartOnScreen)
                                         .collect(Collectors.toList()),
                                 mLauncher.getVisibleBounds(searchBox).bottom
                                         - mLauncher.getVisibleBounds(allAppsContainer).top);
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index f279a82..5138f02 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -155,8 +155,7 @@
     private static final String OVERVIEW_RES_ID = "overview_panel";
     private static final String WIDGETS_RES_ID = "primary_widgets_list_view";
     private static final String CONTEXT_MENU_RES_ID = "deep_shortcuts_container";
-    public static final int WAIT_TIME_MS = 10000;
-    public static final int LONG_WAIT_TIME_MS = 60000;
+    public static final int WAIT_TIME_MS = 60000;
     private static final String SYSTEMUI_PACKAGE = "com.android.systemui";
     private static final String ANDROID_PACKAGE = "android";
 
@@ -656,7 +655,7 @@
         try {
             final AccessibilityEvent event =
                     mInstrumentation.getUiAutomation().executeAndWaitForEvent(
-                            command, eventFilter, LONG_WAIT_TIME_MS);
+                            command, eventFilter, WAIT_TIME_MS);
             assertNotNull("executeAndWaitForEvent returned null (this can't happen)", event);
             final Parcelable parcelableData = event.getParcelableData();
             event.recycle();
@@ -1028,16 +1027,20 @@
                 expectedState);
     }
 
-    int getBottomGestureSize() {
+    private int getBottomGestureSize() {
         return ResourceUtils.getNavbarSize(
                 ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE, getResources()) + 1;
     }
 
     int getBottomGestureMarginInContainer(UiObject2 container) {
-        final int bottomGestureStartOnScreen = getRealDisplaySize().y - getBottomGestureSize();
+        final int bottomGestureStartOnScreen = getBottomGestureStartOnScreen();
         return getVisibleBounds(container).bottom - bottomGestureStartOnScreen;
     }
 
+    int getBottomGestureStartOnScreen() {
+        return getRealDisplaySize().y - getBottomGestureSize();
+    }
+
     void clickLauncherObject(UiObject2 object) {
         expectEvent(TestProtocol.SEQUENCE_MAIN, LauncherInstrumentation.EVENT_TOUCH_DOWN);
         expectEvent(TestProtocol.SEQUENCE_MAIN, LauncherInstrumentation.EVENT_TOUCH_UP);