Merge "Do not enter stage split from keyboard shortcuts if the user is already in split" into main
diff --git a/Android.bp b/Android.bp
index 75902c8..39b1ae0 100644
--- a/Android.bp
+++ b/Android.bp
@@ -175,6 +175,7 @@
         "androidx.preference_preference",
         "androidx.slice_slice-view",
         "androidx.cardview_cardview",
+        "androidx.window_window",
         "com.google.android.material_material",
         "iconloader_base",
         "view_capture",
diff --git a/AndroidManifest-common.xml b/AndroidManifest-common.xml
index 7e824ec..a31ee80 100644
--- a/AndroidManifest-common.xml
+++ b/AndroidManifest-common.xml
@@ -184,5 +184,9 @@
             android:name="androidx.startup.InitializationProvider"
             android:authorities="${applicationId}.androidx-startup"
             tools:node="remove" />
+
+        <property
+            android:name="android.window.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED"
+            android:value="true" />
     </application>
 </manifest>
diff --git a/OWNERS b/OWNERS
index b8aae78..4409b33 100644
--- a/OWNERS
+++ b/OWNERS
@@ -25,5 +25,10 @@
 andonian@google.com
 sihua@google.com
 
+# Multitasking eng team
+tracyzhou@google.com
+peanutbutter@google.com
+jeremysim@google.com
+
 per-file FeatureFlags.java, globs = set noparent
 per-file FeatureFlags.java = sunnygoyal@google.com, winsonc@google.com, adamcohen@google.com, hyunyoungs@google.com, captaincole@google.com
diff --git a/aconfig/launcher.aconfig b/aconfig/launcher.aconfig
index 98dd4d6..defb0e6 100644
--- a/aconfig/launcher.aconfig
+++ b/aconfig/launcher.aconfig
@@ -85,6 +85,13 @@
 }
 
 flag {
+    name: "enable_two_pane_launcher_settings"
+    namespace: "launcher"
+    description: "Enables two panel settings when on large enough displays"
+    bug: "204463748"
+}
+
+flag {
     name: "enable_shortcut_dont_suggest_app"
     namespace: "launcher"
     description: "Enables don't suggest app shortcut for suggested apps"
@@ -126,3 +133,10 @@
     description: "Enables asnc inflation of workspace icons"
     bug: "318539160"
 }
+
+flag {
+    name: "enable_unfold_state_animation"
+    namespace: "launcher"
+    description: "Tie unfold animation with state animation"
+    bug: "297057373"
+}
diff --git a/quickstep/src/com/android/launcher3/appprediction/PredictionRowView.java b/quickstep/src/com/android/launcher3/appprediction/PredictionRowView.java
index f012197..e680ea9 100644
--- a/quickstep/src/com/android/launcher3/appprediction/PredictionRowView.java
+++ b/quickstep/src/com/android/launcher3/appprediction/PredictionRowView.java
@@ -72,6 +72,8 @@
 
     private boolean mPredictionsEnabled = false;
 
+    private boolean mPredictionUiUpdatePaused = false;
+
     public PredictionRowView(@NonNull Context context) {
         this(context, null);
     }
@@ -193,7 +195,18 @@
         applyPredictionApps();
     }
 
+    /** Pause the prediction row UI update */
+    public void setPredictionUiUpdatePaused(boolean predictionUiUpdatePaused) {
+        mPredictionUiUpdatePaused = predictionUiUpdatePaused;
+        if (!mPredictionUiUpdatePaused) {
+            applyPredictionApps();
+        }
+    }
+
     private void applyPredictionApps() {
+        if (mPredictionUiUpdatePaused) {
+            return;
+        }
         if (getChildCount() != mNumPredictedAppsPerRow) {
             while (getChildCount() > mNumPredictedAppsPerRow) {
                 removeViewAt(0);
diff --git a/quickstep/src/com/android/launcher3/proxy/ProxyActivityStarter.java b/quickstep/src/com/android/launcher3/proxy/ProxyActivityStarter.java
index 6d90b035..212a5ff 100644
--- a/quickstep/src/com/android/launcher3/proxy/ProxyActivityStarter.java
+++ b/quickstep/src/com/android/launcher3/proxy/ProxyActivityStarter.java
@@ -53,13 +53,10 @@
 
         try {
             if (mParams.intent != null) {
-                startActivityForResult(mParams.intent, mParams.requestCode, mParams.options);
+                startActivity();
                 return;
             } else if (mParams.intentSender != null) {
-                startIntentSenderForResult(mParams.intentSender, mParams.requestCode,
-                        mParams.fillInIntent, mParams.flagsMask, mParams.flagsValues,
-                        mParams.extraFlags,
-                        mParams.options);
+                startIntentSender();
                 return;
             }
         } catch (NullPointerException | ActivityNotFoundException | SecurityException
@@ -83,4 +80,26 @@
                 .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
                         | Intent.FLAG_ACTIVITY_CLEAR_TASK);
     }
+
+    private void startActivity() throws SendIntentException {
+        if (mParams.requireActivityResult) {
+            startActivityForResult(mParams.intent, mParams.requestCode, mParams.options);
+        } else {
+            startActivity(mParams.intent, mParams.options);
+            finishAndRemoveTask();
+        }
+    }
+
+    private void startIntentSender() throws SendIntentException {
+        if (mParams.requireActivityResult) {
+            startIntentSenderForResult(mParams.intentSender, mParams.requestCode,
+                    mParams.fillInIntent, mParams.flagsMask, mParams.flagsValues,
+                    mParams.extraFlags,
+                    mParams.options);
+        } else {
+            startIntentSender(mParams.intentSender, mParams.fillInIntent, mParams.flagsMask,
+                    mParams.flagsValues, mParams.extraFlags, mParams.options);
+            finishAndRemoveTask();
+        }
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
index 33641a4..7c7c426 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
@@ -22,6 +22,7 @@
 import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL;
 
 import static com.android.launcher3.BaseActivity.EVENT_DESTROYED;
+import static com.android.launcher3.Flags.enableUnfoldStateAnimation;
 import static com.android.launcher3.LauncherState.OVERVIEW;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_TASKBAR_NAVBAR_UNIFICATION;
 import static com.android.launcher3.config.FeatureFlags.enableTaskbarNoRecreate;
@@ -405,8 +406,12 @@
      */
     private UnfoldTransitionProgressProvider getUnfoldTransitionProgressProviderForActivity(
             StatefulActivity activity) {
-        if (activity instanceof QuickstepLauncher) {
-            return ((QuickstepLauncher) activity).getUnfoldTransitionProgressProvider();
+        if (!enableUnfoldStateAnimation()) {
+            if (activity instanceof QuickstepLauncher ql) {
+                return ql.getUnfoldTransitionProgressProvider();
+            }
+        } else {
+            return SystemUiProxy.INSTANCE.get(mContext).getUnfoldTransitionProvider();
         }
         return null;
     }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
index 74517a8..666d98e 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
@@ -51,6 +51,7 @@
 import com.android.launcher3.Utilities;
 import com.android.launcher3.apppairs.AppPairIcon;
 import com.android.launcher3.folder.FolderIcon;
+import com.android.launcher3.folder.PreviewBackground;
 import com.android.launcher3.icons.ThemedIconDrawable;
 import com.android.launcher3.model.data.FolderInfo;
 import com.android.launcher3.model.data.ItemInfo;
@@ -618,9 +619,11 @@
         super.onDraw(canvas);
         if (mLeaveBehindFolderIcon != null) {
             canvas.save();
-            canvas.translate(mLeaveBehindFolderIcon.getLeft(), mLeaveBehindFolderIcon.getTop());
-            mLeaveBehindFolderIcon.getFolderBackground().drawLeaveBehind(canvas,
-                    mFolderLeaveBehindColor);
+            canvas.translate(
+                    mLeaveBehindFolderIcon.getLeft() + mLeaveBehindFolderIcon.getTranslationX(),
+                    mLeaveBehindFolderIcon.getTop());
+            PreviewBackground previewBackground = mLeaveBehindFolderIcon.getFolderBackground();
+            previewBackground.drawLeaveBehind(canvas, mFolderLeaveBehindColor);
             canvas.restore();
         }
     }
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBackground.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBackground.kt
index 1e3f4f1..aa2b29d 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBackground.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBackground.kt
@@ -50,6 +50,22 @@
 
     var width: Float = 0f
 
+    /**
+     * Set whether the drawable is anchored to the left or right edge of the container.
+     *
+     * When `anchorLeft` is set to `true`, drawable left edge aligns up with the container left
+     * edge. Drawable can be drawn outside container bounds on the right edge. When it is set to
+     * `false` (the default), drawable right edge aligns up with the container right edge. Drawable
+     * can be drawn outside container bounds on the left edge.
+     */
+    var anchorLeft: Boolean = false
+        set(value) {
+            if (field != value) {
+                field = value
+                invalidateSelf()
+            }
+        }
+
     init {
         paint.color = context.getColor(R.color.taskbar_background)
         paint.flags = Paint.ANTI_ALIAS_FLAG
@@ -106,15 +122,9 @@
 
         // Draw background.
         val radius = backgroundHeight / 2f
-        canvas.drawRoundRect(
-            canvas.width.toFloat() - width,
-            0f,
-            canvas.width.toFloat(),
-            canvas.height.toFloat(),
-            radius,
-            radius,
-            paint
-        )
+        val left = if (anchorLeft) 0f else canvas.width.toFloat() - width
+        val right = if (anchorLeft) width else canvas.width.toFloat()
+        canvas.drawRoundRect(left, 0f, right, canvas.height.toFloat(), radius, radius, paint)
 
         if (showingArrow) {
             // Draw arrow.
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
index ec9f4e5..5ca2991 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
@@ -197,6 +197,16 @@
         updateChildrenRenderNodeProperties();
     }
 
+    @Override
+    public void onRtlPropertiesChanged(int layoutDirection) {
+        // TODO(b/273310265): set this based on bubble bar position and not LTR or RTL
+        mBubbleBarBackground.setAnchorLeft(layoutDirection == LAYOUT_DIRECTION_RTL);
+    }
+
+    private boolean isOnLeft() {
+        return getLayoutDirection() == LAYOUT_DIRECTION_RTL;
+    }
+
     /**
      * Updates the bounds with translation that may have been applied and returns the result.
      */
@@ -275,18 +285,31 @@
         int bubbleCount = getChildCount();
         final float ty = (mBubbleBarBounds.height() - mIconSize) / 2f;
         final boolean animate = getVisibility() == VISIBLE;
+        final boolean onLeft = isOnLeft();
         for (int i = 0; i < bubbleCount; i++) {
             BubbleView bv = (BubbleView) getChildAt(i);
             bv.setTranslationY(ty);
 
             // the position of the bubble when the bar is fully expanded
-            final float expandedX = i * (mIconSize + mIconSpacing);
+            final float expandedX;
             // the position of the bubble when the bar is fully collapsed
-            final float collapsedX = i == 0 ? 0 : mIconOverlapAmount;
+            final float collapsedX;
+            if (onLeft) {
+                // If bar is on the left, bubbles are ordered right to left
+                expandedX = (bubbleCount - i - 1) * (mIconSize + mIconSpacing);
+                // Shift the first bubble only if there are more bubbles in addition to overflow
+                collapsedX = i == 0 && bubbleCount > 2 ? mIconOverlapAmount : 0;
+            } else {
+                // Bubbles ordered left to right, don't move the first bubble
+                expandedX = i * (mIconSize + mIconSpacing);
+                collapsedX = i == 0 ? 0 : mIconOverlapAmount;
+            }
 
             if (mIsBarExpanded) {
+                // If bar is on the right, account for bubble bar expanding and shifting left
+                final float expandedBarShift = onLeft ? 0 : currentWidth - expandedWidth;
                 // where the bubble will end up when the animation ends
-                final float targetX = currentWidth - expandedWidth + expandedX;
+                final float targetX = expandedX + expandedBarShift;
                 bv.setTranslationX(widthState * (targetX - collapsedX) + collapsedX);
                 // if we're fully expanded, set the z level to 0 or to bubble elevation if dragged
                 if (widthState == 1f) {
@@ -296,7 +319,9 @@
                 bv.setBehindStack(false, animate);
                 bv.setAlpha(1);
             } else {
-                final float targetX = currentWidth - collapsedWidth + collapsedX;
+                // If bar is on the right, account for bubble bar expanding and shifting left
+                final float collapsedBarShift = onLeft ? 0 : currentWidth - collapsedWidth;
+                final float targetX = collapsedX + collapsedBarShift;
                 bv.setTranslationX(widthState * (expandedX - targetX) + targetX);
                 bv.setZ((MAX_BUBBLES * mBubbleElevation) - i);
                 // If we're not the first bubble we're behind the stack
@@ -318,18 +343,22 @@
         final float expandedArrowPosition = arrowPositionForSelectedWhenExpanded();
         final float interpolatedWidth =
                 widthState * (expandedWidth - collapsedWidth) + collapsedWidth;
-        if (mIsBarExpanded) {
-            // when the bar is expanding, the selected bubble is always the first, so the arrow
-            // always shifts with the interpolated width.
-            final float arrowPosition = currentWidth - interpolatedWidth + collapsedArrowPosition;
-            mBubbleBarBackground.setArrowPosition(arrowPosition);
+        final float arrowPosition;
+        if (onLeft) {
+            float interpolatedShift = (expandedArrowPosition - collapsedArrowPosition) * widthState;
+            arrowPosition = collapsedArrowPosition + interpolatedShift;
         } else {
-            final float targetPosition = currentWidth - collapsedWidth + collapsedArrowPosition;
-            final float arrowPosition =
-                    targetPosition + widthState * (expandedArrowPosition - targetPosition);
-            mBubbleBarBackground.setArrowPosition(arrowPosition);
+            if (mIsBarExpanded) {
+                // when the bar is expanding, the selected bubble is always the first, so the arrow
+                // always shifts with the interpolated width.
+                arrowPosition = currentWidth - interpolatedWidth + collapsedArrowPosition;
+            } else {
+                final float targetPosition = currentWidth - collapsedWidth + collapsedArrowPosition;
+                arrowPosition =
+                        targetPosition + widthState * (expandedArrowPosition - targetPosition);
+            }
         }
-
+        mBubbleBarBackground.setArrowPosition(arrowPosition);
         mBubbleBarBackground.setArrowAlpha((int) (255 * widthState));
         mBubbleBarBackground.setWidth(interpolatedWidth);
     }
@@ -394,9 +423,8 @@
             Log.w(TAG, "trying to update selection arrow without a selected view!");
             return;
         }
-        final int index = indexOfChild(mSelectedBubbleView);
         // Find the center of the bubble when it's expanded, set the arrow position to it.
-        final float tx = getPaddingStart() + index * (mIconSize + mIconSpacing) + mIconSize / 2f;
+        final float tx = arrowPositionForSelectedWhenExpanded();
 
         if (shouldAnimate) {
             final float currentArrowPosition = mBubbleBarBackground.getArrowPositionX();
@@ -416,12 +444,27 @@
 
     private float arrowPositionForSelectedWhenExpanded() {
         final int index = indexOfChild(mSelectedBubbleView);
-        return getPaddingStart() + index * (mIconSize + mIconSpacing) + mIconSize / 2f;
+        final int bubblePosition;
+        if (isOnLeft()) {
+            // Bubble positions are reversed. First bubble is on the right.
+            bubblePosition = getChildCount() - index - 1;
+        } else {
+            bubblePosition = index;
+        }
+        return getPaddingStart() + bubblePosition * (mIconSize + mIconSpacing) + mIconSize / 2f;
     }
 
     private float arrowPositionForSelectedWhenCollapsed() {
         final int index = indexOfChild(mSelectedBubbleView);
-        return getPaddingStart() + index * (mIconOverlapAmount) + mIconSize / 2f;
+        final int bubblePosition;
+        if (isOnLeft()) {
+            // Bubble positions are reversed. First bubble may be shifted, if there are more
+            // bubbles than the current bubble and overflow.
+            bubblePosition = index == 0 && getChildCount() > 2 ? 1 : 0;
+        } else {
+            bubblePosition = index;
+        }
+        return getPaddingStart() + bubblePosition * (mIconOverlapAmount) + mIconSize / 2f;
     }
 
     @Override
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
index 065dd58..6bb7b04 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -21,6 +21,7 @@
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.util.Log;
+import android.view.Gravity;
 import android.view.MotionEvent;
 import android.view.View;
 import android.widget.FrameLayout;
@@ -289,7 +290,8 @@
      */
     public void addBubble(BubbleBarItem b) {
         if (b != null) {
-            mBarView.addView(b.getView(), 0, new FrameLayout.LayoutParams(mIconSize, mIconSize));
+            mBarView.addView(b.getView(), 0,
+                    new FrameLayout.LayoutParams(mIconSize, mIconSize, Gravity.LEFT));
             b.getView().setOnClickListener(mBubbleClickListener);
             mBubbleDragController.setupBubbleView(b.getView());
         } else {
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/OWNERS b/quickstep/src/com/android/launcher3/taskbar/bubbles/OWNERS
index 7af0389..edabae2 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/OWNERS
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/OWNERS
@@ -1 +1,3 @@
+atsjenk@google.com
+liranb@google.com
 madym@google.com
diff --git a/quickstep/src/com/android/launcher3/uioverrides/ApiWrapper.java b/quickstep/src/com/android/launcher3/uioverrides/ApiWrapper.java
index 3ebc8ed..5c4eb9d 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/ApiWrapper.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/ApiWrapper.java
@@ -138,6 +138,11 @@
                         || Flags.enablePrivateSpaceInstallShortcut())) {
             StartActivityParams params = new StartActivityParams((PendingIntent) null, 0);
             params.intentSender = launcherApps.getAppMarketActivityIntent(packageName, user);
+            ActivityOptions options = ActivityOptions.makeBasic()
+                    .setPendingIntentBackgroundActivityStartMode(ActivityOptions
+                            .MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
+            params.options = options.toBundle();
+            params.requireActivityResult = false;
             return ProxyActivityStarter.getLaunchIntent(context, params);
         } else {
             return new Intent(Intent.ACTION_VIEW)
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index c3bcde0..2e8e613 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -21,6 +21,7 @@
 import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_FOCUSED;
 
 import static com.android.app.animation.Interpolators.EMPHASIZED;
+import static com.android.launcher3.Flags.enableUnfoldStateAnimation;
 import static com.android.launcher3.LauncherConstants.SavedInstanceKeys.PENDING_SPLIT_SELECT_INFO;
 import static com.android.launcher3.LauncherConstants.SavedInstanceKeys.RUNTIME_STATE;
 import static com.android.launcher3.LauncherSettings.Animation.DEFAULT_NO_ICON;
@@ -170,6 +171,8 @@
 import com.android.quickstep.util.SplitToWorkspaceController;
 import com.android.quickstep.util.SplitWithKeyboardShortcutController;
 import com.android.quickstep.util.TISBindHelper;
+import com.android.quickstep.util.unfold.LauncherUnfoldTransitionController;
+import com.android.quickstep.util.unfold.ProxyUnfoldTransitionProvider;
 import com.android.quickstep.views.FloatingTaskView;
 import com.android.quickstep.views.OverviewActionsView;
 import com.android.quickstep.views.RecentsView;
@@ -367,11 +370,19 @@
         mHotseatPredictionController.setPauseUIUpdate(getTaskbarUIController() == null);
         Log.d("b/318394698", "startActivitySafely being run, getTaskbarUIController is: "
                 + getTaskbarUIController());
+        PredictionRowView<?> predictionRowView =
+                getAppsView().getFloatingHeaderView().findFixedRowByType(PredictionRowView.class);
+        // Pause the prediction row updates until the transition (if it exists) ends.
+        predictionRowView.setPredictionUiUpdatePaused(true);
         RunnableList result = super.startActivitySafely(v, intent, item);
         if (result == null) {
             mHotseatPredictionController.setPauseUIUpdate(false);
+            predictionRowView.setPredictionUiUpdatePaused(false);
         } else {
-            result.add(() -> mHotseatPredictionController.setPauseUIUpdate(false));
+            result.add(() -> {
+                mHotseatPredictionController.setPauseUIUpdate(false);
+                predictionRowView.setPredictionUiUpdatePaused(false);
+            });
         }
         return result;
     }
@@ -468,7 +479,7 @@
 
     @Override
     public void bindExtraContainerItems(FixedContainerItems item) {
-        Log.d(TAG, "Bind extra container items");
+        Log.d(TAG, "Bind extra container items. ContainerId = " + item.containerId);
         if (item.containerId == Favorites.CONTAINER_PREDICTION) {
             mAllAppsPredictions = item;
             PredictionRowView<?> predictionRowView =
@@ -957,9 +968,17 @@
     }
 
     private void initUnfoldTransitionProgressProvider() {
-        final UnfoldTransitionConfig config = new ResourceUnfoldTransitionConfig();
-        if (config.isEnabled()) {
-            initRemotelyCalculatedUnfoldAnimation(config);
+        if (!enableUnfoldStateAnimation()) {
+            final UnfoldTransitionConfig config = new ResourceUnfoldTransitionConfig();
+            if (config.isEnabled()) {
+                initRemotelyCalculatedUnfoldAnimation(config);
+            }
+        } else {
+            ProxyUnfoldTransitionProvider provider =
+                    SystemUiProxy.INSTANCE.get(this).getUnfoldTransitionProvider();
+            if (provider != null) {
+                new LauncherUnfoldTransitionController(this, provider);
+            }
         }
     }
 
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java
index 723af43..56a4024 100644
--- a/quickstep/src/com/android/quickstep/SystemUiProxy.java
+++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java
@@ -17,6 +17,7 @@
 
 import static android.app.ActivityManager.RECENT_IGNORE_UNAVAILABLE;
 
+import static com.android.launcher3.Flags.enableUnfoldStateAnimation;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 import static com.android.launcher3.util.SplitConfigurationOptions.StagePosition;
@@ -67,6 +68,7 @@
 import com.android.launcher3.util.Preconditions;
 import com.android.quickstep.util.ActiveGestureLog;
 import com.android.quickstep.util.AssistUtils;
+import com.android.quickstep.util.unfold.ProxyUnfoldTransitionProvider;
 import com.android.systemui.shared.recents.ISystemUiProxy;
 import com.android.systemui.shared.recents.model.ThumbnailData;
 import com.android.systemui.shared.system.RecentsAnimationControllerCompat;
@@ -74,6 +76,7 @@
 import com.android.systemui.shared.system.smartspace.ILauncherUnlockAnimationController;
 import com.android.systemui.shared.system.smartspace.ISysuiUnlockAnimationController;
 import com.android.systemui.shared.system.smartspace.SmartspaceState;
+import com.android.systemui.unfold.config.ResourceUnfoldTransitionConfig;
 import com.android.systemui.unfold.progress.IUnfoldAnimation;
 import com.android.systemui.unfold.progress.IUnfoldTransitionListener;
 import com.android.wm.shell.back.IBackAnimation;
@@ -177,7 +180,10 @@
      */
     private final PendingIntent mRecentsPendingIntent;
 
-    public SystemUiProxy(Context context) {
+    @Nullable
+    private final ProxyUnfoldTransitionProvider mUnfoldTransitionProvider;
+
+    private SystemUiProxy(Context context) {
         mContext = context;
         mAsyncHandler = new Handler(UI_HELPER_EXECUTOR.getLooper(), this::handleMessageAsync);
         final Intent baseIntent = new Intent().setPackage(mContext.getPackageName());
@@ -187,6 +193,10 @@
         mRecentsPendingIntent = PendingIntent.getActivity(mContext, 0, baseIntent,
                 PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT
                         | Intent.FILL_IN_COMPONENT, options.toBundle());
+
+        mUnfoldTransitionProvider =
+                (enableUnfoldStateAnimation() && new ResourceUnfoldTransitionConfig().isEnabled())
+                         ? new ProxyUnfoldTransitionProvider() : null;
     }
 
     @Override
@@ -251,7 +261,7 @@
         mRecentTasks = recentTasks;
         mBackAnimation = backAnimation;
         mDesktopMode = desktopMode;
-        mUnfoldAnimation = unfoldAnimation;
+        mUnfoldAnimation = enableUnfoldStateAnimation() ? null : unfoldAnimation;
         mDragAndDrop = dragAndDrop;
         linkToDeath();
         // re-attach the listeners once missing due to setProxy has not been initialized yet.
@@ -272,6 +282,19 @@
         setAssistantOverridesRequested(
                 AssistUtils.newInstance(mContext).getSysUiAssistOverrideInvocationTypes());
         mStateChangeCallbacks.forEach(Runnable::run);
+
+        if (mUnfoldTransitionProvider != null) {
+            if (unfoldAnimation != null) {
+                try {
+                    unfoldAnimation.setListener(mUnfoldTransitionProvider);
+                    mUnfoldTransitionProvider.setActive(true);
+                } catch (RemoteException e) {
+                    // Ignore
+                }
+            } else {
+                mUnfoldTransitionProvider.setActive(false);
+            }
+        }
     }
 
     /**
@@ -1451,6 +1474,11 @@
         }
     }
 
+    @Nullable
+    public ProxyUnfoldTransitionProvider getUnfoldTransitionProvider() {
+        return mUnfoldTransitionProvider;
+    }
+
     //
     // Recents
     //
diff --git a/quickstep/src/com/android/quickstep/TaskIconCache.java b/quickstep/src/com/android/quickstep/TaskIconCache.java
index 1b3f598..4e6b23f 100644
--- a/quickstep/src/com/android/quickstep/TaskIconCache.java
+++ b/quickstep/src/com/android/quickstep/TaskIconCache.java
@@ -191,8 +191,7 @@
             entry.contentDescription = getBadgedContentDescription(
                     activityInfo, task.key.userId, task.taskDescription);
             if (enableOverviewIconMenu()) {
-                entry.title = Utilities.trim(
-                        activityInfo.applicationInfo.loadLabel(mContext.getPackageManager()));
+                entry.title = Utilities.trim(activityInfo.loadLabel(mContext.getPackageManager()));
             }
         }
 
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index 647ff90..ce6ddd8 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -77,7 +77,6 @@
 import android.view.InputDevice;
 import android.view.InputEvent;
 import android.view.MotionEvent;
-import android.view.SurfaceControl;
 import android.view.accessibility.AccessibilityManager;
 
 import androidx.annotation.BinderThread;
@@ -303,15 +302,6 @@
             });
         }
 
-        @Override
-        public void onNavigationBarSurface(SurfaceControl surface) {
-            // TODO: implement
-            if (surface != null) {
-                surface.release();
-                surface = null;
-            }
-        }
-
         @BinderThread
         public void onSystemUiStateChanged(int stateFlags) {
             MAIN_EXECUTOR.execute(() -> executeForTouchInteractionService(tis -> {
diff --git a/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java b/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java
index 28efc97..50a5a83 100644
--- a/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java
+++ b/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java
@@ -16,6 +16,7 @@
 
 package com.android.quickstep.util;
 
+import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR;
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
 import static com.android.quickstep.views.DesktopTaskView.isDesktopModeSupported;
 
@@ -43,6 +44,7 @@
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.icons.BitmapInfo;
 import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.model.data.FolderInfo;
 import com.android.launcher3.model.data.PackageItemInfo;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.quickstep.views.FloatingTaskView;
@@ -123,7 +125,12 @@
             intent = appInfo.intent;
             user = appInfo.user;
             bitmapInfo = appInfo.bitmap;
+        } else if (tag instanceof FolderInfo fi && fi.itemType == ITEM_TYPE_APP_PAIR) {
+            // Prompt the user to select something else by wiggling the instructions view
+            mController.getSplitInstructionsView().goBoing();
+            return true;
         } else {
+            // Use Launcher's default click handler
             return false;
         }
 
diff --git a/quickstep/src/com/android/quickstep/util/unfold/LauncherUnfoldTransitionController.kt b/quickstep/src/com/android/quickstep/util/unfold/LauncherUnfoldTransitionController.kt
new file mode 100644
index 0000000..54d317d
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/unfold/LauncherUnfoldTransitionController.kt
@@ -0,0 +1,129 @@
+/*
+ * 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.quickstep.util.unfold
+
+import android.app.Activity
+import android.os.Trace
+import android.view.Surface
+import com.android.launcher3.Alarm
+import com.android.launcher3.DeviceProfile
+import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener
+import com.android.launcher3.Launcher
+import com.android.launcher3.anim.PendingAnimation
+import com.android.launcher3.config.FeatureFlags
+import com.android.launcher3.util.ActivityLifecycleCallbacksAdapter
+import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
+
+/** Controls animations that are happening during unfolding foldable devices */
+class LauncherUnfoldTransitionController(
+    private val launcher: Launcher,
+    private val progressProvider: ProxyUnfoldTransitionProvider
+) : OnDeviceProfileChangeListener, ActivityLifecycleCallbacksAdapter, TransitionProgressListener {
+
+    private var isTablet: Boolean? = null
+    private var hasUnfoldTransitionStarted = false
+    private val timeoutAlarm =
+        Alarm().apply {
+            setOnAlarmListener {
+                onTransitionFinished()
+                Trace.endAsyncSection("$TAG#startedPreemptively", 0)
+            }
+        }
+
+    init {
+        launcher.addOnDeviceProfileChangeListener(this)
+        launcher.registerActivityLifecycleCallbacks(this)
+    }
+
+    override fun onActivityPaused(activity: Activity) {
+        progressProvider.removeCallback(this)
+    }
+
+    override fun onActivityResumed(activity: Activity) {
+        progressProvider.addCallback(this)
+    }
+
+    override fun onDeviceProfileChanged(dp: DeviceProfile) {
+        if (!FeatureFlags.PREEMPTIVE_UNFOLD_ANIMATION_START.get()) {
+            return
+        }
+
+        if (isTablet != null && dp.isTablet != isTablet) {
+            // We should preemptively start the animation only if:
+            // - We changed to the unfolded screen
+            // - SystemUI IPC connection is alive, so we won't end up in a situation that we won't
+            //   receive transition progress events from SystemUI later because there was no
+            //   IPC connection established (e.g. because of SystemUI crash)
+            // - SystemUI has not already sent unfold animation progress events. This might happen
+            //   if Launcher was not open during unfold, in this case we receive the configuration
+            //   change only after we went back to home screen and we don't want to start the
+            //   animation in this case.
+            if (dp.isTablet && progressProvider.isActive && !hasUnfoldTransitionStarted) {
+                // Preemptively start the unfold animation to make sure that we have drawn
+                // the first frame of the animation before the screen gets unblocked
+                onTransitionStarted()
+                Trace.beginAsyncSection("$TAG#startedPreemptively", 0)
+                timeoutAlarm.setAlarm(PREEMPTIVE_UNFOLD_TIMEOUT_MS)
+            }
+            if (!dp.isTablet) {
+                // Reset unfold transition status when folded
+                hasUnfoldTransitionStarted = false
+            }
+        }
+
+        isTablet = dp.isTablet
+    }
+
+    override fun onTransitionStarted() {
+        hasUnfoldTransitionStarted = true
+        launcher.animationCoordinator.setAnimation(
+            provider = this,
+            factory = this::onPrepareUnfoldAnimation,
+            duration =
+                1000L // The expected duration for the animation. Then only comes to play if we have
+            // to run the animation ourselves in case sysui misses the end signal
+        )
+        timeoutAlarm.cancelAlarm()
+    }
+
+    override fun onTransitionProgress(progress: Float) {
+        hasUnfoldTransitionStarted = true
+        launcher.animationCoordinator.getPlaybackController(this)?.setPlayFraction(progress)
+    }
+
+    override fun onTransitionFinished() {
+        // Run the animation to end the animation in case it is not already at end progress. It
+        // will scale the duration to the remaining progress
+        launcher.animationCoordinator.getPlaybackController(this)?.start()
+        timeoutAlarm.cancelAlarm()
+    }
+
+    private fun onPrepareUnfoldAnimation(anim: PendingAnimation) {
+        val dp = launcher.deviceProfile
+        val rotation = dp.displayInfo.rotation
+        val isVertical = rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180
+        UnfoldAnimationBuilder.buildUnfoldAnimation(
+            launcher,
+            isVertical,
+            dp.displayInfo.currentSize,
+            anim
+        )
+    }
+
+    companion object {
+        private const val TAG = "LauncherUnfoldTransitionController"
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/util/unfold/ProxyUnfoldTransitionProvider.kt b/quickstep/src/com/android/quickstep/util/unfold/ProxyUnfoldTransitionProvider.kt
new file mode 100644
index 0000000..83c7f72
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/unfold/ProxyUnfoldTransitionProvider.kt
@@ -0,0 +1,97 @@
+/*
+ * 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.quickstep.util.unfold
+
+import androidx.annotation.AnyThread
+import androidx.annotation.FloatRange
+import com.android.launcher3.util.Executors.MAIN_EXECUTOR
+import com.android.systemui.unfold.UnfoldTransitionProgressProvider
+import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
+import com.android.systemui.unfold.progress.IUnfoldTransitionListener
+import com.android.systemui.unfold.progress.UnfoldRemoteFilter
+
+/** Receives unfold events from remote senders (System UI). */
+class ProxyUnfoldTransitionProvider :
+    UnfoldTransitionProgressProvider, IUnfoldTransitionListener.Stub() {
+
+    private val listeners: MutableSet<TransitionProgressListener> = mutableSetOf()
+    private val delegate = UnfoldRemoteFilter(ProcessedProgressListener())
+
+    private var transitionStarted = false
+    var isActive = false
+        set(value) {
+            field = value
+            if (!value) {
+                // Finish any active transition
+                onTransitionFinished()
+            }
+        }
+
+    @AnyThread
+    override fun onTransitionStarted() {
+        MAIN_EXECUTOR.execute(delegate::onTransitionStarted)
+    }
+
+    @AnyThread
+    override fun onTransitionProgress(progress: Float) {
+        MAIN_EXECUTOR.execute { delegate.onTransitionProgress(progress) }
+    }
+
+    @AnyThread
+    override fun onTransitionFinished() {
+        MAIN_EXECUTOR.execute(delegate::onTransitionFinished)
+    }
+
+    override fun addCallback(listener: TransitionProgressListener) {
+        listeners += listener
+        if (transitionStarted) {
+            // Update the listener in case there was is an active transition
+            listener.onTransitionStarted()
+        }
+    }
+
+    override fun removeCallback(listener: TransitionProgressListener) {
+        listeners -= listener
+        if (transitionStarted) {
+            // Finish the transition if it was already running
+            listener.onTransitionFinished()
+        }
+    }
+
+    override fun destroy() {
+        listeners.clear()
+    }
+
+    private inner class ProcessedProgressListener : TransitionProgressListener {
+        override fun onTransitionStarted() {
+            if (!transitionStarted) {
+                transitionStarted = true
+                listeners.forEach(TransitionProgressListener::onTransitionStarted)
+            }
+        }
+
+        override fun onTransitionProgress(@FloatRange(from = 0.0, to = 1.0) progress: Float) {
+            listeners.forEach { it.onTransitionProgress(progress) }
+        }
+
+        override fun onTransitionFinished() {
+            if (transitionStarted) {
+                transitionStarted = false
+                listeners.forEach(TransitionProgressListener::onTransitionFinished)
+            }
+        }
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/util/unfold/UnfoldAnimationBuilder.kt b/quickstep/src/com/android/quickstep/util/unfold/UnfoldAnimationBuilder.kt
new file mode 100644
index 0000000..d2c4728
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/unfold/UnfoldAnimationBuilder.kt
@@ -0,0 +1,167 @@
+/*
+ * 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.quickstep.util.unfold
+
+import android.graphics.Point
+import android.view.ViewGroup
+import com.android.app.animation.Interpolators.LINEAR
+import com.android.app.animation.Interpolators.clampToProgress
+import com.android.launcher3.CellLayout
+import com.android.launcher3.Launcher
+import com.android.launcher3.LauncherAnimUtils.HOTSEAT_SCALE_PROPERTY_FACTORY
+import com.android.launcher3.LauncherAnimUtils.SCALE_INDEX_UNFOLD_ANIMATION
+import com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X
+import com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y
+import com.android.launcher3.LauncherAnimUtils.WORKSPACE_SCALE_PROPERTY_FACTORY
+import com.android.launcher3.Workspace
+import com.android.launcher3.anim.PendingAnimation
+import com.android.launcher3.util.HorizontalInsettableView
+
+private typealias ViewGroupAction = (ViewGroup, Boolean) -> Unit
+
+object UnfoldAnimationBuilder {
+
+    private val CLIP_CHILDREN: ViewGroupAction = ViewGroup::setClipChildren
+    private val CLIP_TO_PADDING: ViewGroupAction = ViewGroup::setClipToPadding
+
+    data class RestoreInfo(val action: ViewGroupAction, var target: ViewGroup, var value: Boolean)
+
+    // Percentage of the width of the quick search bar that will be reduced
+    // from the both sides of the bar when progress is 0
+    private const val MAX_WIDTH_INSET_FRACTION = 0.04f
+
+    // Scale factor for the whole workspace and hotseat
+    private const val SCALE_LAUNCHER_FROM = 0.92f
+
+    // Translation factor for all the items on the homescreen
+    private const val TRANSLATION_PERCENTAGE = 0.08f
+
+    private fun setClipChildren(
+        target: ViewGroup,
+        value: Boolean,
+        restoreList: MutableList<RestoreInfo>
+    ) {
+        val originalValue = target.clipChildren
+        if (originalValue != value) {
+            target.clipChildren = value
+            restoreList.add(RestoreInfo(CLIP_CHILDREN, target, originalValue))
+        }
+    }
+
+    private fun setClipToPadding(
+        target: ViewGroup,
+        value: Boolean,
+        restoreList: MutableList<RestoreInfo>
+    ) {
+        val originalValue = target.clipToPadding
+        if (originalValue != value) {
+            target.clipToPadding = value
+            restoreList.add(RestoreInfo(CLIP_TO_PADDING, target, originalValue))
+        }
+    }
+
+    private fun addChildrenAnimation(
+        itemsContainer: ViewGroup,
+        isVerticalFold: Boolean,
+        screenSize: Point,
+        anim: PendingAnimation
+    ) {
+        val tempLocation = IntArray(2)
+        for (i in 0 until itemsContainer.childCount) {
+            val child = itemsContainer.getChildAt(i)
+
+            child.getLocationOnScreen(tempLocation)
+            if (isVerticalFold) {
+                val viewCenterX = tempLocation[0] + child.width / 2
+                val distanceFromScreenCenterToViewCenter = screenSize.x / 2 - viewCenterX
+                anim.addFloat(
+                    child,
+                    VIEW_TRANSLATE_X,
+                    distanceFromScreenCenterToViewCenter * TRANSLATION_PERCENTAGE,
+                    0f,
+                    LINEAR
+                )
+            } else {
+                val viewCenterY = tempLocation[1] + child.height / 2
+                val distanceFromScreenCenterToViewCenter = screenSize.y / 2 - viewCenterY
+                anim.addFloat(
+                    child,
+                    VIEW_TRANSLATE_Y,
+                    distanceFromScreenCenterToViewCenter * TRANSLATION_PERCENTAGE,
+                    0f,
+                    LINEAR
+                )
+            }
+        }
+    }
+
+    /**
+     * Builds an animation for the unfold experience and adds it to the provided PendingAnimation
+     */
+    fun buildUnfoldAnimation(
+        launcher: Launcher,
+        isVerticalFold: Boolean,
+        screenSize: Point,
+        anim: PendingAnimation
+    ) {
+        val restoreList = ArrayList<RestoreInfo>()
+        val registerViews: (CellLayout) -> Unit = { cellLayout ->
+            setClipChildren(cellLayout, false, restoreList)
+            setClipToPadding(cellLayout, false, restoreList)
+            addChildrenAnimation(cellLayout.shortcutsAndWidgets, isVerticalFold, screenSize, anim)
+        }
+
+        val workspace: Workspace<*> = launcher.workspace
+        val hotseat = launcher.hotseat
+
+        // Animation icons from workspace for all orientations
+        workspace.forEachVisiblePage { registerViews(it as CellLayout) }
+        setClipChildren(workspace, false, restoreList)
+        setClipToPadding(workspace, true, restoreList)
+
+        // Workspace scale
+        launcher.workspace.setPivotToScaleWithSelf(launcher.hotseat)
+        val interpolator = clampToProgress(LINEAR, 0f, 1f)
+        anim.addFloat(
+            workspace,
+            WORKSPACE_SCALE_PROPERTY_FACTORY[SCALE_INDEX_UNFOLD_ANIMATION],
+            SCALE_LAUNCHER_FROM,
+            1f,
+            interpolator
+        )
+        anim.addFloat(
+            hotseat,
+            HOTSEAT_SCALE_PROPERTY_FACTORY[SCALE_INDEX_UNFOLD_ANIMATION],
+            SCALE_LAUNCHER_FROM,
+            1f,
+            interpolator
+        )
+
+        if (isVerticalFold) {
+            if (hotseat.qsb is HorizontalInsettableView) {
+                anim.addFloat(
+                    hotseat.qsb as HorizontalInsettableView,
+                    HorizontalInsettableView.HORIZONTAL_INSETS,
+                    MAX_WIDTH_INSET_FRACTION,
+                    0f,
+                    LINEAR
+                )
+            }
+            registerViews(hotseat)
+        }
+        anim.addEndListener { restoreList.forEach { it.action(it.target, it.value) } }
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java b/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java
index f6501f1..f39a901 100644
--- a/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java
+++ b/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java
@@ -16,6 +16,8 @@
 
 package com.android.quickstep.views;
 
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
 import android.content.Context;
 import android.os.Bundle;
 import android.util.AttributeSet;
@@ -26,9 +28,15 @@
 import android.widget.TextView;
 
 import androidx.annotation.Nullable;
+import androidx.dynamicanimation.animation.DynamicAnimation;
+import androidx.dynamicanimation.animation.SpringAnimation;
+import androidx.dynamicanimation.animation.SpringForce;
 
+import com.android.app.animation.Interpolators;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.statemanager.StatefulActivity;
 
@@ -40,7 +48,11 @@
  * Appears and disappears concurrently with a FloatingTaskView.
  */
 public class SplitInstructionsView extends LinearLayout {
+    private static final int BOUNCE_DURATION = 250;
+    private static final float BOUNCE_HEIGHT = 20;
+
     private final StatefulActivity mLauncher;
+    public boolean mIsCurrentlyAnimating = false;
 
     public static final FloatProperty<SplitInstructionsView> UNFOLD =
             new FloatProperty<>("SplitInstructionsUnfold") {
@@ -55,6 +67,19 @@
                 }
             };
 
+    public static final FloatProperty<SplitInstructionsView> TRANSLATE_Y =
+            new FloatProperty<>("SplitInstructionsTranslateY") {
+                @Override
+                public void setValue(SplitInstructionsView splitInstructionsView, float v) {
+                    splitInstructionsView.setTranslationY(v);
+                }
+
+                @Override
+                public Float get(SplitInstructionsView splitInstructionsView) {
+                    return splitInstructionsView.getTranslationY();
+                }
+            };
+
     public SplitInstructionsView(Context context) {
         this(context, null);
     }
@@ -143,4 +168,42 @@
                         getMeasuredWidth()
                 );
     }
+
+    /**
+     * Draws attention to the split instructions view by bouncing it up and down.
+     */
+    public void goBoing() {
+        if (mIsCurrentlyAnimating) {
+            return;
+        }
+
+        float restingY = getTranslationY();
+        float bounceToY = restingY - Utilities.dpToPx(BOUNCE_HEIGHT);
+        PendingAnimation anim = new PendingAnimation(BOUNCE_DURATION);
+        // Animate the view lifting up to a higher position
+        anim.addFloat(this, TRANSLATE_Y, restingY, bounceToY, Interpolators.STANDARD);
+
+        anim.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationStart(Animator animation) {
+                mIsCurrentlyAnimating = true;
+            }
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                // Create a low stiffness, medium bounce spring centering at the rest position
+                SpringForce spring = new SpringForce(restingY)
+                        .setDampingRatio(SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY)
+                        .setStiffness(SpringForce.STIFFNESS_LOW);
+                // Animate the view getting pulled back to rest position by the spring
+                SpringAnimation springAnim = new SpringAnimation(SplitInstructionsView.this,
+                        DynamicAnimation.TRANSLATION_Y).setSpring(spring).setStartValue(bounceToY);
+
+                springAnim.addEndListener((a, b, c, d) -> mIsCurrentlyAnimating = false);
+                springAnim.start();
+            }
+        });
+
+        anim.buildAnim().start();
+    }
 }
diff --git a/res/layout/widgets_two_pane_sheet_recyclerview.xml b/res/layout/widgets_two_pane_sheet_recyclerview.xml
index c9c855c..8b48abb 100644
--- a/res/layout/widgets_two_pane_sheet_recyclerview.xml
+++ b/res/layout/widgets_two_pane_sheet_recyclerview.xml
@@ -45,24 +45,22 @@
                 android:background="?attr/widgetPickerPrimarySurfaceColor"
                 android:clipToPadding="false"
                 android:elevation="0.1dp"
-                android:paddingBottom="8dp"
+                android:paddingBottom="16dp"
                 android:paddingHorizontal="@dimen/widget_list_horizontal_margin_two_pane"
                 launcher:layout_sticky="true">
 
                 <include layout="@layout/widgets_search_bar" />
             </FrameLayout>
 
-            <LinearLayout
+            <FrameLayout
                 android:layout_width="match_parent"
                 android:layout_height="match_parent"
                 android:id="@+id/suggestions_header"
-                android:layout_marginTop="8dp"
                 android:layout_marginHorizontal="@dimen/widget_list_horizontal_margin_two_pane"
                 android:paddingBottom="16dp"
-                android:orientation="horizontal"
                 android:background="?attr/widgetPickerPrimarySurfaceColor"
                 launcher:layout_sticky="true">
-            </LinearLayout>
+            </FrameLayout>
         </com.android.launcher3.views.StickyHeaderLayout>
     </FrameLayout>
 </merge>
\ No newline at end of file
diff --git a/res/values/config.xml b/res/values/config.xml
index 2a8ec28..5bdd7ebb 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -252,6 +252,9 @@
     <!--  Used for custom widgets  -->
     <array name="custom_widget_providers"/>
 
+    <!-- Embed parameters -->
+    <dimen name="activity_split_ratio"  format="float">0.5</dimen>
+    <integer name="min_width_split">720</integer>
 
     <!-- Skip "Install to private" long-press shortcut packages name -->
     <string-array name="skip_private_profile_shortcut_packages" translatable="false">
diff --git a/res/xml/split_configuration.xml b/res/xml/split_configuration.xml
new file mode 100644
index 0000000..531fef8
--- /dev/null
+++ b/res/xml/split_configuration.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources
+    xmlns:window="http://schemas.android.com/apk/res-auto">
+    <!-- Automatically split the following activity pairs. -->
+    <SplitPairRule
+        window:splitRatio="@dimen/activity_split_ratio"
+        window:splitLayoutDirection="locale"
+        window:splitMinWidthDp="@integer/min_width_split"
+        window:splitMaxAspectRatioInPortrait="alwaysAllow"
+        window:finishPrimaryWithSecondary="never"
+        window:finishSecondaryWithPrimary="always"
+        window:clearTop="false">
+        <SplitPairFilter
+            window:primaryActivityName="com.android.launcher3.settings.SettingsActivity"
+            window:secondaryActivityName="com.android.launcher3.settings.SettingsActivity"/>
+
+    </SplitPairRule>
+</resources>
\ No newline at end of file
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 826eeb2..39b8de1 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -152,6 +152,7 @@
 import androidx.annotation.StringRes;
 import androidx.annotation.UiThread;
 import androidx.annotation.VisibleForTesting;
+import androidx.window.embedding.RuleController;
 
 import com.android.launcher3.DropTarget.DragObject;
 import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
@@ -580,6 +581,11 @@
         }
         setTitle(R.string.home_screen);
         mStartupLatencyLogger.logEnd(LAUNCHER_LATENCY_STARTUP_ACTIVITY_ON_CREATE);
+
+        if (com.android.launcher3.Flags.enableTwoPaneLauncherSettings()) {
+            RuleController.getInstance(this).setRules(
+                    RuleController.parseRules(this, R.xml.split_configuration));
+        }
     }
 
     protected ModelCallbacks createModelCallbacks() {
diff --git a/src/com/android/launcher3/allapps/PrivateProfileManager.java b/src/com/android/launcher3/allapps/PrivateProfileManager.java
index aee511c..1c46dac 100644
--- a/src/com/android/launcher3/allapps/PrivateProfileManager.java
+++ b/src/com/android/launcher3/allapps/PrivateProfileManager.java
@@ -21,6 +21,7 @@
 import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_PRIVATE_SPACE_HEADER;
 import static com.android.launcher3.allapps.SectionDecorationInfo.ROUND_NOTHING;
 import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_PRIVATE_PROFILE_QUIET_MODE_ENABLED;
+import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_PRIVATE_SPACE_INSTALL_APP;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 import static com.android.launcher3.util.SettingsCache.PRIVATE_SPACE_HIDE_WHEN_LOCKED_URI;
@@ -105,6 +106,7 @@
         itemInfo.bitmap = bitmapInfo;
         itemInfo.contentDescription = context.getResources().getString(
                 com.android.launcher3.R.string.ps_add_button_content_description);
+        itemInfo.runtimeStatusFlags |= FLAG_PRIVATE_SPACE_INSTALL_APP;
 
         BaseAllAppsAdapter.AdapterItem item = new BaseAllAppsAdapter.AdapterItem(VIEW_TYPE_ICON);
         item.itemInfo = itemInfo;
diff --git a/src/com/android/launcher3/logging/StatsLogManager.java b/src/com/android/launcher3/logging/StatsLogManager.java
index 45ff33b..f9b7495 100644
--- a/src/com/android/launcher3/logging/StatsLogManager.java
+++ b/src/com/android/launcher3/logging/StatsLogManager.java
@@ -713,6 +713,9 @@
         @UiEvent(doc = "User tapped on install to private space system shortcut.")
         LAUNCHER_PRIVATE_SPACE_INSTALL_SYSTEM_SHORTCUT_TAP(1565),
 
+        @UiEvent(doc = "User tapped private space install app button.")
+        LAUNCHER_PRIVATE_SPACE_INSTALL_APP_BUTTON_TAP(1605),
+
         @UiEvent(doc = "User attempted to create split screen with a widget")
         LAUNCHER_SPLIT_WIDGET_ATTEMPT(1604)
 
diff --git a/src/com/android/launcher3/model/PackageUpdatedTask.java b/src/com/android/launcher3/model/PackageUpdatedTask.java
index 069e96b..529a8f9 100644
--- a/src/com/android/launcher3/model/PackageUpdatedTask.java
+++ b/src/com/android/launcher3/model/PackageUpdatedTask.java
@@ -353,9 +353,17 @@
         }
 
         if (!removedPackages.isEmpty() || !removedComponents.isEmpty()) {
-            Predicate<ItemInfo> removeMatch = ItemInfoMatcher.ofPackages(removedPackages, mUser)
-                    .or(ItemInfoMatcher.ofComponents(removedComponents, mUser))
-                    .and(ItemInfoMatcher.ofItemIds(forceKeepShortcuts).negate());
+            // This predicate is used to mark an ItemInfo for removal if its package or component
+            // is marked for removal.
+            Predicate<ItemInfo> removeAppMatch =
+                    ItemInfoMatcher.ofPackages(removedPackages, mUser)
+                            .or(ItemInfoMatcher.ofComponents(removedComponents, mUser))
+                            .and(ItemInfoMatcher.ofItemIds(forceKeepShortcuts).negate());
+            // This predicate is used to mark an app pair for removal if it contains an app marked
+            // for removal.
+            Predicate<ItemInfo> removeAppPairMatch =
+                    ItemInfoMatcher.forAppPairMatch(removeAppMatch);
+            Predicate<ItemInfo> removeMatch = removeAppMatch.or(removeAppPairMatch);
             deleteAndBindComponentsRemoved(removeMatch,
                     "removed because the corresponding package or component is removed. "
                             + "mOp=" + mOp + " removedPackages=" + removedPackages.stream().collect(
diff --git a/src/com/android/launcher3/model/data/ItemInfoWithIcon.java b/src/com/android/launcher3/model/data/ItemInfoWithIcon.java
index 58b12b1..c8ab09c 100644
--- a/src/com/android/launcher3/model/data/ItemInfoWithIcon.java
+++ b/src/com/android/launcher3/model/data/ItemInfoWithIcon.java
@@ -122,6 +122,11 @@
     public static final int FLAG_ARCHIVED = 1 << 14;
 
     /**
+     * Flag indicating it's the Private Space Install App icon.
+     */
+    public static final int FLAG_PRIVATE_SPACE_INSTALL_APP = 1 << 15;
+
+    /**
      * Status associated with the system state of the underlying item. This is calculated every
      * time a new info is created and not persisted on the disk.
      */
diff --git a/src/com/android/launcher3/touch/ItemClickHandler.java b/src/com/android/launcher3/touch/ItemClickHandler.java
index ff8b381..369008d 100644
--- a/src/com/android/launcher3/touch/ItemClickHandler.java
+++ b/src/com/android/launcher3/touch/ItemClickHandler.java
@@ -19,6 +19,7 @@
 import static com.android.launcher3.LauncherConstants.ActivityCodes.REQUEST_BIND_PENDING_APPWIDGET;
 import static com.android.launcher3.LauncherConstants.ActivityCodes.REQUEST_RECONFIGURE_APPWIDGET;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_FOLDER_OPEN;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_PRIVATE_SPACE_INSTALL_APP_BUTTON_TAP;
 import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_DISABLED_BY_PUBLISHER;
 import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_DISABLED_LOCKED_USER;
 import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_DISABLED_QUIET_USER;
@@ -352,6 +353,12 @@
                     appInfo.getTargetComponent().getPackageName(), Process.myUserHandle());
         } else {
             intent = item.getIntent();
+            if (item instanceof AppInfo
+                    && (((ItemInfoWithIcon) item).runtimeStatusFlags
+                    & ItemInfoWithIcon.FLAG_PRIVATE_SPACE_INSTALL_APP) != 0) {
+                launcher.getStatsLogManager().logger().log(
+                        LAUNCHER_PRIVATE_SPACE_INSTALL_APP_BUTTON_TAP);
+            }
         }
         if (intent == null) {
             throw new IllegalArgumentException("Input must have a valid intent");
diff --git a/src/com/android/launcher3/util/ItemInfoMatcher.java b/src/com/android/launcher3/util/ItemInfoMatcher.java
index b6af314..3074111 100644
--- a/src/com/android/launcher3/util/ItemInfoMatcher.java
+++ b/src/com/android/launcher3/util/ItemInfoMatcher.java
@@ -70,6 +70,15 @@
     }
 
     /**
+     * Returns a matcher for items within app pairs.
+     */
+    public static Predicate<ItemInfo> forAppPairMatch(Predicate<ItemInfo> childOperator) {
+        Predicate<ItemInfo> isAppPair = info ->
+                info instanceof FolderInfo fi && fi.itemType == Favorites.ITEM_TYPE_APP_PAIR;
+        return isAppPair.and(forFolderMatch(childOperator));
+    }
+
+    /**
      * Returns a matcher for items with provided ids
      */
     public static Predicate<ItemInfo> ofItemIds(IntSet ids) {
diff --git a/src/com/android/launcher3/util/StartActivityParams.java b/src/com/android/launcher3/util/StartActivityParams.java
index b48562f..d66b0a0 100644
--- a/src/com/android/launcher3/util/StartActivityParams.java
+++ b/src/com/android/launcher3/util/StartActivityParams.java
@@ -52,6 +52,7 @@
     public int flagsValues;
     public int extraFlags;
     public Bundle options;
+    public boolean requireActivityResult = true;
 
     public StartActivityParams(Activity activity, int requestCode) {
         this(activity.createPendingResult(requestCode, new Intent(),
@@ -74,6 +75,7 @@
         flagsValues = parcel.readInt();
         extraFlags = parcel.readInt();
         options = parcel.readBundle();
+        requireActivityResult = parcel.readInt() != 0;
     }
 
 
@@ -94,6 +96,7 @@
         parcel.writeInt(flagsValues);
         parcel.writeInt(extraFlags);
         parcel.writeBundle(options);
+        parcel.writeInt(requireActivityResult ? 1 : 0);
     }
 
     /** Perform the operation on the pendingIntent. */
diff --git a/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java b/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java
index 54c9324..6656237 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java
@@ -56,7 +56,7 @@
     private static final int MAXIMUM_WIDTH_LEFT_PANE_FOLDABLE_DP = 395;
     private static final String SUGGESTIONS_PACKAGE_NAME = "widgets_list_suggestions_entry";
 
-    private LinearLayout mSuggestedWidgetsContainer;
+    private FrameLayout mSuggestedWidgetsContainer;
     private WidgetsListHeader mSuggestedWidgetsHeader;
     private LinearLayout mRightPane;
 
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
index 2596b75..daace8e 100644
--- a/tests/AndroidManifest.xml
+++ b/tests/AndroidManifest.xml
@@ -24,7 +24,7 @@
     <application android:debuggable="true">
         <uses-library android:name="android.test.runner" />
 
-        <receiver android:name="com.android.launcher3.compat.PromiseIconUiTest$UnarchiveBroadcastReceiver"
+        <receiver android:name="com.android.launcher3.compat.TaplPromiseIconUiTest$UnarchiveBroadcastReceiver"
                   android:enabled="true"
                   android:exported="true">
             <intent-filter>
diff --git a/tests/multivalentTests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/multivalentTests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index f68e12c..fef93b7 100644
--- a/tests/multivalentTests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/multivalentTests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -1771,6 +1771,7 @@
             }
             endTime = movePointer(
                     start, end, steps, false, downTime, downTime, slowDown, gestureScope);
+        } finally {
             if (mTrackpadGestureType != TrackpadGestureType.NONE) {
                 for (int i = mPointerCount; i >= 2; i--) {
                     sendPointer(downTime, downTime,
@@ -1778,7 +1779,6 @@
                             start, gestureScope);
                 }
             }
-        } finally {
             sendPointer(downTime, endTime, MotionEvent.ACTION_UP, end, gestureScope);
         }
     }
@@ -2055,11 +2055,14 @@
         final long downTime = SystemClock.uptimeMillis();
         sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN, targetCenter,
                 GestureScope.DONT_EXPECT_PILFER);
-        expectEvent(TestProtocol.SEQUENCE_MAIN, longClickEvent);
-        final UiObject2 result = waitForLauncherObject(resName);
-        sendPointer(downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, targetCenter,
-                GestureScope.DONT_EXPECT_PILFER);
-        return result;
+        try {
+            expectEvent(TestProtocol.SEQUENCE_MAIN, longClickEvent);
+            final UiObject2 result = waitForLauncherObject(resName);
+            return result;
+        } finally {
+            sendPointer(downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, targetCenter,
+                    GestureScope.DONT_EXPECT_PILFER);
+        }
     }
 
     @NonNull
@@ -2070,12 +2073,15 @@
         sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN, targetCenter,
                 GestureScope.DONT_EXPECT_PILFER, InputDevice.SOURCE_MOUSE,
                 /* isRightClick= */ true);
-        expectEvent(TestProtocol.SEQUENCE_MAIN, rightClickEvent);
-        final UiObject2 result = waitForLauncherObject(resName);
-        sendPointer(downTime, SystemClock.uptimeMillis(), ACTION_UP, targetCenter,
-                GestureScope.DONT_EXPECT_PILFER, InputDevice.SOURCE_MOUSE,
-                /* isRightClick= */ true);
-        return result;
+        try {
+            expectEvent(TestProtocol.SEQUENCE_MAIN, rightClickEvent);
+            final UiObject2 result = waitForLauncherObject(resName);
+            return result;
+        } finally {
+            sendPointer(downTime, SystemClock.uptimeMillis(), ACTION_UP, targetCenter,
+                    GestureScope.DONT_EXPECT_PILFER, InputDevice.SOURCE_MOUSE,
+                    /* isRightClick= */ true);
+        }
     }
 
     private static int getSystemIntegerRes(Context context, String resName) {
diff --git a/tests/multivalentTests/tapl/com/android/launcher3/tapl/WidgetResizeFrame.java b/tests/multivalentTests/tapl/com/android/launcher3/tapl/WidgetResizeFrame.java
index ec1cbd8..d0573e0 100644
--- a/tests/multivalentTests/tapl/com/android/launcher3/tapl/WidgetResizeFrame.java
+++ b/tests/multivalentTests/tapl/com/android/launcher3/tapl/WidgetResizeFrame.java
@@ -16,6 +16,7 @@
 package com.android.launcher3.tapl;
 
 import static com.android.launcher3.tapl.Launchable.DEFAULT_DRAG_STEPS;
+
 import static org.junit.Assert.assertTrue;
 
 import android.graphics.Point;
@@ -61,11 +62,14 @@
             final long downTime = SystemClock.uptimeMillis();
             mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN, targetStart,
                     LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER);
-            mLauncher.movePointer(targetStart, targetDest, DEFAULT_DRAG_STEPS,
-                    true, downTime, downTime, true,
-                    LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER);
-            mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_UP, targetDest,
-                    LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER);
+            try {
+                mLauncher.movePointer(targetStart, targetDest, DEFAULT_DRAG_STEPS,
+                        true, downTime, downTime, true,
+                        LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER);
+            } finally {
+                mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_UP, targetDest,
+                        LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER);
+            }
 
             try (LauncherInstrumentation.Closable c2 = mLauncher.addContextLayer(
                          "want to return resized widget resize frame")) {