Merge "desktop-exploded-view: Explode desktops when swiping up from home" into main
diff --git a/Android.bp b/Android.bp
index 1e1e0ad..a4a058f 100644
--- a/Android.bp
+++ b/Android.bp
@@ -455,6 +455,9 @@
         extra_check_modules: ["Launcher3LintChecker"],
         baseline_filename: "lint-baseline.xml",
     },
+    kotlincflags: [
+        "-Xjvm-default=all",
+    ],
 }
 
 // Library with all the dependencies for building quickstep
@@ -519,6 +522,9 @@
     min_sdk_version: "current",
     // TODO(b/319712088): re-enable use_resource_processor
     use_resource_processor: false,
+    kotlincflags: [
+        "-Xjvm-default=all",
+    ],
 }
 
 // Library with all the source code and dependencies for building Quickstep
@@ -552,6 +558,9 @@
     min_sdk_version: "current",
     // TODO(b/319712088): re-enable use_resource_processor
     use_resource_processor: false,
+    kotlincflags: [
+        "-Xjvm-default=all",
+    ],
 }
 
 // Build rule for Quickstep app.
diff --git a/AndroidManifest-common.xml b/AndroidManifest-common.xml
index 80d2eac..fe57da1 100644
--- a/AndroidManifest-common.xml
+++ b/AndroidManifest-common.xml
@@ -136,7 +136,7 @@
         TODO: Add proper permissions
         -->
         <provider
-            android:name="com.android.launcher3.graphics.GridCustomizationsProvider"
+            android:name="com.android.launcher3.graphics.LauncherCustomizationProvider"
             android:authorities="${applicationId}.grid_control"
             android:exported="true" />
 
diff --git a/quickstep/src/com/android/launcher3/WidgetPickerActivity.java b/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
index 1cf7dda..f992913 100644
--- a/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
+++ b/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
@@ -322,13 +322,14 @@
             stringCache.loadStrings(this);
 
             bindStringCache(stringCache);
-            bindWidgets(mModel.getWidgetsByPackageItem(), mModel.getDefaultWidgetsFilter());
+            bindWidgets(mModel.getWidgetsByPackageItemForPicker(),
+                    mModel.getDefaultWidgetsFilter());
             // Open sheet once widgets are available, so that it doesn't interrupt the open
             // animation.
             openWidgetsSheet();
             if (mUiSurface != null) {
                 mWidgetPredictionsRequester = new WidgetPredictionsRequester(app.getContext(),
-                        mUiSurface, mModel.getWidgetsByComponentKey());
+                        mUiSurface, mModel.getWidgetsByComponentKeyForPicker());
                 mWidgetPredictionsRequester.request(mAddedWidgets, this::bindRecommendedWidgets);
             }
         });
diff --git a/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchAnimatorHelper.kt b/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchAnimatorHelper.kt
index 533a86a..688018b 100644
--- a/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchAnimatorHelper.kt
+++ b/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchAnimatorHelper.kt
@@ -186,10 +186,12 @@
         onAnimFinish: (Animator) -> Unit,
     ): Animator {
         return MinimizeAnimator.create(
-            context.resources.displayMetrics,
+            context,
             change,
             transaction,
             onAnimFinish,
+            interactionJankMonitor,
+            context.mainThreadHandler,
         )
     }
 
diff --git a/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java b/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java
index 40e1c10..9626a61 100644
--- a/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java
+++ b/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java
@@ -79,7 +79,7 @@
         // Widgets (excluding shortcuts & already added widgets) that belong to apps eligible for
         // being in predictions.
         Map<ComponentKey, WidgetItem> allEligibleWidgets =
-                dataModel.widgetsModel.getWidgetsByComponentKey()
+                dataModel.widgetsModel.getWidgetsByComponentKeyForPicker()
                         .entrySet()
                         .stream()
                         .filter(entry -> entry.getValue().widgetInfo != null
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index 2eb4412..2e42e45 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -47,7 +47,6 @@
 import static com.android.quickstep.util.AnimUtils.completeRunnableListCallback;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING;
-import static com.android.window.flags.Flags.enableStartLaunchTransitionFromTaskbarBugfix;
 import static com.android.wm.shell.Flags.enableBubbleBar;
 import static com.android.wm.shell.Flags.enableBubbleBarOnPhones;
 import static com.android.wm.shell.Flags.enableTinyTaskbar;
@@ -1727,7 +1726,7 @@
         }
         // There is no task associated with this launch - launch a new task through an intent
         ActivityOptionsWrapper opts = getActivityLaunchDesktopOptions();
-        if (enableStartLaunchTransitionFromTaskbarBugfix()) {
+        if (DesktopModeFlags.ENABLE_START_LAUNCH_TRANSITION_FROM_TASKBAR_BUGFIX.isTrue()) {
             mSysUiProxy.startLaunchIntentTransition(intent, opts.options.toBundle(), displayId);
         } else {
             startActivity(intent, opts.options.toBundle());
diff --git a/quickstep/src/com/android/launcher3/uioverrides/flags/DevOptionsUiHelper.kt b/quickstep/src/com/android/launcher3/uioverrides/flags/DevOptionsUiHelper.kt
index 7c09e9a..0d2cfbf 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/flags/DevOptionsUiHelper.kt
+++ b/quickstep/src/com/android/launcher3/uioverrides/flags/DevOptionsUiHelper.kt
@@ -317,6 +317,12 @@
             )
             addPreference(
                 Preference(context).apply {
+                    title = "Launch Full Gesture Tutorial"
+                    intent = Intent(launchSandboxIntent).putExtra("use_tutorial_menu", false)
+                }
+            )
+            addPreference(
+                Preference(context).apply {
                     title = "Launch Back Tutorial"
                     intent =
                         Intent(launchSandboxIntent)
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
index 05d12c3..58b274a 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
@@ -64,6 +64,7 @@
 import android.graphics.PointF;
 import android.view.MotionEvent;
 import android.view.animation.Interpolator;
+import android.window.DesktopModeFlags;
 
 import com.android.internal.jank.Cuj;
 import com.android.launcher3.LauncherState;
@@ -86,7 +87,6 @@
 import com.android.quickstep.util.WorkspaceRevealAnim;
 import com.android.quickstep.views.RecentsView;
 import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
-import com.android.window.flags.Flags;
 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
 
 /**
@@ -188,7 +188,7 @@
         }
         if (DesktopModeStatus.canEnterDesktopMode(mLauncher)
                 //TODO(b/345296916): replace with dev option once in teamfood
-                && Flags.enableQuickswitchDesktopSplitBugfix()
+                && DesktopModeFlags.ENABLE_QUICKSWITCH_DESKTOP_SPLIT_BUGFIX.isTrue()
                 && mRecentsView.getNonDesktopTaskViewCount() < 1) {
             return false;
         }
diff --git a/quickstep/src/com/android/quickstep/TaskAnimationManager.java b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
index 9810308..cb11afa 100644
--- a/quickstep/src/com/android/quickstep/TaskAnimationManager.java
+++ b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
@@ -286,8 +286,32 @@
         mCallbacks.addListener(listener);
 
         final ActivityOptions options = ActivityOptions.makeBasic();
+        options.setPendingIntentBackgroundActivityStartMode(
+                ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS);
+        options.setTransientLaunch();
+        options.setSourceInfo(ActivityOptions.SourceInfo.TYPE_RECENTS_ANIMATION, eventTime);
 
-        // TODO:(b/365777482) if flag is enabled, but on launcher it will crash.
+        // Notify taskbar that we should skip reacting to launcher visibility change to
+        // avoid a jumping taskbar.
+        TaskbarUIController taskbarUIController = containerInterface.getTaskbarController();
+        if (enableScalingRevealHomeAnimation() && taskbarUIController != null) {
+            taskbarUIController.setSkipLauncherVisibilityChange(true);
+
+            mCallbacks.addListener(new RecentsAnimationCallbacks.RecentsAnimationListener() {
+                @Override
+                public void onRecentsAnimationCanceled(
+                        @NonNull HashMap<Integer, ThumbnailData> thumbnailDatas) {
+                    taskbarUIController.setSkipLauncherVisibilityChange(false);
+                }
+
+                @Override
+                public void onRecentsAnimationFinished(
+                        @NonNull RecentsAnimationController controller) {
+                    taskbarUIController.setSkipLauncherVisibilityChange(false);
+                }
+            });
+        }
+
         if(containerInterface.getCreatedContainer() instanceof RecentsWindowManager
                 && (Flags.enableFallbackOverviewInWindow()
                         || Flags.enableLauncherOverviewInWindow())) {
@@ -297,32 +321,6 @@
                     .getRecentsWindowManager(mDeviceState.getDisplayId())
                     .startRecentsWindow(mCallbacks);
         } else {
-            options.setPendingIntentBackgroundActivityStartMode(
-                    ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS);
-            options.setTransientLaunch();
-            options.setSourceInfo(ActivityOptions.SourceInfo.TYPE_RECENTS_ANIMATION, eventTime);
-
-            // Notify taskbar that we should skip reacting to launcher visibility change to
-            // avoid a jumping taskbar.
-            TaskbarUIController taskbarUIController = containerInterface.getTaskbarController();
-            if (enableScalingRevealHomeAnimation() && taskbarUIController != null) {
-                taskbarUIController.setSkipLauncherVisibilityChange(true);
-
-                mCallbacks.addListener(new RecentsAnimationCallbacks.RecentsAnimationListener() {
-                    @Override
-                    public void onRecentsAnimationCanceled(
-                            @NonNull HashMap<Integer, ThumbnailData> thumbnailDatas) {
-                        taskbarUIController.setSkipLauncherVisibilityChange(false);
-                    }
-
-                    @Override
-                    public void onRecentsAnimationFinished(
-                            @NonNull RecentsAnimationController controller) {
-                        taskbarUIController.setSkipLauncherVisibilityChange(false);
-                    }
-                });
-            }
-
             mRecentsAnimationStartPending = getSystemUiProxy().startRecentsActivity(intent,
                     options, mCallbacks, false /* useSyntheticRecentsTransition */);
         }
diff --git a/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowContext.kt b/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowContext.kt
index 047658c..cd48136 100644
--- a/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowContext.kt
+++ b/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowContext.kt
@@ -61,9 +61,13 @@
         return dragLayer
     }
 
+    fun initDeviceProfile() {
+        deviceProfile = InvariantDeviceProfile.INSTANCE[this].getDeviceProfile(this)
+    }
+
     override fun getDeviceProfile(): DeviceProfile {
         if (deviceProfile == null) {
-            deviceProfile = InvariantDeviceProfile.INSTANCE[this].getDeviceProfile(this).copy(this)
+            initDeviceProfile()
         }
         return deviceProfile!!
     }
@@ -79,7 +83,10 @@
      * @param type The window type to pass to the created WindowManager.LayoutParams.
      * @param title The window title to pass to the created WindowManager.LayoutParams.
      */
-    fun createDefaultWindowLayoutParams(type: Int, title: String): WindowManager.LayoutParams {
+    private fun createDefaultWindowLayoutParams(
+        type: Int,
+        title: String,
+    ): WindowManager.LayoutParams {
         var windowFlags =
             (WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS or
                 WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS or
diff --git a/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowManager.kt b/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowManager.kt
index 07288d8..1f4961a 100644
--- a/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowManager.kt
+++ b/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowManager.kt
@@ -21,6 +21,7 @@
 import android.content.ComponentName
 import android.content.Context
 import android.content.LocusId
+import android.content.res.Configuration
 import android.os.Bundle
 import android.view.KeyEvent
 import android.view.LayoutInflater
@@ -32,6 +33,7 @@
 import android.view.WindowManager
 import android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
 import android.window.RemoteTransition
+import com.android.launcher3.AbstractFloatingView
 import com.android.launcher3.BaseActivity
 import com.android.launcher3.LauncherAnimationRunner
 import com.android.launcher3.LauncherAnimationRunner.RemoteAnimationFactory
@@ -135,73 +137,6 @@
         listOf(RunnableList(), RunnableList(), RunnableList(), RunnableList())
     private var onInitListener: Predicate<Boolean>? = null
 
-    private val taskStackChangeListener =
-        object : TaskStackChangeListener {
-            override fun onTaskMovedToFront(taskId: Int) {
-                if ((isShowing() && isInState(DEFAULT))) {
-                    // handling state where we end recents animation by swiping livetile away
-                    // TODO: animate this switch.
-                    cleanupRecentsWindow()
-                }
-            }
-        }
-
-    private val recentsAnimationListener =
-        object : RecentsAnimationListener {
-            override fun onRecentsAnimationCanceled(thumbnailDatas: HashMap<Int, ThumbnailData>) {
-                recentAnimationStopped()
-            }
-
-            override fun onRecentsAnimationFinished(controller: RecentsAnimationController) {
-                recentAnimationStopped()
-            }
-        }
-
-    init {
-        TaskStackChangeListeners.getInstance().registerTaskStackListener(taskStackChangeListener)
-    }
-
-    override fun destroy() {
-        super.destroy()
-        cleanupRecentsWindow()
-        TaskStackChangeListeners.getInstance().unregisterTaskStackListener(taskStackChangeListener)
-        callbacks?.removeListener(recentsAnimationListener)
-        recentsWindowTracker.onContextDestroyed(this)
-        recentsView?.destroy()
-    }
-
-    override fun startHome() {
-        startHome(/* finishRecentsAnimation= */ true)
-    }
-
-    fun startHome(finishRecentsAnimation: Boolean) {
-        val recentsView: RecentsView<*, *> = getOverviewPanel()
-
-        if (!finishRecentsAnimation) {
-            recentsView.switchToScreenshot(/* onFinishRunnable= */ null)
-            startHomeInternal()
-            return
-        }
-        recentsView.switchToScreenshot {
-            recentsView.finishRecentsAnimation(/* toRecents= */ true) { startHomeInternal() }
-        }
-    }
-
-    private fun startHomeInternal() {
-        val runner = LauncherAnimationRunner(mainThreadHandler, animationToHomeFactory, true)
-        val options =
-            ActivityOptions.makeRemoteAnimation(
-                RemoteAnimationAdapter(runner, HOME_APPEAR_DURATION, 0),
-                RemoteTransition(
-                    runner.toRemoteTransition(),
-                    iApplicationThread,
-                    "StartHomeFromRecents",
-                ),
-            )
-        OverviewComponentObserver.startHomeIntentSafely(this, options.toBundle(), TAG)
-        stateManager.moveToRestState()
-    }
-
     private val animationToHomeFactory =
         RemoteAnimationFactory {
             _: Int,
@@ -229,7 +164,7 @@
                 anim,
                 this@RecentsWindowManager,
                 {
-                    getStateManager().goToState(BG_LAUNCHER, false)
+                    getStateManager().goToState(BG_LAUNCHER, true)
                     cleanupRecentsWindow()
                 },
                 true, /* skipFirstFrame */
@@ -242,17 +177,49 @@
         TestLogging.recordEvent(SEQUENCE_MAIN, "onBackInvoked")
     }
 
-    private fun cleanupRecentsWindow() {
-        RecentsWindowProtoLogProxy.logCleanup(isShowing())
-        if (isShowing()) {
-            windowManager.removeViewImmediate(windowView)
+    private val taskStackChangeListener =
+        object : TaskStackChangeListener {
+            override fun onTaskMovedToFront(taskId: Int) {
+                if ((isShowing() && isInState(DEFAULT))) {
+                    // handling state where we end recents animation by swiping livetile away
+                    // TODO: animate this switch.
+                    cleanupRecentsWindow()
+                }
+            }
         }
-        stateManager.moveToRestState()
-        callbacks?.removeListener(recentsAnimationListener)
+
+    private val recentsAnimationListener =
+        object : RecentsAnimationListener {
+            override fun onRecentsAnimationCanceled(thumbnailDatas: HashMap<Int, ThumbnailData>) {
+                recentAnimationStopped()
+            }
+
+            override fun onRecentsAnimationFinished(controller: RecentsAnimationController) {
+                recentAnimationStopped()
+            }
+        }
+
+    init {
+        TaskStackChangeListeners.getInstance().registerTaskStackListener(taskStackChangeListener)
     }
 
-    private fun isShowing(): Boolean {
-        return windowView?.parent != null
+    override fun handleConfigurationChanged(configuration: Configuration?) {
+        initDeviceProfile()
+        AbstractFloatingView.closeOpenViews(
+            this,
+            true,
+            AbstractFloatingView.TYPE_ALL and AbstractFloatingView.TYPE_REBIND_SAFE.inv(),
+        )
+        dispatchDeviceProfileChanged()
+    }
+
+    override fun destroy() {
+        super.destroy()
+        cleanupRecentsWindow()
+        TaskStackChangeListeners.getInstance().unregisterTaskStackListener(taskStackChangeListener)
+        callbacks?.removeListener(recentsAnimationListener)
+        recentsWindowTracker.onContextDestroyed(this)
+        recentsView?.destroy()
     }
 
     fun startRecentsWindow(callbacks: RecentsAnimationCallbacks? = null) {
@@ -301,6 +268,51 @@
         callbacks?.addListener(recentsAnimationListener)
     }
 
+    override fun startHome() {
+        startHome(/* finishRecentsAnimation= */ true)
+    }
+
+    fun startHome(finishRecentsAnimation: Boolean) {
+        val recentsView: RecentsView<*, *> = getOverviewPanel()
+
+        if (!finishRecentsAnimation) {
+            recentsView.switchToScreenshot /* onFinishRunnable= */ {}
+            startHomeInternal()
+            return
+        }
+        recentsView.switchToScreenshot {
+            recentsView.finishRecentsAnimation(/* toRecents= */ true) { startHomeInternal() }
+        }
+    }
+
+    private fun startHomeInternal() {
+        val runner = LauncherAnimationRunner(mainThreadHandler, animationToHomeFactory, true)
+        val options =
+            ActivityOptions.makeRemoteAnimation(
+                RemoteAnimationAdapter(runner, HOME_APPEAR_DURATION, 0),
+                RemoteTransition(
+                    runner.toRemoteTransition(),
+                    iApplicationThread,
+                    "StartHomeFromRecents",
+                ),
+            )
+        OverviewComponentObserver.startHomeIntentSafely(this, options.toBundle(), TAG)
+        stateManager.moveToRestState()
+    }
+
+    private fun cleanupRecentsWindow() {
+        RecentsWindowProtoLogProxy.logCleanup(isShowing())
+        if (isShowing()) {
+            windowManager.removeViewImmediate(windowView)
+        }
+        stateManager.moveToRestState()
+        callbacks?.removeListener(recentsAnimationListener)
+    }
+
+    private fun isShowing(): Boolean {
+        return windowView?.parent != null
+    }
+
     private fun recentAnimationStopped() {
         if (isInState(BACKGROUND_APP)) {
             cleanupRecentsWindow()
@@ -352,7 +364,6 @@
     override fun onStateSetEnd(state: RecentsState) {
         super.onStateSetEnd(state)
         RecentsWindowProtoLogProxy.logOnStateSetEnd(getStateName(state))
-
         if (state == HOME || state == BG_LAUNCHER) {
             cleanupRecentsWindow()
         }
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java
index afe988d..a703c23 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java
@@ -19,6 +19,7 @@
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_DEEP_PRESS_STASHED_TASKBAR;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LONG_PRESS_NAVBAR;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LONG_PRESS_STASHED_TASKBAR;
+import static com.android.launcher3.logging.StatsLogManager.LauncherLatencyEvent.LAUNCHER_LATENCY_CONTEXTUAL_SEARCH_LPNH_ABANDON;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.LogConfig.NAV_HANDLE_LONG_PRESS;
 
@@ -30,6 +31,7 @@
 import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.Utilities;
+import com.android.launcher3.logging.InstanceIdSequence;
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.util.DisplayController;
 import com.android.quickstep.DeviceConfigWrapper;
@@ -49,6 +51,8 @@
     private static final String TAG = "NavHandleLongPressIC";
     private static final boolean DEBUG_NAV_HANDLE = Utilities.isPropertyEnabled(
             NAV_HANDLE_LONG_PRESS);
+    // Minimum time between touch down and abandon to log.
+    @VisibleForTesting static final long MIN_TIME_TO_LOG_ABANDON_MS = 200;
 
     private NavHandleLongPressHandler mNavHandleLongPressHandler;
     private final float mNavHandleWidth;
@@ -62,11 +66,12 @@
     private final int mOuterLongPressTimeout;
     private final boolean mDeepPressEnabled;
     private final NavHandle mNavHandle;
-    private final StatsLogManager mStatsLogManager;
+    private StatsLogManager mStatsLogManager;
     private final TopTaskTracker mTopTaskTracker;
     private final GestureState mGestureState;
 
-    private MotionEvent mCurrentDownEvent;
+    private MotionEvent mCurrentDownEvent;  // Down event that started the current gesture.
+    private MotionEvent mCurrentMotionEvent;  // Most recent motion event.
     private boolean mDeepPressLogged;  // Whether deep press has been logged for the current touch.
 
     public NavHandleLongPressInputConsumer(Context context, InputConsumer delegate,
@@ -125,6 +130,10 @@
 
     @Override
     public void onMotionEvent(MotionEvent ev) {
+        if (mCurrentMotionEvent != null) {
+            mCurrentMotionEvent.recycle();
+        }
+        mCurrentMotionEvent = MotionEvent.obtain(ev);
         if (mDelegate.allowInterceptByParent()) {
             handleMotionEvent(ev);
         } else if (MAIN_EXECUTOR.getHandler().hasCallbacks(mTriggerLongPress)) {
@@ -244,6 +253,15 @@
         if (DEBUG_NAV_HANDLE) {
             Log.d(TAG, "cancelLongPress: " + reason);
         }
+        // Log LPNH abandon latency if we didn't trigger but were still prepared to.
+        long latencyMs = mCurrentMotionEvent.getEventTime() - mCurrentDownEvent.getEventTime();
+        if (mState != STATE_ACTIVE && MAIN_EXECUTOR.getHandler().hasCallbacks(mTriggerLongPress)
+                && latencyMs >= MIN_TIME_TO_LOG_ABANDON_MS) {
+            mStatsLogManager.latencyLogger()
+                    .withInstanceId(new InstanceIdSequence().newInstanceId())
+                    .withLatency(latencyMs)
+                    .log(LAUNCHER_LATENCY_CONTEXTUAL_SEARCH_LPNH_ABANDON);
+        }
         mGestureState.setIsInExtendedSlopRegion(false);
         MAIN_EXECUTOR.getHandler().removeCallbacks(mTriggerLongPress);
         mNavHandleLongPressHandler.onTouchFinished(mNavHandle, reason);
@@ -274,4 +292,9 @@
     void setNavHandleLongPressHandler(NavHandleLongPressHandler navHandleLongPressHandler) {
         mNavHandleLongPressHandler = navHandleLongPressHandler;
     }
+
+    @VisibleForTesting
+    void setStatsLogManager(StatsLogManager statsLogManager) {
+        mStatsLogManager = statsLogManager;
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/util/AppPairsController.java b/quickstep/src/com/android/quickstep/util/AppPairsController.java
index f20d7a5..8385485 100644
--- a/quickstep/src/com/android/quickstep/util/AppPairsController.java
+++ b/quickstep/src/com/android/quickstep/util/AppPairsController.java
@@ -26,6 +26,7 @@
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT;
 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT;
+import static com.android.systemui.shared.recents.utilities.Utilities.isFreeformTask;
 import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50;
 import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_NONE;
 import static com.android.wm.shell.shared.split.SplitScreenConstants.getIndex;
@@ -69,6 +70,7 @@
 import com.android.quickstep.views.TaskView;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
 import com.android.wm.shell.shared.split.SplitScreenConstants.PersistentSnapPosition;
 
 import java.util.Arrays;
@@ -337,10 +339,12 @@
      *   c) App B is on-screen, but App A isn't.
      *   d) Neither is on-screen.
      *
-     * If the user tapped an app pair while inside a single app, there are 3 cases:
-     *   a) The on-screen app is App A of the app pair.
-     *   b) The on-screen app is App B of the app pair.
-     *   c) It is neither.
+     * If the user tapped an app pair while a fullscreen or freeform app is visible on screen,
+     * there are 4 cases:
+     *   a) At least one of the apps in the app pair is in freeform windowing mode.
+     *   b) The on-screen app is App A of the app pair.
+     *   c) The on-screen app is App B of the app pair.
+     *   d) It is neither.
      *
      * For each case, we call the appropriate animation and split launch type.
      */
@@ -422,6 +426,14 @@
                     foundTasks -> {
                         Task foundTask1 = foundTasks[0];
                         Task foundTask2 = foundTasks[1];
+
+                        if (DesktopModeStatus.canEnterDesktopMode(context) && (isFreeformTask(
+                                foundTask1) || isFreeformTask(foundTask2))) {
+                            launchAppPair(launchingIconView,
+                                    CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_TASKBAR);
+                            return;
+                        }
+
                         boolean task1IsOnScreen;
                         boolean task2IsOnScreen;
                         if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
diff --git a/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java b/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java
index 7d5b471..d2f9652 100644
--- a/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java
+++ b/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java
@@ -18,6 +18,7 @@
 
 import android.animation.Animator;
 import android.animation.RectEvaluator;
+import android.app.PictureInPictureParams;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.pm.ActivityInfo;
@@ -25,6 +26,7 @@
 import android.graphics.Rect;
 import android.graphics.RectF;
 import android.util.Log;
+import android.util.Rational;
 import android.view.Surface;
 import android.view.SurfaceControl;
 import android.view.View;
@@ -50,8 +52,6 @@
 
     private static final float END_PROGRESS = 1.0f;
 
-    private static final float PIP_ASPECT_RATIO_MISMATCH_THRESHOLD = 0.01f;
-
     private final int mTaskId;
     private final ActivityInfo mActivityInfo;
     private final SurfaceControl mLeash;
@@ -158,9 +158,8 @@
             // not a valid rectangle to use for cropping app surface
             reasonForCreateOverlay = "Source rect hint exceeds display bounds " + sourceRectHint;
             sourceRectHint.setEmpty();
-        } else if (Math.abs(
-                aspectRatio - (sourceRectHint.width() / (float) sourceRectHint.height()))
-                > PIP_ASPECT_RATIO_MISMATCH_THRESHOLD) {
+        } else if (!PictureInPictureParams.isSameAspectRatio(sourceRectHint,
+                new Rational(destinationBounds.width(), destinationBounds.height()))) {
             // The source rect hint does not aspect ratio
             reasonForCreateOverlay = "Source rect hint does not match aspect ratio "
                     + sourceRectHint + " aspect ratio " + aspectRatio;
diff --git a/quickstep/src/com/android/quickstep/views/IconView.kt b/quickstep/src/com/android/quickstep/views/IconView.kt
index 6da52d6..cb69b22 100644
--- a/quickstep/src/com/android/quickstep/views/IconView.kt
+++ b/quickstep/src/com/android/quickstep/views/IconView.kt
@@ -78,7 +78,8 @@
     override fun setDrawable(d: Drawable?) {
         drawable?.callback = null
 
-        drawable = d
+        // Copy drawable so that mutations below do not affect other users of the drawable
+        drawable = d?.constantState?.newDrawable()?.mutate()
         drawable?.let {
             it.callback = this
             setDrawableSizeInternal(width, height)
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 32854a7..9e53f11 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -38,6 +38,7 @@
 import static com.android.launcher3.Flags.enableAdditionalHomeAnimations;
 import static com.android.launcher3.Flags.enableDesktopExplodedView;
 import static com.android.launcher3.Flags.enableDesktopTaskAlphaAnimation;
+import static com.android.launcher3.Flags.enableExpressiveDismissTaskMotion;
 import static com.android.launcher3.Flags.enableGridOnlyOverview;
 import static com.android.launcher3.Flags.enableLargeDesktopWindowingTile;
 import static com.android.launcher3.Flags.enableRefactorTaskThumbnail;
@@ -3857,7 +3858,8 @@
                 newClearAllShortTotalWidthTranslation = expectedFirstTaskStart - firstTaskStart;
             }
         }
-        if (lastGridTaskView != null && lastGridTaskView.isVisibleToUser()) {
+        if (lastGridTaskView != null && (lastGridTaskView.isVisibleToUser() || (
+                enableExpressiveDismissTaskMotion() && lastGridTaskView == dismissedTaskView))) {
             // After dismissal, animate translation of the remaining tasks to fill any gap left
             // between the end of the grid and the clear all button. Only animate if the clear
             // all button is visible or would become visible after dismissal.
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.kt b/quickstep/src/com/android/quickstep/views/TaskView.kt
index 49ec31d..ba54232 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskView.kt
@@ -789,11 +789,11 @@
 
     private fun updateThumbnailValidity(container: TaskContainer) {
         container.isThumbnailValid =
-            viewModel!!.isThumbnailValid(
+            viewModel?.isThumbnailValid(
                 thumbnail = container.thumbnailData,
                 width = container.thumbnailView.width,
                 height = container.thumbnailView.height,
-            )
+            ) ?: return
         applyThumbnailSplashAlpha()
     }
 
@@ -810,7 +810,8 @@
      */
     private fun updateThumbnailMatrix(container: TaskContainer, width: Int, height: Int) {
         val thumbnailPosition =
-            viewModel!!.getThumbnailPosition(container.thumbnailData, width, height, isLayoutRtl)
+            viewModel?.getThumbnailPosition(container.thumbnailData, width, height, isLayoutRtl)
+                ?: return
         container.updateThumbnailMatrix(thumbnailPosition.matrix)
     }
 
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumerTest.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumerTest.java
index 7776351..de6920b 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumerTest.java
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumerTest.java
@@ -24,6 +24,8 @@
 
 import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
 
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LONG_PRESS_NAVBAR;
+import static com.android.launcher3.logging.StatsLogManager.LauncherLatencyEvent.LAUNCHER_LATENCY_CONTEXTUAL_SEARCH_LPNH_ABANDON;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.quickstep.DeviceConfigWrapper.DEFAULT_LPNH_TIMEOUT_MS;
 
@@ -33,9 +35,11 @@
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.when;
 
 import android.os.SystemClock;
@@ -47,6 +51,7 @@
 
 import com.android.launcher3.dagger.LauncherAppComponent;
 import com.android.launcher3.dagger.LauncherAppSingleton;
+import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.util.AllModulesForTest;
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.MainThreadInitializedObject.SandboxContext;
@@ -83,6 +88,7 @@
     private NavHandleLongPressInputConsumer mUnderTest;
     private SandboxContext mContext;
     private float mScreenWidth;
+    private long mDownTimeMs;
     @Mock InputConsumer mDelegate;
     @Mock InputMonitorCompat mInputMonitor;
     @Mock RecentsAnimationDeviceState mDeviceState;
@@ -91,6 +97,9 @@
     @Mock NavHandleLongPressHandler mNavHandleLongPressHandler;
     @Mock TopTaskTracker mTopTaskTracker;
     @Mock TopTaskTracker.CachedTaskInfo mTaskInfo;
+    @Mock StatsLogManager mStatsLogManager;
+    @Mock StatsLogManager.StatsLogger mStatsLogger;
+    @Mock StatsLogManager.StatsLatencyLogger mStatsLatencyLogger;
 
     @Before
     public void setup() {
@@ -100,6 +109,11 @@
         when(mDelegate.allowInterceptByParent()).thenReturn(true);
         mLongPressTriggered.set(false);
         when(mNavHandleLongPressHandler.getLongPressRunnable(any())).thenReturn(mLongPressRunnable);
+        when(mStatsLogger.withPackageName(any())).thenReturn(mStatsLogger);
+        when(mStatsLatencyLogger.withInstanceId(any())).thenReturn(mStatsLatencyLogger);
+        when(mStatsLatencyLogger.withLatency(anyLong())).thenReturn(mStatsLatencyLogger);
+        when(mStatsLogManager.logger()).thenReturn(mStatsLogger);
+        when(mStatsLogManager.latencyLogger()).thenReturn(mStatsLatencyLogger);
         initializeObjectUnderTest();
     }
 
@@ -124,17 +138,24 @@
         assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE);
         verify(mNavHandleLongPressHandler, never()).onTouchStarted(any());
         verify(mNavHandleLongPressHandler, never()).onTouchFinished(any(), any());
+        verifyNoMoreInteractions(mStatsLogManager);
+        verifyNoMoreInteractions(mStatsLogger);
+        verifyNoMoreInteractions(mStatsLatencyLogger);
     }
 
     @Test
     public void testDelegateDisallowsTouchInterceptAfterTouchDown() {
+        // Touch down and wait the minimum abandonment time.
         mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN));
+        SystemClock.sleep(NavHandleLongPressInputConsumer.MIN_TIME_TO_LOG_ABANDON_MS);
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
 
         // Delegate should still get touches unless long press is triggered.
         verify(mDelegate).onMotionEvent(any());
         verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any());
         verify(mNavHandleLongPressHandler, never()).onTouchFinished(any(), any());
 
+        // Child delegate blocks us from intercepting further motion events.
         when(mDelegate.allowInterceptByParent()).thenReturn(false);
         mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_MOVE));
 
@@ -144,6 +165,9 @@
         assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE);
         verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any());
         verify(mNavHandleLongPressHandler, times(1)).onTouchFinished(any(), any());
+        verifyNoMoreInteractions(mStatsLogger);
+        // Because we handled touch down before the child blocked additional events, log abandon.
+        verify(mStatsLatencyLogger).log(LAUNCHER_LATENCY_CONTEXTUAL_SEARCH_LPNH_ABANDON);
     }
 
     @Test
@@ -156,6 +180,12 @@
         assertTrue(mLongPressTriggered.get());
         verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any());
         verify(mNavHandleLongPressHandler, never()).onTouchFinished(any(), any());
+        verify(mStatsLogger).log(LAUNCHER_LONG_PRESS_NAVBAR);
+        verifyNoMoreInteractions(mStatsLatencyLogger);
+
+        // Ensure abandon latency is still not logged after long press.
+        mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_UP));
+        verifyNoMoreInteractions(mStatsLatencyLogger);
     }
 
     @Test
@@ -170,6 +200,8 @@
         assertTrue(mLongPressTriggered.get());
         verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any());
         verify(mNavHandleLongPressHandler, never()).onTouchFinished(any(), any());
+        verify(mStatsLogger).log(LAUNCHER_LONG_PRESS_NAVBAR);
+        verifyNoMoreInteractions(mStatsLatencyLogger);
     }
 
     @Test
@@ -184,6 +216,8 @@
         assertTrue(mLongPressTriggered.get());
         verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any());
         verify(mNavHandleLongPressHandler, never()).onTouchFinished(any(), any());
+        verify(mStatsLogger).log(LAUNCHER_LONG_PRESS_NAVBAR);
+        verifyNoMoreInteractions(mStatsLatencyLogger);
     }
 
     @Test
@@ -215,6 +249,8 @@
             assertTrue(mLongPressTriggered.get());
             verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any());
             verify(mNavHandleLongPressHandler, never()).onTouchFinished(any(), any());
+            verify(mStatsLogger).log(LAUNCHER_LONG_PRESS_NAVBAR);
+            verifyNoMoreInteractions(mStatsLatencyLogger);
         } catch (Exception e) {
             throw new RuntimeException(e);
         }
@@ -235,6 +271,8 @@
             assertTrue(mLongPressTriggered.get());
             verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any());
             verify(mNavHandleLongPressHandler, never()).onTouchFinished(any(), any());
+            verify(mStatsLogger).log(LAUNCHER_LONG_PRESS_NAVBAR);
+            verifyNoMoreInteractions(mStatsLatencyLogger);
         } catch (Exception e) {
             throw new RuntimeException(e);
         }
@@ -258,6 +296,8 @@
         assertFalse(mLongPressTriggered.get());
         verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any());
         verify(mNavHandleLongPressHandler, times(1)).onTouchFinished(any(), any());
+        verifyNoMoreInteractions(mStatsLogger);
+        verify(mStatsLatencyLogger).log(LAUNCHER_LATENCY_CONTEXTUAL_SEARCH_LPNH_ABANDON);
     }
 
     @Test
@@ -278,6 +318,8 @@
         assertFalse(mLongPressTriggered.get());
         verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any());
         verify(mNavHandleLongPressHandler, times(1)).onTouchFinished(any(), any());
+        verifyNoMoreInteractions(mStatsLogger);
+        verify(mStatsLatencyLogger).log(LAUNCHER_LATENCY_CONTEXTUAL_SEARCH_LPNH_ABANDON);
     }
 
     @Test
@@ -299,6 +341,8 @@
         assertFalse(mLongPressTriggered.get());
         verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any());
         verify(mNavHandleLongPressHandler, times(1)).onTouchFinished(any(), any());
+        verifyNoMoreInteractions(mStatsLogger);
+        verify(mStatsLatencyLogger).log(LAUNCHER_LATENCY_CONTEXTUAL_SEARCH_LPNH_ABANDON);
     }
 
     @Test
@@ -320,6 +364,8 @@
         assertFalse(mLongPressTriggered.get());
         verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any());
         verify(mNavHandleLongPressHandler, times(1)).onTouchFinished(any(), any());
+        verifyNoMoreInteractions(mStatsLogger);
+        verify(mStatsLatencyLogger).log(LAUNCHER_LATENCY_CONTEXTUAL_SEARCH_LPNH_ABANDON);
     }
 
     @Test
@@ -354,6 +400,8 @@
             verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any());
             // Touch cancelled.
             verify(mNavHandleLongPressHandler, times(1)).onTouchFinished(any(), any());
+            verifyNoMoreInteractions(mStatsLogger);
+            verify(mStatsLatencyLogger).log(LAUNCHER_LATENCY_CONTEXTUAL_SEARCH_LPNH_ABANDON);
         } catch (Exception e) {
             throw new RuntimeException(e);
         }
@@ -391,6 +439,8 @@
             verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any());
             // Touch cancelled.
             verify(mNavHandleLongPressHandler, times(1)).onTouchFinished(any(), any());
+            verifyNoMoreInteractions(mStatsLogger);
+            verify(mStatsLatencyLogger).log(LAUNCHER_LATENCY_CONTEXTUAL_SEARCH_LPNH_ABANDON);
         } catch (Exception e) {
             throw new RuntimeException(e);
         }
@@ -408,6 +458,9 @@
         assertFalse(mLongPressTriggered.get());
         verify(mNavHandleLongPressHandler, never()).onTouchStarted(any());
         verify(mNavHandleLongPressHandler, never()).onTouchFinished(any(), any());
+        verifyNoMoreInteractions(mStatsLogManager);
+        verifyNoMoreInteractions(mStatsLogger);
+        verifyNoMoreInteractions(mStatsLatencyLogger);
     }
 
     @Test
@@ -422,6 +475,21 @@
         mUnderTest.onHoverEvent(generateCenteredMotionEvent(ACTION_HOVER_ENTER));
 
         verify(mDelegate, times(2)).onHoverEvent(any());
+
+        verifyNoMoreInteractions(mStatsLogManager);
+        verifyNoMoreInteractions(mStatsLogger);
+        verifyNoMoreInteractions(mStatsLatencyLogger);
+    }
+
+    @Test
+    public void testNoLogsForShortTouch() {
+        mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN));
+        SystemClock.sleep(10);
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+        mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_UP));
+        verifyNoMoreInteractions(mStatsLogManager);
+        verifyNoMoreInteractions(mStatsLogger);
+        verifyNoMoreInteractions(mStatsLatencyLogger);
     }
 
     private void initializeObjectUnderTest() {
@@ -437,6 +505,8 @@
         mUnderTest = new NavHandleLongPressInputConsumer(mContext, mDelegate, mInputMonitor,
                 mDeviceState, mNavHandle, mGestureState);
         mUnderTest.setNavHandleLongPressHandler(mNavHandleLongPressHandler);
+        mUnderTest.setStatsLogManager(mStatsLogManager);
+        mDownTimeMs = 0;
     }
 
     /** Generate a motion event centered horizontally in the screen. */
@@ -449,8 +519,12 @@
         return generateMotionEvent(motionAction, mScreenWidth / 2f, y);
     }
 
-    private static MotionEvent generateMotionEvent(int motionAction, float x, float y) {
-        return MotionEvent.obtain(0, 0, motionAction, x, y, 0);
+    private MotionEvent generateMotionEvent(int motionAction, float x, float y) {
+        if (motionAction == ACTION_DOWN) {
+            mDownTimeMs = SystemClock.uptimeMillis();
+        }
+        long eventTime = SystemClock.uptimeMillis();
+        return MotionEvent.obtain(mDownTimeMs, eventTime, motionAction, x, y, 0);
     }
 
     private static AutoCloseable overrideTwoStageFlag(boolean value) {
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/AppPairsControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/AppPairsControllerTest.kt
index ee70e0a..76d36d3 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/AppPairsControllerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/AppPairsControllerTest.kt
@@ -16,7 +16,9 @@
 
 package com.android.quickstep.util
 
+import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
 import android.content.Context
+import android.content.res.Resources
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import com.android.launcher3.apppairs.AppPairIcon
 import com.android.launcher3.logging.StatsLogManager
@@ -28,6 +30,7 @@
 import com.android.quickstep.TopTaskTracker.CachedTaskInfo
 import com.android.systemui.shared.recents.model.Task
 import com.android.systemui.shared.recents.model.Task.TaskKey
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
 import com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_33_66
 import com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50
 import com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_66_33
@@ -54,6 +57,7 @@
 @RunWith(AndroidJUnit4::class)
 class AppPairsControllerTest {
     @Mock lateinit var context: Context
+    @Mock lateinit var resources: Resources
     @Mock lateinit var splitSelectStateController: SplitSelectStateController
     @Mock lateinit var statsLogManager: StatsLogManager
 
@@ -109,6 +113,8 @@
         doNothing()
             .whenever(spyAppPairsController)
             .launchToSide(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull())
+        whenever(mockAppPairIcon.context.resources).thenReturn(resources)
+        whenever(DesktopModeStatus.canEnterDesktopMode(mockAppPairIcon.context)).thenReturn(false)
     }
 
     @Test
@@ -392,6 +398,68 @@
     }
 
     @Test
+    fun handleAppPairLaunchInApp_freeformTask1IsOnScreen_shouldLaunchAppPair() {
+        whenever(DesktopModeStatus.canEnterDesktopMode(mockAppPairIcon.context)).thenReturn(true)
+        /// Test launching apps 1 and 2 from app pair
+        whenever(mockTaskKey1.getId()).thenReturn(1)
+        whenever(mockTaskKey2.getId()).thenReturn(2)
+        // Task 1 is in freeform windowing mode
+        mockTaskKey1.windowingMode = WINDOWING_MODE_FREEFORM
+        // ... and app 1 is already on screen
+        if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+            whenever(mockCachedTaskInfo.topGroupedTaskContainsTask(eq(1))).thenReturn(true)
+        } else {
+            whenever(mockCachedTaskInfo.taskId).thenReturn(1)
+        }
+
+        // Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback
+        spyAppPairsController.handleAppPairLaunchInApp(
+            mockAppPairIcon,
+            listOf(mockItemInfo1, mockItemInfo2),
+        )
+        verify(splitSelectStateController)
+            .findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture())
+        val callback: Consumer<Array<Task>> = callbackCaptor.value
+        callback.accept(arrayOf(mockTask1, mockTask2))
+
+        // Verify that launchAppPair was called
+        verify(spyAppPairsController, times(1)).launchAppPair(any(), any())
+        verify(spyAppPairsController, never())
+            .launchToSide(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull())
+    }
+
+    @Test
+    fun handleAppPairLaunchInApp_freeformTask2IsOnScreen_shouldLaunchAppPair() {
+        whenever(DesktopModeStatus.canEnterDesktopMode(mockAppPairIcon.context)).thenReturn(true)
+        /// Test launching apps 1 and 2 from app pair
+        whenever(mockTaskKey1.getId()).thenReturn(1)
+        whenever(mockTaskKey2.getId()).thenReturn(2)
+        // Task 2 is in freeform windowing mode
+        mockTaskKey1.windowingMode = WINDOWING_MODE_FREEFORM
+        // ... and app 2 is already on screen
+        if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+            whenever(mockCachedTaskInfo.topGroupedTaskContainsTask(eq(2))).thenReturn(true)
+        } else {
+            whenever(mockCachedTaskInfo.taskId).thenReturn(2)
+        }
+
+        // Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback
+        spyAppPairsController.handleAppPairLaunchInApp(
+            mockAppPairIcon,
+            listOf(mockItemInfo1, mockItemInfo2),
+        )
+        verify(splitSelectStateController)
+            .findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture())
+        val callback: Consumer<Array<Task>> = callbackCaptor.value
+        callback.accept(arrayOf(mockTask1, mockTask2))
+
+        // Verify that launchAppPair was called
+        verify(spyAppPairsController, times(1)).launchAppPair(any(), any())
+        verify(spyAppPairsController, never())
+            .launchToSide(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull())
+    }
+
+    @Test
     fun handleAppPairLaunchInApp_shouldLaunchAppPairNormallyWhenUnrelatedSingleAppIsFullscreen() {
         // Test launching apps 1 and 2 from app pair
         whenever(mockTaskKey1.getId()).thenReturn(1)
diff --git a/quickstep/tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java b/quickstep/tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java
index fa2eb1e..d52d054 100644
--- a/quickstep/tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java
+++ b/quickstep/tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java
@@ -91,6 +91,7 @@
     private AppWidgetProviderInfo mApp4Provider1;
     private AppWidgetProviderInfo mApp4Provider2;
     private AppWidgetProviderInfo mApp5Provider1;
+    private AppWidgetProviderInfo mApp6PinOnlyProvider1;
     private List<AppWidgetProviderInfo> allWidgets;
 
     private FakeBgDataModelCallback mCallback = new FakeBgDataModelCallback();
@@ -117,8 +118,14 @@
                 ComponentName.createRelative("app4", ".provider2"));
         mApp5Provider1 = createAppWidgetProviderInfo(
                 ComponentName.createRelative("app5", "provider1"));
+        mApp6PinOnlyProvider1 = createAppWidgetProviderInfo(
+                ComponentName.createRelative("app6", "provider1"),
+                /*hideFromPicker=*/ true
+        );
+
+
         allWidgets = Arrays.asList(mApp1Provider1, mApp1Provider2, mApp2Provider1,
-                mApp4Provider1, mApp4Provider2, mApp5Provider1);
+                mApp4Provider1, mApp4Provider2, mApp5Provider1, mApp6PinOnlyProvider1);
 
         mLauncherApps = mModelHelper.sandboxContext.spyService(LauncherApps.class);
         doAnswer(i -> {
@@ -270,6 +277,32 @@
         });
     }
 
+    @Test
+    public void widgetsRecommendations_excludesWidgetsHiddenForPicker() {
+        runOnExecutorSync(MODEL_EXECUTOR, () -> {
+
+            // Not installed widget - hence eligible
+            AppTarget widget1 = new AppTarget(new AppTargetId("app1"), "app1", "provider1",
+                    mUserHandle);
+            // Provider marked as hidden from picker - hence not eligible
+            AppTarget widget6 = new AppTarget(new AppTargetId("app6"), "app6", "provider1",
+                    mUserHandle);
+
+            mCallback.mRecommendedWidgets = null;
+            mModelHelper.getModel().enqueueModelUpdateTask(
+                    newWidgetsPredicationTask(List.of(widget1, widget6)));
+            runOnExecutorSync(MAIN_EXECUTOR, () -> { });
+
+            // Only widget 1 (and no widget 6 as its meant to be hidden from picker).
+            List<PendingAddWidgetInfo> recommendedWidgets = mCallback.mRecommendedWidgets.items
+                    .stream()
+                    .map(itemInfo -> (PendingAddWidgetInfo) itemInfo)
+                    .collect(Collectors.toList());
+            assertThat(recommendedWidgets).hasSize(1);
+            assertThat(recommendedWidgets.get(0).componentName.getPackageName()).isEqualTo("app1");
+        });
+    }
+
     private void assertWidgetInfo(
             LauncherAppWidgetProviderInfo actual, AppWidgetProviderInfo expected) {
         assertThat(actual.provider).isEqualTo(expected.provider);
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
index 1c7f51c..1c87bce 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
@@ -23,6 +23,7 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeFalse;
@@ -581,6 +582,39 @@
                 numTasks - 1, recentsView.getTaskViewCount()));
     }
 
+    @Test
+    @PortraitLandscape
+    public void testDismissLastGridRow() throws Exception {
+        assumeTrue(mLauncher.isTablet());
+        mLauncher.goHome().switchToOverview().dismissAllTasks();
+        startTestAppsWithCheck();
+        startTestActivity(3);
+        startTestActivity(4);
+        runOnRecentsView(
+                recentsView -> assertNotEquals("Grid overview should have unequal row counts",
+                        recentsView.getTopRowTaskCountForTablet(),
+                        recentsView.getBottomRowTaskCountForTablet()));
+        Overview overview = mLauncher.goHome().switchToOverview();
+        assertIsInState("Launcher internal state didn't switch to Overview",
+                ExpectedState.OVERVIEW);
+        overview.flingForwardUntilClearAllVisible();
+        assertTrue("Clear All not visible.", overview.isClearAllVisible());
+        final Integer numTasks = getFromRecentsView(RecentsView::getTaskViewCount);
+        OverviewTask lastGridTask = overview.getCurrentTasksForTablet().stream().min(
+                Comparator.comparingInt(OverviewTask::getTaskCenterX)).get();
+        assertNotNull("lastGridTask null.", lastGridTask);
+
+        lastGridTask.dismiss();
+
+        runOnRecentsView(recentsView -> assertEquals(
+                "Dismissing a lastGridTask didn't remove 1 lastGridTask from Overview",
+                numTasks - 1, recentsView.getTaskViewCount()));
+        runOnRecentsView(recentsView -> assertEquals("Grid overview should have equal row counts.",
+                recentsView.getTopRowTaskCountForTablet(),
+                recentsView.getBottomRowTaskCountForTablet()));
+        assertTrue("Clear All not visible.", overview.isClearAllVisible());
+    }
+
     private void startTestAppsWithCheck() throws Exception {
         startTestApps();
         expectLaunchedAppState();
diff --git a/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java b/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java
index 31d0da0..f6acda4 100644
--- a/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java
+++ b/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java
@@ -20,6 +20,7 @@
 
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.LauncherPrefs;
+import com.android.launcher3.graphics.GridCustomizationsProxy;
 import com.android.launcher3.graphics.ThemeManager;
 import com.android.launcher3.icons.LauncherIcons.IconPool;
 import com.android.launcher3.model.ItemInstallQueue;
@@ -75,6 +76,8 @@
     InvariantDeviceProfile getIDP();
     IconPool getIconPool();
 
+    GridCustomizationsProxy getGridCustomizationsProxy();
+
     /** Builder for LauncherBaseAppComponent. */
     interface Builder {
         @BindsInstance Builder appContext(@ApplicationContext Context context);
diff --git a/src/com/android/launcher3/graphics/GridCustomizationsProvider.java b/src/com/android/launcher3/graphics/GridCustomizationsProxy.java
similarity index 87%
rename from src/com/android/launcher3/graphics/GridCustomizationsProvider.java
rename to src/com/android/launcher3/graphics/GridCustomizationsProxy.java
index 12c65c7..01c9d7e 100644
--- a/src/com/android/launcher3/graphics/GridCustomizationsProvider.java
+++ b/src/com/android/launcher3/graphics/GridCustomizationsProxy.java
@@ -22,7 +22,6 @@
 
 import static java.util.Objects.requireNonNullElse;
 
-import android.content.ContentProvider;
 import android.content.ContentValues;
 import android.content.Context;
 import android.content.pm.PackageManager;
@@ -45,9 +44,13 @@
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherModel;
 import com.android.launcher3.LauncherPrefs;
+import com.android.launcher3.dagger.ApplicationContext;
+import com.android.launcher3.dagger.LauncherAppSingleton;
 import com.android.launcher3.model.BgDataModel;
 import com.android.launcher3.shapes.IconShapeModel;
 import com.android.launcher3.shapes.ShapesProvider;
+import com.android.launcher3.util.ContentProviderProxy.ProxyProvider;
+import com.android.launcher3.util.DaggerSingletonTracker;
 import com.android.launcher3.util.Executors;
 import com.android.launcher3.util.Preconditions;
 import com.android.launcher3.util.RunnableList;
@@ -62,6 +65,8 @@
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ExecutionException;
 
+import javax.inject.Inject;
+
 /**
  * Exposes various launcher grid options and allows the caller to change them.
  * APIs:
@@ -76,7 +81,7 @@
  *          rows: number of rows in the grid
  *          cols: number of columns in the grid
  *          preview_count: number of previews available for this grid option. The preview uri
- *                         looks like /preview/<grid-name>/<preview index starting with 0>
+ *                         looks like /preview/[grid-name]/[preview index starting with 0]
  *          is_default: true if this grid option is currently set to the system
  *
  *     /get_preview: Open a file stream for the grid preview
@@ -85,7 +90,8 @@
  *          shape_key: key of the shape to apply
  *          name: key of the grid to apply
  */
-public class GridCustomizationsProvider extends ContentProvider {
+@LauncherAppSingleton
+public class GridCustomizationsProxy implements ProxyProvider {
 
     private static final String TAG = "GridCustomizationsProvider";
 
@@ -132,17 +138,31 @@
     private final Set<PreviewLifecycleObserver> mActivePreviews =
             Collections.newSetFromMap(new ConcurrentHashMap<>());
 
-    @Override
-    public boolean onCreate() {
-        return true;
+    private final Context mContext;
+    private final ThemeManager mThemeManager;
+    private final LauncherPrefs mPrefs;
+    private final InvariantDeviceProfile mIdp;
+
+    @Inject
+    GridCustomizationsProxy(
+            @ApplicationContext Context context,
+            ThemeManager themeManager,
+            LauncherPrefs prefs,
+            InvariantDeviceProfile idp,
+            DaggerSingletonTracker lifeCycle
+    ) {
+        mContext = context;
+        mThemeManager = themeManager;
+        mPrefs = prefs;
+        mIdp = idp;
+        lifeCycle.addCloseable(() -> mActivePreviews.forEach(PreviewLifecycleObserver::binderDied));
     }
 
     @Override
     public Cursor query(Uri uri, String[] projection, String selection,
             String[] selectionArgs, String sortOrder) {
-        Context context = getContext();
         String path = uri.getPath();
-        if (context == null || path == null) {
+        if (path == null) {
             return null;
         }
 
@@ -151,8 +171,7 @@
                 if (Flags.newCustomizationPickerUi()) {
                     MatrixCursor cursor = new MatrixCursor(new String[]{
                             KEY_SHAPE_KEY, KEY_SHAPE_TITLE, KEY_PATH, KEY_IS_DEFAULT});
-                    String currentShapePath =
-                            ThemeManager.INSTANCE.get(context).getIconState().getIconMask();
+                    String currentShapePath = mThemeManager.getIconState().getIconMask();
                     Optional<IconShapeModel> selectedShape = ShapesProvider.INSTANCE.getIconShapes()
                             .values()
                             .stream()
@@ -180,8 +199,7 @@
                 MatrixCursor cursor = new MatrixCursor(new String[]{
                         KEY_NAME, KEY_GRID_TITLE, KEY_ROWS, KEY_COLS, KEY_PREVIEW_COUNT,
                         KEY_IS_DEFAULT, KEY_GRID_ICON_ID});
-                InvariantDeviceProfile idp = InvariantDeviceProfile.INSTANCE.get(getContext());
-                List<GridOption> gridOptionList = idp.parseAllGridOptions(getContext());
+                List<GridOption> gridOptionList = mIdp.parseAllGridOptions(mContext);
                 if (com.android.launcher3.Flags.oneGridSpecs()) {
                     gridOptionList.sort(Comparator
                             .comparingInt((GridOption option) -> option.numColumns)
@@ -194,8 +212,8 @@
                             .add(KEY_ROWS, gridOption.numRows)
                             .add(KEY_COLS, gridOption.numColumns)
                             .add(KEY_PREVIEW_COUNT, 1)
-                            .add(KEY_IS_DEFAULT, idp.numColumns == gridOption.numColumns
-                                    && idp.numRows == gridOption.numRows)
+                            .add(KEY_IS_DEFAULT, mIdp.numColumns == gridOption.numColumns
+                                    && mIdp.numRows == gridOption.numRows)
                             .add(KEY_GRID_ICON_ID, gridOption.gridIconId);
                 }
                 return cursor;
@@ -203,8 +221,7 @@
             case GET_ICON_THEMED:
             case ICON_THEMED: {
                 MatrixCursor cursor = new MatrixCursor(new String[]{BOOLEAN_VALUE});
-                cursor.newRow().add(BOOLEAN_VALUE,
-                        ThemeManager.INSTANCE.get(getContext()).isMonoThemeEnabled() ? 1 : 0);
+                cursor.newRow().add(BOOLEAN_VALUE, mThemeManager.isMonoThemeEnabled() ? 1 : 0);
                 return cursor;
             }
             default:
@@ -213,38 +230,21 @@
     }
 
     @Override
-    public String getType(Uri uri) {
-        return "vnd.android.cursor.dir/launcher_grid";
-    }
-
-    @Override
-    public Uri insert(Uri uri, ContentValues initialValues) {
-        return null;
-    }
-
-    @Override
-    public int delete(Uri uri, String selection, String[] selectionArgs) {
-        return 0;
-    }
-
-    @Override
     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
         String path = uri.getPath();
-        Context context = getContext();
-        if (path == null || context == null) {
+        if (path == null) {
             return 0;
         }
         switch (path) {
             case KEY_DEFAULT_GRID: {
                 if (Flags.newCustomizationPickerUi()) {
-                    LauncherPrefs.INSTANCE.get(context).put(PREF_ICON_SHAPE,
+                    mPrefs.put(PREF_ICON_SHAPE,
                             requireNonNullElse(values.getAsString(KEY_SHAPE_KEY), ""));
                 }
                 String gridName = values.getAsString(KEY_NAME);
-                InvariantDeviceProfile idp = InvariantDeviceProfile.INSTANCE.get(context);
                 // Verify that this is a valid grid option
                 GridOption match = null;
-                for (GridOption option : idp.parseAllGridOptions(context)) {
+                for (GridOption option : mIdp.parseAllGridOptions(mContext)) {
                     String name = option.name;
                     if (name != null && name.equals(gridName)) {
                         match = option;
@@ -255,23 +255,22 @@
                     return 0;
                 }
 
-                idp.setCurrentGrid(context, gridName);
+                mIdp.setCurrentGrid(mContext, gridName);
                 if (Flags.newCustomizationPickerUi()) {
                     try {
                         // Wait for device profile to be fully reloaded and applied to the launcher
-                        loadModelSync(context);
+                        loadModelSync(mContext);
                     } catch (ExecutionException | InterruptedException e) {
                         Log.e(TAG, "Fail to load model", e);
                     }
                 }
-                context.getContentResolver().notifyChange(uri, null);
+                mContext.getContentResolver().notifyChange(uri, null);
                 return 1;
             }
             case ICON_THEMED:
             case SET_ICON_THEMED: {
-                ThemeManager.INSTANCE.get(context)
-                        .setMonoThemeEnabled(values.getAsBoolean(BOOLEAN_VALUE));
-                context.getContentResolver().notifyChange(uri, null);
+                mThemeManager.setMonoThemeEnabled(values.getAsBoolean(BOOLEAN_VALUE));
+                mContext.getContentResolver().notifyChange(uri, null);
                 return 1;
             }
             default:
@@ -298,12 +297,7 @@
 
     @Override
     public Bundle call(@NonNull String method, String arg, Bundle extras) {
-        Context context = getContext();
-        if (context == null) {
-            return null;
-        }
-
-        if (context.checkPermission("android.permission.BIND_WALLPAPER",
+        if (mContext.checkPermission("android.permission.BIND_WALLPAPER",
                 Binder.getCallingPid(), Binder.getCallingUid())
                 != PackageManager.PERMISSION_GRANTED) {
             return null;
@@ -317,14 +311,10 @@
     }
 
     private synchronized Bundle getPreview(Bundle request) {
-        Context context = getContext();
-        if (context == null) {
-            return null;
-        }
         RunnableList lifeCycleTracker = new RunnableList();
         try {
             PreviewSurfaceRenderer renderer = new PreviewSurfaceRenderer(
-                    getContext(), lifeCycleTracker, request);
+                    mContext, lifeCycleTracker, request);
             PreviewLifecycleObserver observer =
                     new PreviewLifecycleObserver(lifeCycleTracker, renderer);
 
diff --git a/src/com/android/launcher3/graphics/LauncherCustomizationProvider.kt b/src/com/android/launcher3/graphics/LauncherCustomizationProvider.kt
new file mode 100644
index 0000000..c949e2e
--- /dev/null
+++ b/src/com/android/launcher3/graphics/LauncherCustomizationProvider.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.graphics
+
+import android.content.Context
+import android.net.Uri
+import com.android.launcher3.dagger.LauncherComponentProvider.appComponent
+import com.android.launcher3.util.ContentProviderProxy
+
+/** Provider for various Launcher customizations exposed via a ContentProvider API */
+class LauncherCustomizationProvider : ContentProviderProxy() {
+
+    override fun getProxy(ctx: Context): ProxyProvider? = ctx.appComponent.gridCustomizationsProxy
+
+    override fun getType(uri: Uri) = "vnd.android.cursor.dir/launcher_grid"
+}
diff --git a/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java b/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java
index f0d670e..6fe5804 100644
--- a/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java
+++ b/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java
@@ -124,7 +124,7 @@
         if (Flags.newCustomizationPickerUi()) {
             updateColorOverrides(bundle);
         }
-        mHideQsb = bundle.getBoolean(GridCustomizationsProvider.KEY_HIDE_BOTTOM_ROW);
+        mHideQsb = bundle.getBoolean(GridCustomizationsProxy.KEY_HIDE_BOTTOM_ROW);
 
         mHostToken = bundle.getBinder(KEY_HOST_TOKEN);
         mWidth = bundle.getInt(KEY_VIEW_WIDTH);
diff --git a/src/com/android/launcher3/icons/LauncherIcons.kt b/src/com/android/launcher3/icons/LauncherIcons.kt
index 6c018e8..29c0de1 100644
--- a/src/com/android/launcher3/icons/LauncherIcons.kt
+++ b/src/com/android/launcher3/icons/LauncherIcons.kt
@@ -66,11 +66,16 @@
         return userCache.getUserInfo(user)
     }
 
-    public override fun getShapePath(drawable: AdaptiveIconDrawable, iconBounds: Rect): Path {
-        if (!Flags.enableLauncherIconShapes()) return drawable.iconMask
+    override fun getShapePath(drawable: AdaptiveIconDrawable, iconBounds: Rect): Path {
+        if (!Flags.enableLauncherIconShapes()) return super.getShapePath(drawable, iconBounds)
         return themeManager.iconShape.getPath(iconBounds)
     }
 
+    override fun getIconScale(): Float {
+        if (!Flags.enableLauncherIconShapes()) return super.getIconScale()
+        return themeManager.iconState.iconScale
+    }
+
     override fun drawAdaptiveIcon(
         canvas: Canvas,
         drawable: AdaptiveIconDrawable,
diff --git a/src/com/android/launcher3/logging/StatsLogManager.java b/src/com/android/launcher3/logging/StatsLogManager.java
index 9a1c874..2f1af68 100644
--- a/src/com/android/launcher3/logging/StatsLogManager.java
+++ b/src/com/android/launcher3/logging/StatsLogManager.java
@@ -905,6 +905,10 @@
         @UiEvent(doc = "Time passed between Contextual Search runnable creation and execution. This"
                 + " ensures that Recent animations have finished before Contextual Search starts.")
         LAUNCHER_LATENCY_OMNI_RUNNABLE(1546),
+
+        @UiEvent(doc = "Time passed between nav handle touch down and cancellation without "
+                + "triggering Contextual Search")
+        LAUNCHER_LATENCY_CONTEXTUAL_SEARCH_LPNH_ABANDON(2171),
         ;
 
         private final int mId;
diff --git a/src/com/android/launcher3/model/BaseLauncherBinder.java b/src/com/android/launcher3/model/BaseLauncherBinder.java
index 003bef3..3ee8b87 100644
--- a/src/com/android/launcher3/model/BaseLauncherBinder.java
+++ b/src/com/android/launcher3/model/BaseLauncherBinder.java
@@ -167,7 +167,7 @@
             return;
         }
         Map<PackageItemInfo, List<WidgetItem>>
-                widgetsByPackageItem = mBgDataModel.widgetsModel.getWidgetsByPackageItem();
+                widgetsByPackageItem = mBgDataModel.widgetsModel.getWidgetsByPackageItemForPicker();
         List<WidgetsListBaseEntry> widgets = new WidgetsListBaseEntriesBuilder(mApp.getContext())
                 .build(widgetsByPackageItem);
         Predicate<WidgetItem> filter = mBgDataModel.widgetsModel.getDefaultWidgetsFilter();
diff --git a/src/com/android/launcher3/model/ModelTaskController.kt b/src/com/android/launcher3/model/ModelTaskController.kt
index 40ea17d..6e3e35e 100644
--- a/src/com/android/launcher3/model/ModelTaskController.kt
+++ b/src/com/android/launcher3/model/ModelTaskController.kt
@@ -77,7 +77,7 @@
     }
 
     fun bindUpdatedWidgets(dataModel: BgDataModel) {
-        val widgetsByPackageItem = dataModel.widgetsModel.widgetsByPackageItem
+        val widgetsByPackageItem = dataModel.widgetsModel.widgetsByPackageItemForPicker
         val allWidgets = WidgetsListBaseEntriesBuilder(app.context).build(widgetsByPackageItem)
 
         val defaultWidgetsFilter = dataModel.widgetsModel.defaultWidgetsFilter
diff --git a/src/com/android/launcher3/model/WidgetsModel.java b/src/com/android/launcher3/model/WidgetsModel.java
index a176465..ab960d8 100644
--- a/src/com/android/launcher3/model/WidgetsModel.java
+++ b/src/com/android/launcher3/model/WidgetsModel.java
@@ -70,6 +70,7 @@
     private final Map<PackageItemInfo, List<WidgetItem>> mWidgetsByPackageItem = new HashMap<>();
     @Nullable private Predicate<WidgetItem> mDefaultWidgetsFilter = null;
     @Nullable private Predicate<WidgetItem> mPredictedWidgetsFilter = null;
+    @Nullable private WidgetValidityCheckForPicker mWidgetValidityCheckForPicker = null;
 
     /**
      * Returns all widgets keyed by their component key.
@@ -87,13 +88,44 @@
     }
 
     /**
-     * Returns widgets grouped by the package item that they should belong to.
+     * Returns widgets (eligible for display in picker) keyed by their component key.
      */
-    public synchronized Map<PackageItemInfo, List<WidgetItem>> getWidgetsByPackageItem() {
-        if (!WIDGETS_ENABLED) {
+    public synchronized Map<ComponentKey, WidgetItem> getWidgetsByComponentKeyForPicker() {
+        if (!WIDGETS_ENABLED || mWidgetValidityCheckForPicker == null) {
             return Collections.emptyMap();
         }
-        return new HashMap<>(mWidgetsByPackageItem);
+
+        return mWidgetsByPackageItem.values().stream()
+                .flatMap(Collection::stream).distinct()
+                .filter(widgetItem -> mWidgetValidityCheckForPicker.test(widgetItem))
+                .collect(Collectors.toMap(
+                        widget -> new ComponentKey(widget.componentName, widget.user),
+                        Function.identity()
+                ));
+    }
+
+    /**
+     * Returns widgets (displayable in the widget picker) grouped by the package item that
+     * they should belong to.
+     */
+    public synchronized Map<PackageItemInfo, List<WidgetItem>> getWidgetsByPackageItemForPicker() {
+        if (!WIDGETS_ENABLED || mWidgetValidityCheckForPicker == null) {
+            return Collections.emptyMap();
+        }
+
+        return mWidgetsByPackageItem.entrySet().stream()
+                .collect(
+                        Collectors.toMap(
+                                Map.Entry::getKey,
+                                entry -> entry.getValue().stream()
+                                        .filter(widgetItem ->
+                                                mWidgetValidityCheckForPicker.test(widgetItem))
+                                        .collect(Collectors.toList())
+                        )
+                )
+                .entrySet().stream()
+                .filter(entry -> !entry.getValue().isEmpty())
+                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
     }
 
     /**
@@ -181,6 +213,9 @@
             Log.d(TAG, "addWidgetsAndShortcuts, widgetsShortcuts#=" + rawWidgetsShortcuts.size());
         }
 
+        // Refresh the validity checker with latest app state.
+        mWidgetValidityCheckForPicker = new WidgetValidityCheckForPicker(app);
+
         // Temporary cache for {@link PackageItemInfos} to avoid having to go through
         // {@link mPackageItemInfos} to locate the key to be used for {@link #mWidgetsList}
         PackageItemInfoCache packageItemInfoCache = new PackageItemInfoCache();
@@ -195,7 +230,6 @@
 
         // add and update.
         mWidgetsByPackageItem.putAll(rawWidgetsShortcuts.stream()
-                .filter(new WidgetValidityCheck(app))
                 .filter(new WidgetFlagCheck())
                 .flatMap(widgetItem -> getPackageUserKeys(app.getContext(), widgetItem).stream()
                         .map(key -> new Pair<>(packageItemInfoCache.getOrCreate(key), widgetItem)))
@@ -270,12 +304,15 @@
         return packageUserKeys;
     }
 
-    private static class WidgetValidityCheck implements Predicate<WidgetItem> {
+    /**
+     * Checks if widgets are eligible for displaying in widget picker / tray.
+     */
+    private static class WidgetValidityCheckForPicker implements Predicate<WidgetItem> {
 
         private final InvariantDeviceProfile mIdp;
         private final AppFilter mAppFilter;
 
-        WidgetValidityCheck(LauncherAppState app) {
+        WidgetValidityCheckForPicker(LauncherAppState app) {
             mIdp = app.getInvariantDeviceProfile();
             mAppFilter = new AppFilter(app.getContext());
         }
@@ -310,6 +347,10 @@
         }
     }
 
+    /**
+     * Checks if certain widgets that are available behind flag can be used across all surfaces in
+     * launcher.
+     */
     private static class WidgetFlagCheck implements Predicate<WidgetItem> {
 
         private static final String BUBBLES_SHORTCUT_WIDGET =
diff --git a/src/com/android/launcher3/testing/TestInformationProvider.java b/src/com/android/launcher3/testing/TestInformationProvider.java
index 17b472a..4b592e7 100644
--- a/src/com/android/launcher3/testing/TestInformationProvider.java
+++ b/src/com/android/launcher3/testing/TestInformationProvider.java
@@ -16,61 +16,40 @@
 
 package com.android.launcher3.testing;
 
-import android.content.ContentProvider;
-import android.content.ContentValues;
-import android.database.Cursor;
-import android.net.Uri;
+import android.content.Context;
 import android.os.Bundle;
 import android.util.Log;
 
-import com.android.launcher3.Utilities;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 
-public class TestInformationProvider extends ContentProvider {
+import com.android.launcher3.Utilities;
+import com.android.launcher3.util.ContentProviderProxy;
+
+public class TestInformationProvider extends ContentProviderProxy {
 
     private static final String TAG = "TestInformationProvider";
 
+    @Nullable
     @Override
-    public boolean onCreate() {
-        return true;
-    }
-
-    @Override
-    public int update(Uri uri, ContentValues contentValues, String s, String[] strings) {
-        return 0;
-    }
-
-    @Override
-    public int delete(Uri uri, String s, String[] strings) {
-        return 0;
-    }
-
-    @Override
-    public Uri insert(Uri uri, ContentValues contentValues) {
-        return null;
-    }
-
-    @Override
-    public String getType(Uri uri) {
-        return null;
-    }
-
-    @Override
-    public Cursor query(Uri uri, String[] strings, String s, String[] strings1, String s1) {
-        return null;
-    }
-
-    @Override
-    public Bundle call(String method, String arg, Bundle extras) {
+    public ProxyProvider getProxy(@NonNull Context context) {
         if (Utilities.isRunningInTestHarness()) {
-            TestInformationHandler handler = TestInformationHandler.newInstance(getContext());
-            handler.init(getContext());
+            return new ProxyProvider() {
+                @Nullable
+                @Override
+                public Bundle call(@NonNull String method, @Nullable String arg,
+                        @Nullable Bundle extras) {
+                    TestInformationHandler handler = TestInformationHandler.newInstance(context);
+                    handler.init(context);
 
-            Bundle response =  handler.call(method, arg, extras);
-            if (response == null) {
-                Log.e(TAG, "Couldn't handle method: " + method + "; current handler="
-                        + handler.getClass().getSimpleName());
-            }
-            return response;
+                    Bundle response = handler.call(method, arg, extras);
+                    if (response == null) {
+                        Log.e(TAG, "Couldn't handle method: " + method + "; current handler="
+                                + handler.getClass().getSimpleName());
+                    }
+                    return response;
+                }
+            };
         }
         return null;
     }
diff --git a/src/com/android/launcher3/util/ContentProviderProxy.kt b/src/com/android/launcher3/util/ContentProviderProxy.kt
new file mode 100644
index 0000000..db693db
--- /dev/null
+++ b/src/com/android/launcher3/util/ContentProviderProxy.kt
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2025 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.content.ContentProvider
+import android.content.ContentValues
+import android.content.Context
+import android.database.Cursor
+import android.net.Uri
+import android.os.Bundle
+
+/** Wrapper around [ContentProvider] which allows delegating all calls to an interface */
+abstract class ContentProviderProxy : ContentProvider() {
+
+    override fun onCreate() = true
+
+    override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int =
+        checkGetProxy()?.delete(uri, selection, selectionArgs) ?: 0
+
+    /** Do not route this call through proxy as it doesn't generally require initializing objects */
+    override fun getType(uri: Uri): String? = null
+
+    override fun insert(uri: Uri, values: ContentValues?): Uri? =
+        checkGetProxy()?.insert(uri, values)
+
+    override fun query(
+        uri: Uri,
+        projection: Array<out String>?,
+        selection: String?,
+        selectionArgs: Array<out String>?,
+        sortOrder: String?,
+    ): Cursor? = checkGetProxy()?.query(uri, projection, selection, selectionArgs, sortOrder)
+
+    override fun update(
+        uri: Uri,
+        values: ContentValues?,
+        selection: String?,
+        selectionArgs: Array<out String>?,
+    ): Int = checkGetProxy()?.update(uri, values, selection, selectionArgs) ?: 0
+
+    override fun call(method: String, arg: String?, extras: Bundle?): Bundle? =
+        checkGetProxy()?.call(method, arg, extras)
+
+    private fun checkGetProxy(): ProxyProvider? = context?.let { getProxy(it) }
+
+    abstract fun getProxy(ctx: Context): ProxyProvider?
+
+    /** Interface for handling the actual content provider calls */
+    interface ProxyProvider {
+
+        fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int = 0
+
+        fun insert(uri: Uri, values: ContentValues?): Uri? = null
+
+        fun query(
+            uri: Uri,
+            projection: Array<out String>?,
+            selection: String?,
+            selectionArgs: Array<out String>?,
+            sortOrder: String?,
+        ): Cursor? = null
+
+        fun update(
+            uri: Uri,
+            values: ContentValues?,
+            selection: String?,
+            selectionArgs: Array<out String>?,
+        ): Int = 0
+
+        fun call(method: String, arg: String?, extras: Bundle?): Bundle? = null
+    }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/model/WidgetsModelTest.kt b/tests/multivalentTests/src/com/android/launcher3/model/WidgetsModelTest.kt
index ae4ff04..d704195 100644
--- a/tests/multivalentTests/src/com/android/launcher3/model/WidgetsModelTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/model/WidgetsModelTest.kt
@@ -119,6 +119,11 @@
                     // A widget in different package (none of that app's widgets are in widget
                     // sections xml)
                     createAppWidgetProviderInfo(AppBTestWidgetComponent),
+                    // A widget in different app that is meant to be hidden from picker
+                    createAppWidgetProviderInfo(
+                        AppCPinOnlyTestWidgetComponent,
+                        /*hideFromPicker=*/ true,
+                    ),
                 )
             )
 
@@ -129,12 +134,13 @@
     }
 
     @Test
-    fun widgetsByPackage_treatsWidgetSectionsAsSeparatePackageItems() {
+    fun widgetsByPackageForPicker_treatsWidgetSectionsAsSeparatePackageItems() {
         loadWidgets()
 
-        val packages: Map<PackageItemInfo, List<WidgetItem>> = underTest.widgetsByPackageItem
+        val packages: Map<PackageItemInfo, List<WidgetItem>> =
+            underTest.widgetsByPackageItemForPicker
 
-        // expect 3 package items
+        // expect 3 package items (no app C as its widget is hidden from picker)
         // one for the custom section with widget from appA
         // one for package section for second widget from appA (that wasn't listed in xml)
         // and one for package section for appB
@@ -167,6 +173,13 @@
         assertThat(appBPackageSection).hasSize(1)
         val widgetsInAppBSection = appBPackageSection.entries.first().value
         assertThat(widgetsInAppBSection).hasSize(1)
+
+        // No App C's package section - as the only widget hosted by it is hidden in picker
+        val appCPackageSection =
+            packageSections.filter {
+                it.key.packageName == AppCPinOnlyTestWidgetComponent.packageName
+            }
+        assertThat(appCPackageSection).isEmpty()
     }
 
     @Test
@@ -175,7 +188,29 @@
 
         val widgetsByComponentKey: Map<ComponentKey, WidgetItem> = underTest.widgetsByComponentKey
 
+        // Has all widgets including ones not visible in picker
+        assertThat(widgetsByComponentKey).hasSize(4)
+        widgetsByComponentKey.forEach { entry ->
+            assertThat(entry.key).isEqualTo(entry.value as ComponentKey)
+        }
+    }
+
+    @Test
+    fun widgetComponentMapForPicker_excludesWidgetsHiddenInPicker() {
+        loadWidgets()
+
+        val widgetsByComponentKey: Map<ComponentKey, WidgetItem> =
+            underTest.widgetsByComponentKeyForPicker
+
+        // Has all widgets excluding the appC's widget.
         assertThat(widgetsByComponentKey).hasSize(3)
+        assertThat(
+                widgetsByComponentKey.filter {
+                    it.key.componentName == AppCPinOnlyTestWidgetComponent
+                }
+            )
+            .isEmpty()
+        // widgets mapped correctly
         widgetsByComponentKey.forEach { entry ->
             assertThat(entry.key).isEqualTo(entry.value as ComponentKey)
         }
@@ -189,7 +224,7 @@
     }
 
     @Test
-    fun getWidgetsByPackageItem_returnsACopyOfMap() {
+    fun getWidgetsByPackageItemForPicker_returnsACopyOfMap() {
         loadWidgets()
 
         val latch = CountDownLatch(1)
@@ -198,8 +233,8 @@
 
             // each "widgetsByPackageItem" read returns a different copy of the map held internally.
             // Modifying one shouldn't impact another.
-            for ((_, _) in underTest.widgetsByPackageItem.entries) {
-                underTest.widgetsByPackageItem.clear()
+            for ((_, _) in underTest.widgetsByPackageItemForPicker.entries) {
+                underTest.widgetsByPackageItemForPicker.clear()
                 if (update) { // trigger update
                     update = false
                     // Similarly, model could update its code independently while a client is
@@ -256,6 +291,9 @@
         private val AppBTestWidgetComponent: ComponentName =
             ComponentName.createRelative("com.test.package", "TestProvider")
 
+        private val AppCPinOnlyTestWidgetComponent: ComponentName =
+            ComponentName.createRelative("com.testC.package", "PinOnlyTestProvider")
+
         private const val LOAD_WIDGETS_TIMEOUT_SECONDS = 2L
     }
 }
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/WidgetUtils.java b/tests/multivalentTests/src/com/android/launcher3/util/WidgetUtils.java
index a87a208..9fbd7ff 100644
--- a/tests/multivalentTests/src/com/android/launcher3/util/WidgetUtils.java
+++ b/tests/multivalentTests/src/com/android/launcher3/util/WidgetUtils.java
@@ -15,6 +15,8 @@
  */
 package com.android.launcher3.util;
 
+import static android.appwidget.AppWidgetProviderInfo.WIDGET_FEATURE_HIDE_FROM_PICKER;
+
 import android.appwidget.AppWidgetProviderInfo;
 import android.content.ComponentName;
 import android.content.Context;
@@ -83,14 +85,30 @@
 
     /**
      * Creates a {@link AppWidgetProviderInfo} for the provided component name
+     *
+     * @param cn component name of the appwidget provider
+     * @param hideFromPicker indicates if the widget should appear in widget picker
      */
-    public static AppWidgetProviderInfo createAppWidgetProviderInfo(ComponentName cn) {
+    public static AppWidgetProviderInfo createAppWidgetProviderInfo(ComponentName cn,
+            boolean hideFromPicker) {
         ActivityInfo activityInfo = new ActivityInfo();
         activityInfo.applicationInfo = new ApplicationInfo();
         activityInfo.applicationInfo.uid = Process.myUid();
         AppWidgetProviderInfo info = new AppWidgetProviderInfo();
+        if (hideFromPicker) {
+            info.widgetFeatures = WIDGET_FEATURE_HIDE_FROM_PICKER;
+        }
         info.providerInfo = activityInfo;
         info.provider = cn;
         return info;
     }
+
+    /**
+     * Creates a {@link AppWidgetProviderInfo} for the provided component name
+     *
+     * @param cn component name of the appwidget provider
+     */
+    public static AppWidgetProviderInfo createAppWidgetProviderInfo(ComponentName cn) {
+        return createAppWidgetProviderInfo(cn, /*hideFromPicker=*/ false);
+    }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/BaseOverview.java b/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
index ef72a0f..f633d48 100644
--- a/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
+++ b/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
@@ -157,12 +157,7 @@
              LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
                      "dismissing all tasks")) {
             final BySelector clearAllSelector = mLauncher.getOverviewObjectSelector("clear_all");
-            for (int i = 0;
-                    i < FLINGS_FOR_DISMISS_LIMIT
-                            && !verifyActiveContainer().hasObject(clearAllSelector);
-                    ++i) {
-                flingForwardImpl();
-            }
+            flingForwardUntilClearAllVisible();
 
             final Runnable clickClearAll = () -> mLauncher.clickLauncherObject(
                     mLauncher.waitForObjectInContainer(verifyActiveContainer(),
@@ -183,6 +178,17 @@
     }
 
     /**
+     * Scrolls until Clear-all button is visible.
+     */
+    public void flingForwardUntilClearAllVisible() {
+        final BySelector clearAllSelector = mLauncher.getOverviewObjectSelector("clear_all");
+        for (int i = 0; i < FLINGS_FOR_DISMISS_LIMIT
+                && !verifyActiveContainer().hasObject(clearAllSelector); ++i) {
+            flingForwardImpl();
+        }
+    }
+
+    /**
      * Touch to the right of current task. This should dismiss overview and go back to Workspace.
      */
     public Workspace touchOutsideFirstTask() {
diff --git a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
index 4e94e1e..8fbb5e3 100644
--- a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
+++ b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
@@ -125,7 +125,7 @@
         return right - left;
     }
 
-    int getTaskCenterX() {
+    public int getTaskCenterX() {
         return mTask.getVisibleCenter().x;
     }