Merge "Remove ENABLE_NO_LONG_PRESS_DRAG flag." into main
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index 2a1f39f..232c441 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -322,6 +322,7 @@
 
     <!-- Taskbar -->
     <dimen name="taskbar_size">@*android:dimen/taskbar_frame_height</dimen>
+    <dimen name="taskbar_phone_size">@*android:dimen/navigation_bar_frame_height</dimen>
     <dimen name="taskbar_ime_size">48dp</dimen>
     <dimen name="taskbar_icon_min_touch_size">48dp</dimen>
     <!-- Note that this applies to both sides of all icons, so visible space is double this. -->
diff --git a/quickstep/res/values/override.xml b/quickstep/res/values/override.xml
index df32626..29779a7 100644
--- a/quickstep/res/values/override.xml
+++ b/quickstep/res/values/override.xml
@@ -35,4 +35,6 @@
 
   <string name="assist_state_manager_class" translatable="false"></string>
 
+  <string name="launcher_restore_event_logger_class" translatable="false">com.android.quickstep.LauncherRestoreEventLoggerImpl</string>
+
 </resources>
diff --git a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
index b4754c6..159a6ef 100644
--- a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
@@ -204,6 +204,11 @@
     }
 
     @Override
+    public void onStateTransitionCompletedAfterSwipeToHome(LauncherState state) {
+        mTaskbarLauncherStateController.onStateTransitionCompletedAfterSwipeToHome(state);
+    }
+
+    @Override
     public void refreshResumedState() {
         onLauncherVisibilityChanged(mLauncher.hasBeenResumed());
     }
diff --git a/quickstep/src/com/android/launcher3/taskbar/StashedHandleViewController.java b/quickstep/src/com/android/launcher3/taskbar/StashedHandleViewController.java
index c4255bf..da1f766 100644
--- a/quickstep/src/com/android/launcher3/taskbar/StashedHandleViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/StashedHandleViewController.java
@@ -108,7 +108,7 @@
         DeviceProfile deviceProfile = mActivity.getDeviceProfile();
         Resources resources = mActivity.getResources();
         if (isPhoneGestureNavMode(mActivity.getDeviceProfile())) {
-            mTaskbarSize = resources.getDimensionPixelSize(R.dimen.taskbar_size);
+            mTaskbarSize = resources.getDimensionPixelSize(R.dimen.taskbar_phone_size);
             mStashedHandleWidth =
                     resources.getDimensionPixelSize(R.dimen.taskbar_stashed_small_screen);
         } else {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index 38ee4ac..4290948 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -885,7 +885,7 @@
 
         if (ENABLE_TASKBAR_NAVBAR_UNIFICATION && mDeviceProfile.isPhone) {
             return isThreeButtonNav() ?
-                    resources.getDimensionPixelSize(R.dimen.taskbar_size) :
+                    resources.getDimensionPixelSize(R.dimen.taskbar_phone_size) :
                     resources.getDimensionPixelSize(R.dimen.taskbar_stashed_size);
         }
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
index 9a37bcb..057b71b 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
@@ -205,13 +205,6 @@
                 public void onStateTransitionComplete(LauncherState finalState) {
                     mLauncherState = finalState;
                     updateStateForFlag(FLAG_LAUNCHER_IN_STATE_TRANSITION, false);
-                    // TODO(b/279514548) Cleans up bad state that can occur when user interacts with
-                    // taskbar on top of transparent activity.
-                    if (!FeatureFlags.enableHomeTransitionListener()
-                            && finalState == LauncherState.NORMAL
-                            && mLauncher.hasBeenResumed()) {
-                        updateStateForFlag(FLAG_VISIBLE, true);
-                    }
                     applyState();
                     boolean disallowLongClick =
                             FeatureFlags.enableSplitContextually()
@@ -223,6 +216,21 @@
                 }
             };
 
+    /**
+     * Callback for when launcher state transition completes after user swipes to home.
+     * @param finalState The final state of the transition.
+     */
+    public void onStateTransitionCompletedAfterSwipeToHome(LauncherState finalState) {
+        // TODO(b/279514548) Cleans up bad state that can occur when user interacts with
+        // taskbar on top of transparent activity.
+        if (!FeatureFlags.enableHomeTransitionListener()
+                && (finalState == LauncherState.NORMAL)
+                && mLauncher.hasBeenResumed()) {
+            updateStateForFlag(FLAG_VISIBLE, true);
+            applyState();
+        }
+    }
+
     /** Initializes the controller instance, and applies the initial state immediately. */
     public void init(TaskbarControllers controllers, QuickstepLauncher launcher,
             int sysuiStateFlags) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarSharedState.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarSharedState.java
index 1224b3f..d09f74c 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarSharedState.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarSharedState.java
@@ -86,4 +86,8 @@
     public void setTaskbarWasPinned(boolean taskbarWasPinned) {
         mTaskbarWasPinned = taskbarWasPinned;
     }
+
+    // To track if taskbar was stashed / unstashed between configuration changes (which recreates
+    // the task bar).
+    public Boolean taskbarWasStashedAuto = true;
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
index c74ddcb..9aaa80f 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
@@ -21,6 +21,7 @@
 import static com.android.app.animation.Interpolators.FINAL_FRAME;
 import static com.android.app.animation.Interpolators.INSTANT;
 import static com.android.app.animation.Interpolators.LINEAR;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_TASKBAR_NAVBAR_UNIFICATION;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TRANSIENT_TASKBAR_HIDE;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TRANSIENT_TASKBAR_SHOW;
 import static com.android.launcher3.taskbar.TaskbarKeyguardController.MASK_ANY_SYSUI_LOCKED;
@@ -256,7 +257,8 @@
         mAccessibilityManager = mActivity.getSystemService(AccessibilityManager.class);
 
         if (isPhoneMode()) {
-            mUnstashedHeight = mActivity.getResources().getDimensionPixelSize(R.dimen.taskbar_size);
+            mUnstashedHeight = mActivity.getResources().getDimensionPixelSize(
+                    R.dimen.taskbar_phone_size);
             mStashedHeight = mActivity.getResources().getDimensionPixelSize(
                     R.dimen.taskbar_stashed_size);
         } else {
@@ -306,8 +308,12 @@
 
         boolean isTransientTaskbar = DisplayController.isTransientTaskbar(mActivity);
         boolean isInSetup = !mActivity.isUserSetupComplete() || setupUIVisible;
-        updateStateForFlag(FLAG_STASHED_IN_APP_AUTO,
-                isTransientTaskbar && !mTaskbarSharedState.getTaskbarWasPinned());
+        boolean isStashedInAppAuto =
+                isTransientTaskbar && !mTaskbarSharedState.getTaskbarWasPinned();
+        if (ENABLE_TASKBAR_NAVBAR_UNIFICATION) {
+            isStashedInAppAuto = isStashedInAppAuto && mTaskbarSharedState.taskbarWasStashedAuto;
+        }
+        updateStateForFlag(FLAG_STASHED_IN_APP_AUTO, isStashedInAppAuto);
         updateStateForFlag(FLAG_STASHED_IN_APP_SETUP, isInSetup);
         updateStateForFlag(FLAG_IN_SETUP, isInSetup);
         updateStateForFlag(FLAG_STASHED_SMALL_SCREEN, isPhoneMode()
@@ -316,7 +322,8 @@
         // us that we're paused until a bit later. This avoids flickering upon recreating taskbar.
         updateStateForFlag(FLAG_IN_APP, true);
         applyState(/* duration = */ 0);
-        if (mTaskbarSharedState.getTaskbarWasPinned()) {
+        if (mTaskbarSharedState.getTaskbarWasPinned()
+                || !mTaskbarSharedState.taskbarWasStashedAuto) {
             tryStartTaskbarTimeout();
         }
         notifyStashChange(/* visible */ false, /* stashed */ isStashedInApp());
@@ -491,6 +498,7 @@
         }
 
         if (hasAnyFlag(FLAG_STASHED_IN_APP_AUTO) != stash) {
+            mTaskbarSharedState.taskbarWasStashedAuto = stash;
             updateStateForFlag(FLAG_STASHED_IN_APP_AUTO, stash);
             applyState();
         }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
index a29a25c..df2a43b 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
@@ -30,6 +30,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
+import com.android.launcher3.LauncherState;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.ItemInfoWithIcon;
@@ -330,6 +331,14 @@
     }
 
     /**
+     * Callback for when launcher state transition completes after user swipes to home.
+     * @param finalState The final state of the transition.
+     */
+    public void onStateTransitionCompletedAfterSwipeToHome(LauncherState finalState) {
+        // Overridden
+    }
+
+    /**
      * Refreshes the resumed state of this ui controller.
      */
     public void refreshResumedState() {}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
index 14ab471..c0cbd45 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
@@ -192,7 +192,7 @@
         mControllers = controllers;
         mTaskbarView.init(new TaskbarViewCallbacks());
         mTaskbarView.getLayoutParams().height = isPhoneMode(mActivity.getDeviceProfile())
-                ? mActivity.getResources().getDimensionPixelSize(R.dimen.taskbar_size)
+                ? mActivity.getResources().getDimensionPixelSize(R.dimen.taskbar_phone_size)
                 : mActivity.getDeviceProfile().taskbarHeight;
 
         mTaskbarIconScaleForStash.updateValue(1f);
diff --git a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsSlideInView.java b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsSlideInView.java
index 5ce2a7a..964d329 100644
--- a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsSlideInView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsSlideInView.java
@@ -222,7 +222,7 @@
         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
             mNoIntercept = !mAppsView.shouldContainerScroll(ev)
                     || getTopOpenViewWithType(
-                            mActivityContext, TYPE_ACCESSIBLE & ~TYPE_TASKBAR_OVERLAYS) != null;
+                            mActivityContext, TYPE_TOUCH_CONTROLLER_NO_INTERCEPT) != null;
         }
         return super.onControllerInterceptTouchEvent(ev);
     }
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index 14e258b..9438e00 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -707,6 +707,13 @@
     }
 
     @Override
+    public void onStateTransitionCompletedAfterSwipeToHome(LauncherState finalState) {
+        if (mTaskbarUIController != null) {
+            mTaskbarUIController.onStateTransitionCompletedAfterSwipeToHome(finalState);
+        }
+    }
+
+    @Override
     protected void onResume() {
         super.onResume();
 
diff --git a/quickstep/src/com/android/launcher3/uioverrides/flags/DeveloperOptionsUI.java b/quickstep/src/com/android/launcher3/uioverrides/flags/DeveloperOptionsUI.java
index 301fbe4..c1a85fa 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/flags/DeveloperOptionsUI.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/flags/DeveloperOptionsUI.java
@@ -28,6 +28,7 @@
 import static com.android.launcher3.LauncherPrefs.LONG_PRESS_NAV_HANDLE_HAPTIC_HINT_START_SCALE_PERCENT;
 import static com.android.launcher3.LauncherPrefs.LONG_PRESS_NAV_HANDLE_SLOP_PERCENTAGE;
 import static com.android.launcher3.LauncherPrefs.LONG_PRESS_NAV_HANDLE_TIMEOUT_MS;
+import static com.android.launcher3.LauncherPrefs.PRIVATE_SPACE_APPS;
 import static com.android.launcher3.settings.SettingsActivity.EXTRA_FRAGMENT_HIGHLIGHT_KEY;
 import static com.android.launcher3.uioverrides.plugins.PluginManagerWrapper.PLUGIN_CHANGED;
 import static com.android.launcher3.uioverrides.plugins.PluginManagerWrapper.pluginEnabledKey;
@@ -67,6 +68,7 @@
 import androidx.preference.SwitchPreference;
 
 import com.android.launcher3.ConstantItem;
+import com.android.launcher3.Flags;
 import com.android.launcher3.LauncherPrefs;
 import com.android.launcher3.R;
 import com.android.launcher3.config.FeatureFlags;
@@ -115,6 +117,9 @@
             addAllAppsFromOverviewCatergory();
         }
         addCustomLpnhCategory();
+        if (Flags.enablePrivateSpace()) {
+            addCustomPrivateAppsCategory();
+        }
     }
 
     private void filterPreferences(String query, PreferenceGroup pg) {
@@ -365,6 +370,12 @@
         }
     }
 
+    private void addCustomPrivateAppsCategory() {
+        PreferenceCategory category = newCategory("Apps in Private Space Config");
+        category.addPreference(createSeekBarPreference(
+                "Number of Apps to put in private region", 0, 100, 1, PRIVATE_SPACE_APPS));
+    }
+
     /**
      * Create a preference with text and a seek bar. Should be added to a PreferenceCategory.
      *
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java
index f6cd30a..82a9c05 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java
@@ -45,6 +45,7 @@
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.compat.AccessibilityManagerCompat;
 import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.statemanager.StateManager;
 import com.android.launcher3.touch.SingleAxisSwipeDetector;
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.TouchController;
@@ -194,7 +195,20 @@
             recentsView.switchToScreenshot(null,
                     () -> recentsView.finishRecentsAnimation(true /* toRecents */, null));
             if (mStartState.overviewUi) {
-                new OverviewToHomeAnim(mLauncher, () -> onSwipeInteractionCompleted(mEndState),
+                Runnable onReachedHome = () -> {
+                    StateManager.StateListener<LauncherState> listener =
+                            new StateManager.StateListener<>() {
+                                @Override
+                                public void onStateTransitionComplete(LauncherState finalState) {
+                                    mLauncher.onStateTransitionCompletedAfterSwipeToHome(
+                                            finalState);
+                                    mLauncher.getStateManager().removeStateListener(this);
+                                }
+                            };
+                    mLauncher.getStateManager().addStateListener(listener);
+                    onSwipeInteractionCompleted(mEndState);
+                };
+                new OverviewToHomeAnim(mLauncher, onReachedHome,
                         FeatureFlags.enableSplitContextually()
                                 ? mCancelSplitRunnable
                                 : null)
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
index 8cbf239..2c937b0 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
@@ -15,8 +15,7 @@
  */
 package com.android.launcher3.uioverrides.touchcontrollers;
 
-import static com.android.launcher3.AbstractFloatingView.TYPE_ACCESSIBLE;
-import static com.android.launcher3.AbstractFloatingView.TYPE_ALL_APPS_EDU;
+import static com.android.launcher3.AbstractFloatingView.TYPE_TOUCH_CONTROLLER_NO_INTERCEPT;
 import static com.android.launcher3.AbstractFloatingView.getTopOpenViewWithType;
 import static com.android.launcher3.LauncherState.ALL_APPS;
 import static com.android.launcher3.LauncherState.NORMAL;
@@ -84,7 +83,7 @@
                 return false;
             }
         }
-        if (getTopOpenViewWithType(mLauncher, TYPE_ACCESSIBLE | TYPE_ALL_APPS_EDU) != null) {
+        if (getTopOpenViewWithType(mLauncher, TYPE_TOUCH_CONTROLLER_NO_INTERCEPT) != null) {
             return false;
         }
         return true;
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
index 3d94857..19bfe06 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
@@ -15,7 +15,7 @@
  */
 package com.android.launcher3.uioverrides.touchcontrollers;
 
-import static com.android.launcher3.AbstractFloatingView.TYPE_ACCESSIBLE;
+import static com.android.launcher3.AbstractFloatingView.TYPE_TOUCH_CONTROLLER_NO_INTERCEPT;
 import static com.android.launcher3.LauncherAnimUtils.SUCCESS_TRANSITION_PROGRESS;
 import static com.android.launcher3.touch.SingleAxisSwipeDetector.DIRECTION_BOTH;
 
@@ -112,7 +112,8 @@
             // If we are already animating from a previous state, we can intercept.
             return true;
         }
-        if (AbstractFloatingView.getTopOpenViewWithType(mActivity, TYPE_ACCESSIBLE) != null) {
+        if (AbstractFloatingView.getTopOpenViewWithType(
+                mActivity, TYPE_TOUCH_CONTROLLER_NO_INTERCEPT) != null) {
             return false;
         }
         return isRecentsInteractive();
diff --git a/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java b/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java
index 9e58160..7c24ba8 100644
--- a/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java
+++ b/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java
@@ -23,6 +23,8 @@
 import static com.android.launcher3.BaseActivity.INVISIBLE_ALL;
 import static com.android.launcher3.BaseActivity.INVISIBLE_BY_PENDING_FLAGS;
 import static com.android.launcher3.BaseActivity.PENDING_INVISIBLE_BY_WALLPAPER_ANIMATION;
+import static com.android.launcher3.LauncherPrefs.TASKBAR_PINNING;
+import static com.android.launcher3.config.FeatureFlags.enableTaskbarPinning;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -52,6 +54,7 @@
 
 import com.android.internal.view.AppearanceRegion;
 import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.LauncherPrefs;
 import com.android.launcher3.QuickstepTransitionManager;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
@@ -294,8 +297,12 @@
         mBackTarget = appTarget;
         mInitialTouchPos.set(backEvent.getTouchX(), backEvent.getTouchY());
 
-        // TODO(b/218916755): Offset start rectangle in multiwindow mode.
         mStartRect.set(appTarget.windowConfiguration.getMaxBounds());
+        if (mLauncher.getDeviceProfile().isTaskbarPresent && enableTaskbarPinning()
+                && LauncherPrefs.get(mLauncher).get(TASKBAR_PINNING)) {
+            int insetBottom = mStartRect.bottom - appTarget.contentInsets.bottom;
+            mStartRect.set(mStartRect.left, mStartRect.top, mStartRect.right, insetBottom);
+        }
         mCurrentRect.set(mStartRect);
         addScrimLayer();
         mTransaction.apply();
diff --git a/quickstep/src/com/android/quickstep/LauncherRestoreEventLoggerImpl.kt b/quickstep/src/com/android/quickstep/LauncherRestoreEventLoggerImpl.kt
new file mode 100644
index 0000000..645ecf4
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/LauncherRestoreEventLoggerImpl.kt
@@ -0,0 +1,136 @@
+package com.android.quickstep
+
+import android.app.backup.BackupManager
+import android.app.backup.BackupRestoreEventLogger
+import android.app.backup.BackupRestoreEventLogger.BackupRestoreDataType
+import android.app.backup.BackupRestoreEventLogger.BackupRestoreError
+import android.content.Context
+import com.android.launcher3.Flags
+import com.android.launcher3.LauncherSettings.Favorites
+import com.android.launcher3.backuprestore.LauncherRestoreEventLogger
+
+/**
+ * Concrete implementation for wrapper to log Restore event metrics for both success and failure to
+ * restore Launcher workspace from a backup. This implementation accesses SystemApis so is only
+ * available to QuickStep/NexusLauncher.
+ */
+class LauncherRestoreEventLoggerImpl(val context: Context) : LauncherRestoreEventLogger() {
+    companion object {
+        const val TAG = "LauncherRestoreEventLoggerImpl"
+
+        // Generic type for any possible workspace items, when specific type is not known.
+        @BackupRestoreDataType private const val DATA_TYPE_LAUNCHER_ITEM = "launcher_item"
+        // Specific workspace item types, based off of Favorites Table.
+        @BackupRestoreDataType private const val DATA_TYPE_APPLICATION = "application"
+        @BackupRestoreDataType private const val DATA_TYPE_FOLDER = "folder"
+        @BackupRestoreDataType private const val DATA_TYPE_APPWIDGET = "widget"
+        @BackupRestoreDataType private const val DATA_TYPE_CUSTOM_APPWIDGET = "custom_widget"
+        @BackupRestoreDataType private const val DATA_TYPE_DEEP_SHORTCUT = "deep_shortcut"
+        @BackupRestoreDataType private const val DATA_TYPE_APP_PAIR = "app_pair"
+    }
+
+    private val backupManager: BackupManager = BackupManager(context)
+    private val restoreEventLogger: BackupRestoreEventLogger = backupManager.delayedRestoreLogger
+
+    /**
+     * For logging when multiple items of a given data type failed to restore.
+     *
+     * @param dataType The data type that was not restored.
+     * @param count the number of data items that were not restored.
+     * @param error error type for why the data was not restored.
+     */
+    override fun logLauncherItemsRestoreFailed(
+        @BackupRestoreDataType dataType: String,
+        count: Int,
+        @BackupRestoreError error: String?
+    ) {
+        if (Flags.enableLauncherBrMetrics()) {
+            restoreEventLogger.logItemsRestoreFailed(dataType, count, error)
+        }
+    }
+
+    /**
+     * For logging when multiple items of a given data type were successfully restored.
+     *
+     * @param dataType The data type that was restored.
+     * @param count the number of data items restored.
+     */
+    override fun logLauncherItemsRestored(@BackupRestoreDataType dataType: String, count: Int) {
+        if (Flags.enableLauncherBrMetrics()) {
+            restoreEventLogger.logItemsRestored(dataType, count)
+        }
+    }
+
+    /**
+     * Helper to log successfully restoring a single item from the Favorites table.
+     *
+     * @param favoritesId The id of the item type from [Favorites] that was restored.
+     */
+    override fun logSingleFavoritesItemRestored(favoritesId: Int) {
+        if (Flags.enableLauncherBrMetrics()) {
+            restoreEventLogger.logItemsRestored(favoritesIdToDataType(favoritesId), 1)
+        }
+    }
+
+    /**
+     * Helper to log a failure to restore a single item from the Favorites table.
+     *
+     * @param favoritesId The id of the item type from [Favorites] that was not restored.
+     * @param error error type for why the data was not restored.
+     */
+    override fun logSingleFavoritesItemRestoreFailed(
+        favoritesId: Int,
+        @BackupRestoreError error: String?
+    ) {
+        if (Flags.enableLauncherBrMetrics()) {
+            restoreEventLogger.logItemsRestoreFailed(favoritesIdToDataType(favoritesId), 1, error)
+        }
+    }
+
+    /**
+     * Helper to log a failure to restore items from the Favorites table.
+     *
+     * @param favoritesId The id of the item type from [Favorites] that was not restored.
+     * @param count number of items that failed to restore.
+     * @param error error type for why the data was not restored.
+     */
+    override fun logFavoritesItemsRestoreFailed(
+        favoritesId: Int,
+        count: Int,
+        @BackupRestoreError error: String?
+    ) {
+        if (Flags.enableLauncherBrMetrics()) {
+            restoreEventLogger.logItemsRestoreFailed(
+                favoritesIdToDataType(favoritesId),
+                count,
+                error
+            )
+        }
+    }
+
+    /**
+     * Uses the current [restoreEventLogger] to report its results to the [backupManager]. Use when
+     * done restoring items for Launcher.
+     */
+    override fun reportLauncherRestoreResults() {
+        if (Flags.enableLauncherBrMetrics()) {
+            backupManager.reportDelayedRestoreResult(restoreEventLogger)
+        }
+    }
+
+    /**
+     * Helper method to convert item types from [Favorites] to B&R data types for logging. Also to
+     * avoid direct usage of @BackupRestoreDataType which is protected under @SystemApi.
+     */
+    @BackupRestoreDataType
+    private fun favoritesIdToDataType(favoritesId: Int): String =
+        when (favoritesId) {
+            Favorites.ITEM_TYPE_APPLICATION -> DATA_TYPE_APPLICATION
+            Favorites.ITEM_TYPE_FOLDER -> DATA_TYPE_FOLDER
+            Favorites.ITEM_TYPE_APPWIDGET -> DATA_TYPE_APPWIDGET
+            Favorites.ITEM_TYPE_CUSTOM_APPWIDGET -> DATA_TYPE_CUSTOM_APPWIDGET
+            Favorites.ITEM_TYPE_DEEP_SHORTCUT -> DATA_TYPE_DEEP_SHORTCUT
+            Favorites.ITEM_TYPE_APP_PAIR -> DATA_TYPE_APP_PAIR
+            else -> DATA_TYPE_LAUNCHER_ITEM
+        }
+}
diff --git a/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java b/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java
index 69c15a5..c91ee81 100644
--- a/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java
+++ b/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java
@@ -25,6 +25,7 @@
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.app.Activity;
+import android.content.Context;
 import android.content.Intent;
 import android.content.SharedPreferences;
 import android.graphics.Insets;
@@ -86,6 +87,8 @@
     private boolean mIsFoldable;
     private boolean mOnAttachedToWindowPendingCreate;
 
+    @Nullable private Runnable mOnAttachedOnGlobalLayoutCallback = null;
+
     public static TutorialFragment newInstance(
             TutorialType tutorialType, boolean gestureComplete, boolean fromTutorialMenu) {
         TutorialFragment fragment = getFragmentForTutorialType(tutorialType, fromTutorialMenu);
@@ -349,13 +352,27 @@
                     new ViewTreeObserver.OnGlobalLayoutListener() {
                         @Override
                         public void onGlobalLayout() {
-                            changeController(mTutorialType);
+                            runOnAttached(() -> changeController(mTutorialType));
                             mRootView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                         }
                     });
         }
     }
 
+    private void runOnAttached(Runnable callback) {
+        mOnAttachedOnGlobalLayoutCallback = callback;
+        if (getContext() != null) {
+            onAttached();
+        }
+    }
+
+    private void onAttached() {
+        if (mOnAttachedOnGlobalLayoutCallback != null) {
+            mOnAttachedOnGlobalLayoutCallback.run();
+            mOnAttachedOnGlobalLayoutCallback = null;
+        }
+    }
+
     @Override
     public boolean onTouch(View view, MotionEvent motionEvent) {
         if (mTutorialController != null && !isGestureComplete()) {
@@ -378,6 +395,12 @@
     }
 
     @Override
+    public void onAttach(@NonNull Context context) {
+        super.onAttach(context);
+        onAttached();
+    }
+
+    @Override
     void onAttachedToWindow() {
         if (mEdgeBackGestureHandler == null) {
             mOnAttachedToWindowPendingCreate = true;
diff --git a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
index 5d8e53e..baaa062 100644
--- a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
+++ b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
@@ -427,9 +427,16 @@
             // conflict with layers that WM core positions (ie. the input consumers).  For shell
             // transitions, the animation leashes are reparented to an animation container so we
             // can bump layers as needed.
-            builder.setLayer(mDrawsBelowRecents
-                    ? Integer.MIN_VALUE + app.prefixOrderIndex
-                    : ENABLE_SHELL_TRANSITIONS ? Integer.MAX_VALUE : 0);
+            if (ENABLE_SHELL_TRANSITIONS) {
+                builder.setLayer(mDrawsBelowRecents
+                        ? Integer.MIN_VALUE + app.prefixOrderIndex
+                        // 1000 is an arbitrary number to give room for multiple layers.
+                        : Integer.MAX_VALUE - 1000 + app.prefixOrderIndex);
+            } else {
+                builder.setLayer(mDrawsBelowRecents
+                        ? Integer.MIN_VALUE + app.prefixOrderIndex
+                        : 0);
+            }
         }
     }
 
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java
index b42f055..66a880b 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskView.java
@@ -1035,9 +1035,6 @@
             anim.addListener(new AnimatorListenerAdapter() {
                 @Override
                 public void onAnimationStart(Animator animation) {
-                    if (!recentsView.showAsGrid()) {
-                        return;
-                    }
                     recentsView.runActionOnRemoteHandles(
                             (Consumer<RemoteTargetHandle>) remoteTargetHandle ->
                                     remoteTargetHandle
diff --git a/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java b/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
index eced5a9..8d54dce 100644
--- a/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
+++ b/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
@@ -144,7 +144,7 @@
                 .around(new TestStabilityRule())
                 .around(new NavigationModeSwitchRule(mLauncher))
                 .around(new FailureWatcher(mLauncher, viewCaptureRule::getViewCaptureData))
-                .around(viewCaptureRule)
+                // .around(viewCaptureRule) b/315482167
                 .around(new TestIsolationRule(mLauncher, false))
                 .around(setLauncherCommand);
 
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsTrackpad.java b/quickstep/tests/src/com/android/quickstep/TaplTestsTrackpad.java
index 0eec8b7..3465f23 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsTrackpad.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsTrackpad.java
@@ -86,7 +86,7 @@
             mLauncher.setTrackpadGestureType(TrackpadGestureType.THREE_FINGER);
 
             startTestActivity(2);
-            mLauncher.pressBack();
+            mLauncher.getLaunchedAppState().pressBackToWorkspace();
         } finally {
             instrumentation.getUiAutomation().dropShellPermissionIdentity();
         }
diff --git a/res/layout/widgets_two_pane_sheet_foldable.xml b/res/layout/widgets_two_pane_sheet_foldable.xml
deleted file mode 100644
index 93c0c70..0000000
--- a/res/layout/widgets_two_pane_sheet_foldable.xml
+++ /dev/null
@@ -1,131 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?><!-- 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.
--->
-<com.android.launcher3.widget.picker.WidgetsTwoPaneSheet
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:tools="http://schemas.android.com/tools"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
-    android:orientation="vertical"
-    android:theme="?attr/widgetsTheme">
-
-    <androidx.constraintlayout.widget.ConstraintLayout
-        android:id="@+id/container"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:focusable="true"
-        android:importantForAccessibility="no">
-
-        <View
-            android:id="@+id/collapse_handle"
-            android:gravity="center_horizontal"
-            android:layout_width="@dimen/bottom_sheet_handle_width"
-            android:layout_height="@dimen/bottom_sheet_handle_height"
-            android:layout_marginTop="@dimen/bottom_sheet_handle_margin"
-            app:layout_constraintTop_toTopOf="parent"
-            app:layout_constraintStart_toStartOf="parent"
-            app:layout_constraintEnd_toEndOf="parent"
-            android:background="@drawable/widget_picker_collapse_handle"/>
-
-        <TextView
-            android:id="@+id/title"
-            android:gravity="center_horizontal"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:layout_marginTop="24dp"
-            android:paddingHorizontal="@dimen/widget_list_horizontal_margin_two_pane"
-            android:text="@string/widget_button_text"
-            app:layout_constraintTop_toBottomOf="@id/collapse_handle"
-            app:layout_constraintStart_toStartOf="parent"
-            app:layout_constraintEnd_toEndOf="parent"
-            android:textColor="?attr/widgetPickerTitleColor"
-            android:textSize="24sp" />
-
-        <FrameLayout
-            android:id="@+id/recycler_view_container"
-            android:layout_width="0dp"
-            android:layout_height="0dp"
-            app:layout_constraintTop_toBottomOf="@id/title"
-            app:layout_constraintBottom_toBottomOf="parent"
-            app:layout_constraintStart_toStartOf="parent"
-            app:layout_constraintWidth_percent="0.33"
-            app:layout_constraintWidth_min="254dp"
-            app:layout_constraintWidth_max="395dp">
-            <TextView
-                android:id="@+id/fast_scroller_popup"
-                style="@style/FastScrollerPopup"
-                android:layout_marginEnd="@dimen/fastscroll_popup_margin" />
-
-            <!-- Fast scroller popup -->
-            <com.android.launcher3.views.RecyclerViewFastScroller
-                android:id="@+id/fast_scroller"
-                android:layout_width="@dimen/fastscroll_width"
-                android:layout_height="match_parent"
-                android:layout_marginEnd="@dimen/fastscroll_end_margin" />
-
-            <com.android.launcher3.widget.picker.WidgetsRecyclerView
-                android:id="@+id/search_widgets_list_view"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:clipToPadding="false"
-                android:paddingHorizontal="@dimen/widget_list_horizontal_margin_two_pane"
-                android:visibility="gone" />
-        </FrameLayout>
-
-        <FrameLayout
-            android:id="@+id/right_pane_container"
-            android:layout_width="0dp"
-            android:layout_height="0dp"
-            android:layout_marginEnd="@dimen/widget_list_horizontal_margin_two_pane"
-            android:paddingTop="@dimen/widget_list_horizontal_margin_two_pane"
-            app:layout_constraintTop_toBottomOf="@id/title"
-            app:layout_constraintBottom_toBottomOf="parent"
-            app:layout_constraintStart_toEndOf="@id/recycler_view_container"
-            app:layout_constraintEnd_toEndOf="parent">
-            <TextView
-                android:id="@+id/no_widgets_text"
-                style="@style/PrimaryHeadline"
-                android:layout_width="match_parent"
-                android:layout_height="match_parent"
-                android:gravity="center"
-                android:textSize="18sp"
-                android:visibility="gone"
-                tools:text="No widgets available" />
-            <ScrollView
-                android:id="@+id/right_pane_scroll_view"
-                android:layout_width="match_parent"
-                android:layout_height="match_parent"
-                android:fillViewport="true">
-                <LinearLayout
-                    android:orientation="vertical"
-                    android:layout_width="match_parent"
-                    android:layout_height="wrap_content"
-                    android:gravity="center_vertical"
-                    android:clipToOutline="true"
-                    android:paddingBottom="36dp"
-                    android:background="@drawable/widgets_surface_background"
-                    android:id="@+id/right_pane">
-                    <com.android.launcher3.widget.picker.WidgetsRecommendationTableLayout
-                        android:id="@+id/recommended_widget_table"
-                        android:layout_width="match_parent"
-                        android:layout_height="wrap_content"
-                        android:paddingHorizontal=
-                            "@dimen/widget_list_horizontal_margin_two_pane"
-                        android:visibility="gone" />
-                </LinearLayout>
-            </ScrollView>
-        </FrameLayout>
-    </androidx.constraintlayout.widget.ConstraintLayout>
-</com.android.launcher3.widget.picker.WidgetsTwoPaneSheet>
diff --git a/res/values/config.xml b/res/values/config.xml
index 154312a..2980635 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -86,6 +86,7 @@
     <string name="widget_holder_factory_class" translatable="false"></string>
     <string name="taskbar_search_session_controller_class" translatable="false"></string>
     <string name="taskbar_model_callbacks_factory_class" translatable="false"></string>
+    <string name="launcher_restore_event_logger_class" translatable="false"></string>
 
     <!-- View ID to use for QSB widget -->
     <item type="id" name="qsb_widget" />
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 242c439..0a57127 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -125,6 +125,9 @@
     <dimen name="all_apps_tip_bottom_margin">8dp</dimen>
     <dimen name="all_apps_height_extra">6dp</dimen>
     <dimen name="all_apps_paged_view_top_padding">40dp</dimen>
+    <dimen name="all_apps_recycler_view_decorator_padding">1dp</dimen>
+    <dimen name="all_apps_recycler_view_decorator_group_radius">28dp</dimen>
+    <dimen name="all_apps_recycler_view_decorator_result_radius">4dp</dimen>
 
     <dimen name="all_apps_icon_drawable_padding">8dp</dimen>
     <dimen name="all_apps_predicted_icon_vertical_padding">8dp</dimen>
@@ -364,6 +367,7 @@
 
     <!-- Taskbar related (placeholders to compile in Launcher3 without Quickstep) -->
     <dimen name="taskbar_size">0dp</dimen>
+    <dimen name="taskbar_phone_size">@*android:dimen/navigation_bar_frame_height</dimen>
     <dimen name="taskbar_stashed_size">0dp</dimen>
     <dimen name="qsb_widget_height">0dp</dimen>
     <dimen name="qsb_shadow_height">0dp</dimen>
diff --git a/src/com/android/launcher3/AbstractFloatingView.java b/src/com/android/launcher3/AbstractFloatingView.java
index d78afd3..f72c556 100644
--- a/src/com/android/launcher3/AbstractFloatingView.java
+++ b/src/com/android/launcher3/AbstractFloatingView.java
@@ -135,6 +135,10 @@
     public static final int TYPE_TASKBAR_OVERLAYS =
             TYPE_TASKBAR_ALL_APPS | TYPE_TASKBAR_EDUCATION_DIALOG;
 
+    // Floating views that a TouchController should not try to intercept touches from.
+    public static final int TYPE_TOUCH_CONTROLLER_NO_INTERCEPT = TYPE_ALL & ~TYPE_DISCOVERY_BOUNCE
+            & ~TYPE_LISTENER & ~TYPE_TASKBAR_OVERLAYS;
+
     public static final int TYPE_ALL_EXCEPT_ON_BOARD_POPUP = TYPE_ALL & ~TYPE_ON_BOARD_POPUP
             & ~TYPE_PIN_IME_POPUP;
 
diff --git a/src/com/android/launcher3/AppWidgetsRestoredReceiver.java b/src/com/android/launcher3/AppWidgetsRestoredReceiver.java
index 641fd83..429978e 100644
--- a/src/com/android/launcher3/AppWidgetsRestoredReceiver.java
+++ b/src/com/android/launcher3/AppWidgetsRestoredReceiver.java
@@ -9,9 +9,13 @@
 import android.content.Intent;
 import android.util.Log;
 
+import com.android.launcher3.logging.FileLog;
+import com.android.launcher3.provider.RestoreDbTask;
 import com.android.launcher3.util.IntArray;
 import com.android.launcher3.widget.LauncherWidgetHolder;
 
+import java.util.Arrays;
+
 public class AppWidgetsRestoredReceiver extends BroadcastReceiver {
 
     private static final String TAG = "AppWidgetsRestoredReceiver";
@@ -20,8 +24,11 @@
     public void onReceive(final Context context, Intent intent) {
         if (AppWidgetManager.ACTION_APPWIDGET_HOST_RESTORED.equals(intent.getAction())) {
             int hostId = intent.getIntExtra(AppWidgetManager.EXTRA_HOST_ID, 0);
-            Log.d(TAG, "Widget ID map received for host:" + hostId);
+            Log.d(TAG, "onReceive: Widget ID map received for host:" + hostId);
             if (hostId != LauncherWidgetHolder.APPWIDGET_HOST_ID) {
+                Log.w(TAG,  "onReceive: hostId does not match Launcher."
+                        + " Expected: " + LauncherWidgetHolder.APPWIDGET_HOST_ID
+                        + ", Actual: " + hostId);
                 return;
             }
 
@@ -31,8 +38,18 @@
                 LauncherPrefs.get(context).putSync(
                         OLD_APP_WIDGET_IDS.to(IntArray.wrap(oldIds).toConcatString()),
                         APP_WIDGET_IDS.to(IntArray.wrap(newIds).toConcatString()));
+                FileLog.d(TAG, "onReceive: Valid Widget IDs received."
+                        + " old IDs=" + Arrays.toString(oldIds)
+                        + ", new IDs=" + Arrays.toString(newIds));
+                if (!RestoreDbTask.isPending(context)) {
+                    FileLog.w(TAG, "onReceive: Restored App Widget Ids received but Launcher"
+                            + " restore is not pending. New widget Ids might not get restored.");
+                }
             } else {
-                Log.e(TAG, "Invalid host restored received");
+                Log.e(TAG, "onReceive: Invalid widget ids received for Launcher"
+                        + ", skipping restore of widget ids."
+                        + " newIds=" + Arrays.toString(newIds)
+                        + ", oldIds=" + Arrays.toString(oldIds));
             }
         }
     }
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index e2e528c..91da7e6 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -175,6 +175,7 @@
     @ViewDebug.ExportedProperty(category = "launcher")
     private DotInfo mDotInfo;
     private DotRenderer mDotRenderer;
+    private Locale mCurrentLocale;
     @ViewDebug.ExportedProperty(category = "launcher", deepExport = true)
     protected DotRenderer.DrawParams mDotParams;
     private Animator mDotScaleAnim;
@@ -250,6 +251,7 @@
 
         mDotParams = new DotRenderer.DrawParams();
 
+        mCurrentLocale = context.getResources().getConfiguration().locale;
         setEllipsize(TruncateAt.END);
         setAccessibilityDelegate(mActivity.getAccessibilityDelegate());
         setTextAlpha(1f);
@@ -411,10 +413,12 @@
      *  Only if actual text can be displayed in two line, the {@code true} value will be effective.
      */
     protected boolean shouldUseTwoLine() {
-        return ((FeatureFlags.enableTwolineAllapps())
-                && (mDisplay == DISPLAY_ALL_APPS || mDisplay == DISPLAY_PREDICTION_ROW))
-                || (FeatureFlags.ENABLE_TWOLINE_DEVICESEARCH.get()
-                && mDisplay == DISPLAY_SEARCH_RESULT);
+        return (FeatureFlags.enableTwolineAllapps() && isCurrentLanguageEnglish())
+                && (mDisplay == DISPLAY_ALL_APPS || mDisplay == DISPLAY_PREDICTION_ROW);
+    }
+
+    protected boolean isCurrentLanguageEnglish() {
+        return mCurrentLocale.equals(Locale.US);
     }
 
     @UiThread
diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index b6e8ec3..1ca7da9 100644
--- a/src/com/android/launcher3/DeviceProfile.java
+++ b/src/com/android/launcher3/DeviceProfile.java
@@ -356,7 +356,7 @@
         final Resources res = context.getResources();
         mMetrics = res.getDisplayMetrics();
 
-        mIconSizeSteps = mIsResponsiveGrid ? new IconSizeSteps(res) : null;
+        mIconSizeSteps = new IconSizeSteps(res);
 
         // Determine sizes.
         widthPx = windowBounds.bounds.width();
@@ -1486,6 +1486,17 @@
                 folderCellWidthPx = roundPxValueFromFloat(folderCellWidthPx * scale);
                 folderCellHeightPx = roundPxValueFromFloat(folderCellHeightPx * scale);
             }
+            // Recalculating padding and cell height
+            folderChildDrawablePaddingPx = getNormalizedFolderChildDrawablePaddingPx(textHeight);
+
+            CellContentDimensions cellContentDimensions = new CellContentDimensions(
+                    folderChildIconSizePx,
+                    folderChildDrawablePaddingPx,
+                    folderChildTextSizePx);
+            cellContentDimensions.resizeToFitCellHeight(folderCellHeightPx, mIconSizeSteps);
+            folderChildIconSizePx = cellContentDimensions.getIconSizePx();
+            folderChildDrawablePaddingPx = cellContentDimensions.getIconDrawablePaddingPx();
+            folderChildTextSizePx = cellContentDimensions.getIconTextSizePx();
 
             folderContentPaddingTop = roundPxValueFromFloat(folderContentPaddingTop * scale);
             folderCellLayoutBorderSpacePx = new Point(
@@ -1493,10 +1504,7 @@
                     roundPxValueFromFloat(folderCellLayoutBorderSpacePx.y * scale)
             );
             folderFooterHeightPx = roundPxValueFromFloat(folderFooterHeightPx * scale);
-
             folderContentPaddingLeftRight = folderCellLayoutBorderSpacePx.x;
-
-            folderChildDrawablePaddingPx = getNormalizedFolderChildDrawablePaddingPx(textHeight);
         } else {
             int cellPaddingX = (int) (res.getDimensionPixelSize(R.dimen.folder_cell_x_padding)
                     * scale);
diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java
index dfbbcaa..5721ed3 100644
--- a/src/com/android/launcher3/InvariantDeviceProfile.java
+++ b/src/com/android/launcher3/InvariantDeviceProfile.java
@@ -298,11 +298,15 @@
      * Reinitialize the current grid after a restore, where some grids might now be disabled.
      */
     public void reinitializeAfterRestore(Context context) {
-        FileLog.d(TAG, "Reinitializing grid after restore");
         String currentGridName = getCurrentGridName(context);
         String currentDbFile = dbFile;
         String newGridName = initGrid(context, currentGridName);
         String newDbFile = dbFile;
+        FileLog.d(TAG, "Reinitializing grid after restore."
+                + " currentGridName=" + currentGridName
+                + ", currentDbFile=" + currentDbFile
+                + ", newGridName=" + newGridName
+                + ", newDbFile=" + newDbFile);
         if (!newDbFile.equals(currentDbFile)) {
             FileLog.d(TAG, "Restored grid is disabled : " + currentGridName
                     + ", migrating to: " + newGridName
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 5adfd43..e41a8a5 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -2905,6 +2905,14 @@
         // Overridden; move this into ActivityContext if necessary for Taskbar
     }
 
+    /**
+     * Callback for when launcher state transition completes after user swipes to home.
+     * @param finalState The final state of the transition.
+     */
+    public void onStateTransitionCompletedAfterSwipeToHome(LauncherState finalState) {
+        // Overridden
+    }
+
     @Override
     public void returnToHomescreen() {
         super.returnToHomescreen();
diff --git a/src/com/android/launcher3/LauncherPrefs.kt b/src/com/android/launcher3/LauncherPrefs.kt
index a05b0f5..78056e6 100644
--- a/src/com/android/launcher3/LauncherPrefs.kt
+++ b/src/com/android/launcher3/LauncherPrefs.kt
@@ -364,6 +364,13 @@
                         EncryptionType.MOVE_TO_DEVICE_PROTECTED
                 )
         @JvmField
+        val PRIVATE_SPACE_APPS =
+                nonRestorableItem(
+                        "pref_private_space_apps",
+                        0,
+                        EncryptionType.MOVE_TO_DEVICE_PROTECTED
+                )
+        @JvmField
         val THEMED_ICONS =
             backedUpItem(Themes.KEY_THEMED_ICONS, false, EncryptionType.MOVE_TO_DEVICE_PROTECTED)
         @JvmField val PROMISE_ICON_IDS = backedUpItem(InstallSessionHelper.PROMISE_ICON_IDS, "")
diff --git a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
index e5a223a..7f1d216 100644
--- a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
@@ -407,7 +407,7 @@
             // If exiting search, revert predictive back scale on all apps
             mAllAppsTransitionController.animateAllAppsToNoScale();
         }
-        mSearchTransitionController.animateToSearchState(goingToSearch, durationMs,
+        mSearchTransitionController.animateToState(goingToSearch, durationMs,
                 /* onEndRunnable = */ () -> {
                     mIsSearching = goingToSearch;
                     updateSearchResultsVisibility();
diff --git a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
index b0f13ef..36a44cc 100644
--- a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
@@ -36,7 +36,11 @@
 import android.graphics.Canvas;
 import android.util.AttributeSet;
 import android.util.Log;
+import android.view.View;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.util.Consumer;
 import androidx.recyclerview.widget.RecyclerView;
 
 import com.android.launcher3.DeviceProfile;
@@ -57,6 +61,7 @@
     protected static final String TAG = "AllAppsRecyclerView";
     private static final boolean DEBUG = false;
     private static final boolean DEBUG_LATENCY = Utilities.isPropertyEnabled(SEARCH_LOGGING);
+    private Consumer<View> mChildAttachedConsumer;
 
     protected final int mNumAppsPerRow;
     private final AllAppsFastScrollHelper mFastScrollHelper;
@@ -282,6 +287,22 @@
         }
     }
 
+    /**
+     * This will be called just before a new child is attached to the window. Passing in null will
+     * remove the consumer.
+     */
+    protected void setChildAttachedConsumer(@Nullable Consumer<View> childAttachedConsumer) {
+        mChildAttachedConsumer = childAttachedConsumer;
+    }
+
+    @Override
+    public void onChildAttachedToWindow(@NonNull View child) {
+        if (mChildAttachedConsumer != null) {
+            mChildAttachedConsumer.accept(child);
+        }
+        super.onChildAttachedToWindow(child);
+    }
+
     @Override
     public int getScrollBarTop() {
         return ActivityContext.lookupContext(getContext()).getAppsView().isSearchSupported()
diff --git a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
index 328516e..1782791 100644
--- a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
+++ b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
@@ -15,6 +15,10 @@
  */
 package com.android.launcher3.allapps;
 
+import static com.android.launcher3.allapps.SectionDecorationInfo.ROUND_BOTTOM_LEFT;
+import static com.android.launcher3.allapps.SectionDecorationInfo.ROUND_BOTTOM_RIGHT;
+import static com.android.launcher3.allapps.SectionDecorationInfo.ROUND_NOTHING;
+
 import android.content.Context;
 
 import androidx.annotation.Nullable;
@@ -318,6 +322,10 @@
                 case PrivateProfileManager.STATE_ENABLED:
                     // Add PS Apps only in Enabled State.
                     addAppsWithSections(mPrivateApps, position);
+                    if (mActivityContext.getAppsView() != null) {
+                        mActivityContext.getAppsView().getActiveRecyclerView()
+                                .scrollToBottomWithMotion();
+                    }
                     break;
             }
         }
@@ -325,8 +333,34 @@
 
     private void addAppsWithSections(List<AppInfo> appList, int startPosition) {
         String lastSectionName = null;
+        boolean hasPrivateApps = false;
+        if (mPrivateProviderManager != null) {
+            hasPrivateApps = appList.stream().
+                    allMatch(mPrivateProviderManager.getItemInfoMatcher());
+        }
+        int privateAppCount = 0;
+        int numberOfColumns = mActivityContext.getDeviceProfile().numShownAllAppsColumns;
+        int numberOfAppRows = (int) Math.ceil((double) appList.size() / numberOfColumns);
         for (AppInfo info : appList) {
-            mAdapterItems.add(AdapterItem.asApp(info));
+            // Apply decorator to private apps.
+            if (hasPrivateApps) {
+                int roundRegion = ROUND_NOTHING;
+                if ((privateAppCount / numberOfColumns) == numberOfAppRows - 1) {
+                    if ((privateAppCount % numberOfColumns) == 0) {
+                        // App is the first column
+                        roundRegion = ROUND_BOTTOM_LEFT;
+                    } else if ((privateAppCount % numberOfColumns) == numberOfColumns-1) {
+                        roundRegion = ROUND_BOTTOM_RIGHT;
+                    }
+                }
+                mAdapterItems.add(AdapterItem.asAppWithDecorationInfo(info,
+                        new SectionDecorationInfo(mActivityContext.getApplicationContext(),
+                                roundRegion,
+                                true /* decorateTogether */)));
+                privateAppCount += 1;
+            } else {
+                mAdapterItems.add(AdapterItem.asApp(info));
+            }
 
             String sectionName = info.sectionName;
             // Create a new section if the section names do not match
diff --git a/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java b/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java
index 5e26ea5..5eeb259 100644
--- a/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java
+++ b/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java
@@ -15,6 +15,12 @@
  */
 package com.android.launcher3.allapps;
 
+import static com.android.launcher3.allapps.SectionDecorationInfo.ROUND_BOTTOM_LEFT;
+import static com.android.launcher3.allapps.SectionDecorationInfo.ROUND_BOTTOM_RIGHT;
+import static com.android.launcher3.allapps.SectionDecorationInfo.ROUND_TOP_LEFT;
+import static com.android.launcher3.allapps.SectionDecorationInfo.ROUND_TOP_RIGHT;
+import static com.android.launcher3.allapps.UserProfileManager.STATE_DISABLED;
+
 import android.content.Context;
 import android.view.LayoutInflater;
 import android.view.View;
@@ -25,6 +31,7 @@
 import android.widget.RelativeLayout;
 import android.widget.TextView;
 
+import androidx.annotation.Nullable;
 import androidx.recyclerview.widget.RecyclerView;
 
 import com.android.launcher3.BubbleTextView;
@@ -92,7 +99,8 @@
         public int rowAppIndex;
         // The associated ItemInfoWithIcon for the item
         public AppInfo itemInfo = null;
-
+        // Private App Decorator
+        public SectionDecorationInfo decorationInfo = null;
         public AdapterItem(int viewType) {
             this.viewType = viewType;
         }
@@ -106,6 +114,13 @@
             return item;
         }
 
+        public static AdapterItem asAppWithDecorationInfo(AppInfo appInfo,
+                SectionDecorationInfo decorationInfo) {
+            AdapterItem item = asApp(appInfo);
+            item.decorationInfo = decorationInfo;
+            return item;
+        }
+
         protected boolean isCountedForAccessibility() {
             return viewType == VIEW_TYPE_ICON;
         }
@@ -125,9 +140,17 @@
             return itemInfo == null && other.itemInfo == null;
         }
 
-        /** Sets the alpha of the decorator for this item. Returns true if successful. */
-        public boolean setDecorationFillAlpha(int alpha) {
-            return false;
+        @Nullable
+        public SectionDecorationInfo getDecorationInfo() {
+            return decorationInfo;
+        }
+
+        /** Sets the alpha of the decorator for this item. */
+        protected void setDecorationFillAlpha(int alpha) {
+            if (decorationInfo == null || decorationInfo.getDecorationHandler() == null) {
+                return;
+            }
+            decorationInfo.getDecorationHandler().setFillAlpha(alpha);
         }
     }
 
@@ -249,6 +272,15 @@
                 assert mPrivateSpaceHeaderViewController != null;
                 assert psHeaderLayout != null;
                 mPrivateSpaceHeaderViewController.addPrivateSpaceHeaderViewElements(psHeaderLayout);
+                AdapterItem adapterItem = mApps.getAdapterItems().get(position);
+                int roundRegions = ROUND_TOP_LEFT | ROUND_TOP_RIGHT;
+                if (mPrivateSpaceHeaderViewController.getPrivateProfileManager().getCurrentState()
+                        == STATE_DISABLED) {
+                    roundRegions |= (ROUND_BOTTOM_LEFT | ROUND_BOTTOM_RIGHT);
+                }
+                adapterItem.decorationInfo =
+                        new SectionDecorationInfo(mActivityContext, roundRegions,
+                                false /* decorateTogether */);
                 break;
             case VIEW_TYPE_ALL_APPS_DIVIDER:
             case VIEW_TYPE_WORK_DISABLED_CARD:
diff --git a/src/com/android/launcher3/allapps/PrivateAppsSectionDecorator.java b/src/com/android/launcher3/allapps/PrivateAppsSectionDecorator.java
index f4ed754..8712b84 100644
--- a/src/com/android/launcher3/allapps/PrivateAppsSectionDecorator.java
+++ b/src/com/android/launcher3/allapps/PrivateAppsSectionDecorator.java
@@ -16,97 +16,55 @@
 
 package com.android.launcher3.allapps;
 
-import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_ICON;
-import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_PRIVATE_SPACE_HEADER;
-
-import android.content.Context;
 import android.graphics.Canvas;
-import android.graphics.Paint;
-import android.graphics.Path;
-import android.graphics.RectF;
 import android.view.View;
 
-import androidx.core.content.ContextCompat;
 import androidx.recyclerview.widget.RecyclerView;
 
-import com.android.launcher3.R;
-import com.android.launcher3.pm.UserCache;
-import com.android.launcher3.views.ActivityContext;
+import java.util.HashMap;
 
 /**
  * Decorator which changes the background color for Private Space Icon Rows in AllAppsContainer.
  */
 public class PrivateAppsSectionDecorator extends RecyclerView.ItemDecoration {
 
-    private final Path mTmpPath = new Path();
-    private final RectF mTmpRect = new RectF();
-    private final Context mContext;
+    private static final String PRIVATE_APP_SECTION = "private_apps";
     private final AlphabeticalAppsList<?> mAppsList;
-    private final UserCache mUserCache;
-    private final Paint mPaint;
-    private final int mCornerRadius;
 
-    public PrivateAppsSectionDecorator(Context context, AlphabeticalAppsList<?> appsList) {
-        mContext = context;
+    public PrivateAppsSectionDecorator(AlphabeticalAppsList<?> appsList) {
         mAppsList = appsList;
-        mUserCache = UserCache.getInstance(context);
-        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
-        mPaint.setColor(ContextCompat.getColor(context,
-                R.color.material_color_surface_container_high));
-        mCornerRadius = context.getResources().getDimensionPixelSize(
-                R.dimen.ps_container_corner_radius);
     }
 
     /** Decorates Private Space Header and Icon Rows to give the shape of a container. */
     @Override
     public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
-        mTmpPath.reset();
-        mTmpRect.setEmpty();
-        int numCol = ActivityContext.lookupContext(mContext).getDeviceProfile()
-                .numShownAllAppsColumns;
+        HashMap<String, SectionDecorationHandler.UnionDecorationHandler> deferredDecorations =
+                new HashMap<>();
         for (int i = 0; i < parent.getChildCount(); i++) {
             View view = parent.getChildAt(i);
             int position = parent.getChildAdapterPosition(view);
             BaseAllAppsAdapter.AdapterItem adapterItem = mAppsList.getAdapterItems().get(position);
-            // Rectangle that covers the bottom half of the PS Header View when Space is unlocked.
-            if (adapterItem.viewType == VIEW_TYPE_PRIVATE_SPACE_HEADER) {
-                // We flatten the bottom corners of the rectangle, so that it merges with
-                // the private space app row decorator.
-                mTmpRect.set(
-                        view.getLeft(),
-                        view.getTop() + (float) (view.getBottom() - view.getTop()) / 2,
-                        view.getRight(),
-                        view.getBottom());
-                mTmpPath.addRect(mTmpRect, Path.Direction.CW);
-                c.drawPath(mTmpPath, mPaint);
-            } else if (adapterItem.viewType == VIEW_TYPE_ICON
-                    && mUserCache.getUserInfo(adapterItem.itemInfo.user).isPrivate()
-                    // No decoration for any private space app icon other than those at first row.
-                    && adapterItem.rowAppIndex == 0) {
-                c.drawPath(getPrivateAppRowPath(parent, view, position, numCol), mPaint);
+            SectionDecorationInfo info = adapterItem.decorationInfo;
+            if (info == null) {
+                continue;
+            }
+            SectionDecorationHandler decorationHandler = info.getDecorationHandler();
+            if (info.shouldDecorateItemsTogether()) {
+                SectionDecorationHandler.UnionDecorationHandler unionHandler =
+                        deferredDecorations.getOrDefault(
+                                PRIVATE_APP_SECTION,
+                                new SectionDecorationHandler.UnionDecorationHandler(
+                                        decorationHandler, parent.getPaddingLeft(),
+                                        parent.getPaddingRight()));
+                unionHandler.addChild(decorationHandler, view, true /* applyBackground */);
+                deferredDecorations.put(PRIVATE_APP_SECTION, unionHandler);
+            } else {
+                decorationHandler.onFocusDraw(c, view);
             }
         }
-    }
-
-    /** Returns the path to be decorated for Private Space App Row */
-    private Path getPrivateAppRowPath(RecyclerView parent, View iconView, int adapterPosition,
-            int numCol) {
-        // We always decorate the entire app row here.
-        // As the iconView just represents the first icon of the row, we get the right margin of
-        // our decorator using the parent view.
-        mTmpRect.set(iconView.getLeft(),
-                iconView.getTop(),
-                parent.getRight() - parent.getPaddingRight(),
-                iconView.getBottom());
-        // Decorates last app row with rounded bottom corners.
-        if (adapterPosition + numCol >= mAppsList.getAdapterItems().size()) {
-            float[] mCornersBot = new float[]{0, 0, 0, 0, mCornerRadius, mCornerRadius,
-                    mCornerRadius, mCornerRadius};
-            mTmpPath.addRoundRect(mTmpRect, mCornersBot, Path.Direction.CW);
-        } else {
-            // Decorate other rows as a plain rectangle
-            mTmpPath.addRect(mTmpRect, Path.Direction.CW);
+        for (SectionDecorationHandler.UnionDecorationHandler decorationHandler
+                : deferredDecorations.values()) {
+            decorationHandler.onGroupDecorate(c);
         }
-        return mTmpPath;
     }
 }
diff --git a/src/com/android/launcher3/allapps/PrivateProfileManager.java b/src/com/android/launcher3/allapps/PrivateProfileManager.java
index 334d5c1..693681b 100644
--- a/src/com/android/launcher3/allapps/PrivateProfileManager.java
+++ b/src/com/android/launcher3/allapps/PrivateProfileManager.java
@@ -30,6 +30,7 @@
 
 import androidx.annotation.VisibleForTesting;
 
+import com.android.launcher3.Flags;
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.pm.UserCache;
 import com.android.launcher3.util.Preconditions;
@@ -47,6 +48,7 @@
     private static final String SAFETY_CENTER_INTENT = Intent.ACTION_SAFETY_CENTER;
     private static final String PS_SETTINGS_FRAGMENT_KEY = ":settings:fragment_args_key";
     private static final String PS_SETTINGS_FRAGMENT_VALUE = "AndroidPrivateSpace_personal";
+    private static final int ANIMATION_DURATION = 2000;
     private final ActivityAllAppsContainerView<?> mAllApps;
     private final Predicate<UserHandle> mPrivateProfileMatcher;
     private PrivateAppsSectionDecorator mPrivateAppsSectionDecorator;
@@ -125,7 +127,6 @@
             // Create a new decorator instance if not already available.
             if (mPrivateAppsSectionDecorator == null) {
                 mPrivateAppsSectionDecorator = new PrivateAppsSectionDecorator(
-                        mAllApps.mActivityContext,
                         mainAdapterHolder.mAppsList);
             }
             for (int i = 0; i < mainAdapterHolder.mRecyclerView.getItemDecorationCount(); i++) {
@@ -137,6 +138,13 @@
             }
             // Add Private Space Decorator to the Recycler view.
             mainAdapterHolder.mRecyclerView.addItemDecoration(mPrivateAppsSectionDecorator);
+            if (Flags.privateSpaceAnimation() && mAllApps.getActiveRecyclerView()
+                    == mainAdapterHolder.mRecyclerView) {
+                RecyclerViewAnimationController recyclerViewAnimationController =
+                        new RecyclerViewAnimationController(mAllApps);
+                recyclerViewAnimationController.animateToState(true /* expand */,
+                        ANIMATION_DURATION, () -> {});
+            }
         } else {
             // Remove Private Space Decorator from the Recycler view.
             if (mPrivateAppsSectionDecorator != null) {
diff --git a/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewController.java b/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewController.java
index e0ca947..568ce32 100644
--- a/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewController.java
+++ b/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewController.java
@@ -105,4 +105,8 @@
             transitionImage.setVisibility(View.GONE);
         }
     }
+
+    PrivateProfileManager getPrivateProfileManager() {
+        return mPrivateProfileManager;
+    }
 }
diff --git a/src/com/android/launcher3/allapps/RecyclerViewAnimationController.java b/src/com/android/launcher3/allapps/RecyclerViewAnimationController.java
new file mode 100644
index 0000000..6209393
--- /dev/null
+++ b/src/com/android/launcher3/allapps/RecyclerViewAnimationController.java
@@ -0,0 +1,309 @@
+/*
+ * 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.allapps;
+
+import static androidx.recyclerview.widget.RecyclerView.NO_POSITION;
+
+import static com.android.app.animation.Interpolators.DECELERATE_1_7;
+import static com.android.app.animation.Interpolators.INSTANT;
+import static com.android.app.animation.Interpolators.clampToProgress;
+import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
+import static com.android.launcher3.anim.AnimatorListeners.forEndCallback;
+import static com.android.launcher3.anim.AnimatorListeners.forSuccessCallback;
+
+import android.animation.ObjectAnimator;
+import android.animation.TimeInterpolator;
+import android.graphics.drawable.Drawable;
+import android.util.FloatProperty;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.Interpolator;
+
+import com.android.launcher3.BubbleTextView;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.model.data.ItemInfo;
+
+import java.util.List;
+
+public class RecyclerViewAnimationController {
+
+    private static final String LOG_TAG = "AnimationCtrl";
+
+    /**
+     * These values represent points on the [0, 1] animation progress spectrum. They are used to
+     * animate items in the {@link SearchRecyclerView} and private space container in
+     * {@link AllAppsRecyclerView}.
+     */
+    protected static final float TOP_CONTENT_FADE_PROGRESS_START = 0.133f;
+    protected static final float CONTENT_FADE_PROGRESS_DURATION = 0.083f;
+    protected static final float TOP_BACKGROUND_FADE_PROGRESS_START = 0.633f;
+    protected static final float BACKGROUND_FADE_PROGRESS_DURATION = 0.15f;
+    // Progress before next item starts fading.
+    protected static final float CONTENT_STAGGER = 0.01f;
+
+    protected static final FloatProperty<RecyclerViewAnimationController> PROGRESS =
+            new FloatProperty<RecyclerViewAnimationController>("expansionProgress") {
+                @Override
+                public Float get(RecyclerViewAnimationController controller) {
+                    return controller.getAnimationProgress();
+                }
+
+                @Override
+                public void setValue(RecyclerViewAnimationController controller, float progress) {
+                    controller.setAnimationProgress(progress);
+                }
+            };
+
+    protected final ActivityAllAppsContainerView<?> mAllAppsContainerView;
+    protected ObjectAnimator mAnimator = null;
+    private float mAnimatorProgress = 1f;
+
+    public RecyclerViewAnimationController(ActivityAllAppsContainerView<?> allAppsContainerView) {
+        mAllAppsContainerView = allAppsContainerView;
+    }
+
+    /**
+     * Updates the children views of the current recyclerView based on the current animation
+     * progress.
+     *
+     * @return the total height of animating views (may exclude at most one row of app icons
+     * depending on which recyclerView is being acted upon).
+     */
+    protected int onProgressUpdated(float expansionProgress) {
+        int numItemsAnimated = 0;
+        int totalHeight = 0;
+        int appRowHeight = 0;
+        boolean appRowComplete = false;
+        Integer top = null;
+        AllAppsRecyclerView allAppsRecyclerView = getRecyclerView();
+
+        for (int i = 0; i < allAppsRecyclerView.getChildCount(); i++) {
+            View currentView = allAppsRecyclerView.getChildAt(i);
+            if (currentView == null) {
+                continue;
+            }
+            if (top == null) {
+                top = currentView.getTop();
+            }
+            int adapterPosition = allAppsRecyclerView.getChildAdapterPosition(currentView);
+            List<BaseAllAppsAdapter.AdapterItem> allAppsAdapters = allAppsRecyclerView.getApps()
+                    .getAdapterItems();
+            if (adapterPosition < 0 || adapterPosition >= allAppsAdapters.size()) {
+                continue;
+            }
+            BaseAllAppsAdapter.AdapterItem adapterItemAtPosition =
+                    allAppsAdapters.get(adapterPosition);
+            int spanIndex = getSpanIndex(allAppsRecyclerView, adapterPosition);
+            appRowComplete |= appRowHeight > 0 && spanIndex == 0;
+
+            float backgroundAlpha = 1f;
+            boolean hasDecorationInfo = adapterItemAtPosition.getDecorationInfo() != null;
+            boolean shouldAnimate = shouldAnimate(currentView, hasDecorationInfo, appRowComplete);
+
+            if (shouldAnimate) {
+                if (spanIndex > 0) {
+                    // Animate this item with the previous item on the same row.
+                    numItemsAnimated--;
+                }
+                // Adjust background (or decorator) alpha based on start progress and stagger.
+                backgroundAlpha = getAdjustedBackgroundAlpha(numItemsAnimated);
+            }
+
+            Drawable background = currentView.getBackground();
+            if (background != null && currentView instanceof ViewGroup currentViewGroup) {
+                currentView.setAlpha(1f);
+                // Apply content alpha to each child, since the view needs to be fully opaque for
+                // the background to show properly.
+                for (int j = 0; j < currentViewGroup.getChildCount(); j++) {
+                    setViewAdjustedContentAlpha(currentViewGroup.getChildAt(j), numItemsAnimated,
+                            shouldAnimate);
+                }
+
+                // Apply background alpha to the background drawable directly.
+                background.setAlpha((int) (255 * backgroundAlpha));
+            } else {
+                // Adjust content alpha based on start progress and stagger.
+                setViewAdjustedContentAlpha(currentView, numItemsAnimated, shouldAnimate);
+
+                // Apply background alpha to decorator if possible.
+                setAdjustedAdapterItemDecorationBackgroundAlpha(
+                        allAppsRecyclerView.getApps().getAdapterItems().get(adapterPosition),
+                        numItemsAnimated);
+
+                // Apply background alpha to view's background (e.g. for Search Edu card).
+                if (background != null) {
+                    background.setAlpha((int) (255 * backgroundAlpha));
+                }
+            }
+
+            float scaleY = 1;
+            if (shouldAnimate) {
+                scaleY = 1 - getAnimationProgress();
+                // Update number of search results that has been animated.
+                numItemsAnimated++;
+            }
+            int scaledHeight = (int) (currentView.getHeight() * scaleY);
+            currentView.setScaleY(scaleY);
+
+            // For rows with multiple elements, only count the height once and translate elements to
+            // the same y position.
+            int y = top + totalHeight;
+            if (spanIndex > 0) {
+                // Continuation of an existing row; move this item into the row.
+                y -= scaledHeight;
+            } else {
+                // Start of a new row contributes to total height.
+                totalHeight += scaledHeight;
+                if (!shouldAnimate) {
+                    appRowHeight = scaledHeight;
+                }
+            }
+            currentView.setY(y);
+        }
+        return totalHeight - appRowHeight;
+    }
+
+    protected void animateToState(boolean expand, long duration, Runnable onEndRunnable) {
+        float targetProgress = expand ? 0 : 1;
+        if (mAnimator != null) {
+            mAnimator.cancel();
+        }
+        mAnimator = ObjectAnimator.ofFloat(this, PROGRESS, targetProgress);
+
+        TimeInterpolator timeInterpolator = getInterpolator();
+        if (timeInterpolator == INSTANT) {
+            duration = 0;
+        }
+
+        mAnimator.addListener(forEndCallback(() -> mAnimator = null));
+        mAnimator.setDuration(duration).setInterpolator(timeInterpolator);
+        mAnimator.addListener(forSuccessCallback(onEndRunnable));
+        mAnimator.start();
+        getRecyclerView().setChildAttachedConsumer(this::onChildAttached);
+    }
+
+    /** Called just before a child is attached to the RecyclerView. */
+    private void onChildAttached(View child) {
+        // Avoid allocating hardware layers for alpha changes.
+        child.forceHasOverlappingRendering(false);
+        child.setPivotY(0);
+        if (getAnimationProgress() > 0 && getAnimationProgress() < 1) {
+            // Before the child is rendered, apply the animation including it to avoid flicker.
+            onProgressUpdated(getAnimationProgress());
+        } else {
+            // Apply default states without processing the full layout.
+            child.setAlpha(1);
+            child.setScaleY(1);
+            child.setTranslationY(0);
+            int adapterPosition = getRecyclerView().getChildAdapterPosition(child);
+            List<BaseAllAppsAdapter.AdapterItem> allAppsAdapters =
+                    getRecyclerView().getApps().getAdapterItems();
+            if (adapterPosition >= 0 && adapterPosition < allAppsAdapters.size()) {
+                allAppsAdapters.get(adapterPosition).setDecorationFillAlpha(255);
+            }
+            if (child instanceof ViewGroup childGroup) {
+                for (int i = 0; i < childGroup.getChildCount(); i++) {
+                    childGroup.getChildAt(i).setAlpha(1f);
+                }
+            }
+            if (child.getBackground() != null) {
+                child.getBackground().setAlpha(255);
+            }
+        }
+    }
+
+    /** @return the column that the view at this position is found (0 assumed if indeterminate). */
+    protected int getSpanIndex(AllAppsRecyclerView appsRecyclerView, int adapterPosition) {
+        if (adapterPosition == NO_POSITION) {
+            Log.w(LOG_TAG, "Can't determine span index - child not found in adapter");
+            return 0;
+        }
+        if (!(appsRecyclerView.getAdapter() instanceof AllAppsGridAdapter<?>)) {
+            Log.e(LOG_TAG, "Search RV doesn't have an AllAppsGridAdapter?");
+            // This case shouldn't happen, but for debug devices we will continue to create a more
+            // visible crash.
+            if (!Utilities.IS_DEBUG_DEVICE) {
+                return 0;
+            }
+        }
+        AllAppsGridAdapter<?> adapter = (AllAppsGridAdapter<?>) appsRecyclerView.getAdapter();
+        return adapter.getSpanIndex(adapterPosition);
+    }
+
+    protected TimeInterpolator getInterpolator() {
+        return DECELERATE_1_7;
+    }
+
+    protected AllAppsRecyclerView getRecyclerView() {
+        return mAllAppsContainerView.mAH.get(ActivityAllAppsContainerView.AdapterHolder.MAIN)
+                .mRecyclerView;
+    }
+
+    /** Returns true if a transition animation is currently in progress. */
+    protected boolean isRunning() {
+        return mAnimator != null;
+    }
+
+    /** Should only animate if the view is an app icon and if it has a decoration info. */
+    protected boolean shouldAnimate(View view, boolean hasDecorationInfo,
+            boolean firstAppRowComplete) {
+        return isAppIcon(view) && hasDecorationInfo;
+    }
+
+    private float getAdjustedContentAlpha(int itemsAnimated) {
+        float startContentFadeProgress = Math.max(0,
+                TOP_CONTENT_FADE_PROGRESS_START - CONTENT_STAGGER * itemsAnimated);
+        float endContentFadeProgress = Math.min(1,
+                startContentFadeProgress + CONTENT_FADE_PROGRESS_DURATION);
+        return 1 - clampToProgress(mAnimatorProgress,
+                startContentFadeProgress, endContentFadeProgress);
+    }
+
+    private float getAdjustedBackgroundAlpha(int itemsAnimated) {
+        float startBackgroundFadeProgress = Math.max(0,
+                TOP_BACKGROUND_FADE_PROGRESS_START - CONTENT_STAGGER * itemsAnimated);
+        float endBackgroundFadeProgress = Math.min(1,
+                startBackgroundFadeProgress + BACKGROUND_FADE_PROGRESS_DURATION);
+        return 1 - clampToProgress(mAnimatorProgress,
+                startBackgroundFadeProgress, endBackgroundFadeProgress);
+    }
+
+    private void setViewAdjustedContentAlpha(View view, int numberOfItemsAnimated,
+            boolean shouldAnimate) {
+        view.setAlpha(shouldAnimate ? getAdjustedContentAlpha(numberOfItemsAnimated) : 1f);
+    }
+
+    private void setAdjustedAdapterItemDecorationBackgroundAlpha(
+            BaseAllAppsAdapter.AdapterItem adapterItem, int numberOfItemsAnimated) {
+        adapterItem.setDecorationFillAlpha((int)
+                (255 * getAdjustedBackgroundAlpha(numberOfItemsAnimated)));
+    }
+
+    private float getAnimationProgress() {
+        return mAnimatorProgress;
+    }
+
+    private void setAnimationProgress(float expansionProgress) {
+        mAnimatorProgress = expansionProgress;
+        onProgressUpdated(expansionProgress);
+    }
+
+    protected boolean isAppIcon(View item) {
+        return item instanceof BubbleTextView && item.getTag() instanceof ItemInfo
+                && ((ItemInfo) item.getTag()).itemType == ITEM_TYPE_APPLICATION;
+    }
+}
diff --git a/src/com/android/launcher3/allapps/SearchRecyclerView.java b/src/com/android/launcher3/allapps/SearchRecyclerView.java
index 9d1dfc0..68f9f11 100644
--- a/src/com/android/launcher3/allapps/SearchRecyclerView.java
+++ b/src/com/android/launcher3/allapps/SearchRecyclerView.java
@@ -27,8 +27,6 @@
 /** A RecyclerView for AllApps Search results. */
 public class SearchRecyclerView extends AllAppsRecyclerView {
 
-    private Consumer<View> mChildAttachedConsumer;
-
     public SearchRecyclerView(Context context) {
         this(context, null);
     }
@@ -46,11 +44,6 @@
         super(context, attrs, defStyleAttr, defStyleRes);
     }
 
-    /** This will be called just before a new child is attached to the window. */
-    public void setChildAttachedConsumer(Consumer<View> childAttachedConsumer) {
-        mChildAttachedConsumer = childAttachedConsumer;
-    }
-
     @Override
     protected void updatePoolSize() {
         RecycledViewPool pool = getRecycledViewPool();
@@ -67,12 +60,4 @@
     public RecyclerViewFastScroller getScrollbar() {
         return null;
     }
-
-    @Override
-    public void onChildAttachedToWindow(@NonNull View child) {
-        if (mChildAttachedConsumer != null) {
-            mChildAttachedConsumer.accept(child);
-        }
-        super.onChildAttachedToWindow(child);
-    }
 }
diff --git a/src/com/android/launcher3/allapps/SearchTransitionController.java b/src/com/android/launcher3/allapps/SearchTransitionController.java
index eb1bc0a..d5c3b57 100644
--- a/src/com/android/launcher3/allapps/SearchTransitionController.java
+++ b/src/com/android/launcher3/allapps/SearchTransitionController.java
@@ -18,34 +18,21 @@
 
 import static android.view.View.VISIBLE;
 
-import static androidx.recyclerview.widget.RecyclerView.NO_POSITION;
-
 import static com.android.app.animation.Interpolators.DECELERATE_1_7;
 import static com.android.app.animation.Interpolators.INSTANT;
 import static com.android.app.animation.Interpolators.clampToProgress;
-import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
 import static com.android.launcher3.anim.AnimatorListeners.forEndCallback;
 import static com.android.launcher3.anim.AnimatorListeners.forSuccessCallback;
 
 import android.animation.ObjectAnimator;
 import android.animation.TimeInterpolator;
-import android.graphics.drawable.Drawable;
-import android.util.FloatProperty;
-import android.util.Log;
 import android.view.View;
-import android.view.ViewGroup;
 import android.view.animation.Interpolator;
 
-import com.android.launcher3.BubbleTextView;
 import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.model.data.ItemInfo;
 
 /** Coordinates the transition between Search and A-Z in All Apps. */
-public class SearchTransitionController {
-
-    private static final String LOG_TAG = "SearchTransitionCtrl";
+public class SearchTransitionController extends RecyclerViewAnimationController {
 
     // Interpolator when the user taps the QSB while already in All Apps.
     private static final Interpolator INTERPOLATOR_WITHIN_ALL_APPS = DECELERATE_1_7;
@@ -53,42 +40,10 @@
     // happening simultaneously.
     private static final Interpolator INTERPOLATOR_TRANSITIONING_TO_ALL_APPS = INSTANT;
 
-    /**
-     * These values represent points on the [0, 1] animation progress spectrum. They are used to
-     * animate items in the {@link SearchRecyclerView}.
-     */
-    private static final float TOP_CONTENT_FADE_PROGRESS_START = 0.133f;
-    private static final float CONTENT_FADE_PROGRESS_DURATION = 0.083f;
-    private static final float TOP_BACKGROUND_FADE_PROGRESS_START = 0.633f;
-    private static final float BACKGROUND_FADE_PROGRESS_DURATION = 0.15f;
-    private static final float CONTENT_STAGGER = 0.01f;  // Progress before next item starts fading.
-
-    private static final FloatProperty<SearchTransitionController> SEARCH_TO_AZ_PROGRESS =
-            new FloatProperty<SearchTransitionController>("searchToAzProgress") {
-                @Override
-                public Float get(SearchTransitionController controller) {
-                    return controller.getSearchToAzProgress();
-                }
-
-                @Override
-                public void setValue(SearchTransitionController controller, float progress) {
-                    controller.setSearchToAzProgress(progress);
-                }
-            };
-
-    private final ActivityAllAppsContainerView<?> mAllAppsContainerView;
-
-    private ObjectAnimator mSearchToAzAnimator = null;
-    private float mSearchToAzProgress = 1f;
     private boolean mSkipNextAnimationWithinAllApps;
 
     public SearchTransitionController(ActivityAllAppsContainerView<?> allAppsContainerView) {
-        mAllAppsContainerView = allAppsContainerView;
-    }
-
-    /** Returns true if a transition animation is currently in progress. */
-    public boolean isRunning() {
-        return mSearchToAzAnimator != null;
+        super(allAppsContainerView);
     }
 
     /**
@@ -101,51 +56,31 @@
      * @param onEndRunnable will be called when the animation finishes, unless another animation is
      *                      scheduled in the meantime
      */
-    public void animateToSearchState(boolean goingToSearch, long duration, Runnable onEndRunnable) {
-        float targetProgress = goingToSearch ? 0 : 1;
-
-        if (mSearchToAzAnimator != null) {
-            mSearchToAzAnimator.cancel();
-        }
-
-        mSearchToAzAnimator = ObjectAnimator.ofFloat(this, SEARCH_TO_AZ_PROGRESS, targetProgress);
-        boolean inAllApps = mAllAppsContainerView.isInAllApps();
-        TimeInterpolator timeInterpolator =
-                inAllApps ? INTERPOLATOR_WITHIN_ALL_APPS : INTERPOLATOR_TRANSITIONING_TO_ALL_APPS;
-        if (mSkipNextAnimationWithinAllApps) {
-            timeInterpolator = INSTANT;
-            mSkipNextAnimationWithinAllApps = false;
-        }
-        if (timeInterpolator == INSTANT) {
-            duration = 0;  // Don't want to animate when coming from QSB.
-        }
-        mSearchToAzAnimator.setDuration(duration).setInterpolator(timeInterpolator);
-        mSearchToAzAnimator.addListener(forEndCallback(() -> mSearchToAzAnimator = null));
+    @Override
+    protected void animateToState(boolean goingToSearch, long duration, Runnable onEndRunnable) {
+        super.animateToState(goingToSearch, duration, onEndRunnable);
         if (!goingToSearch) {
-            mSearchToAzAnimator.addListener(forSuccessCallback(() -> {
+            mAnimator.addListener(forSuccessCallback(() -> {
                 mAllAppsContainerView.getFloatingHeaderView().setFloatingRowsCollapsed(false);
                 mAllAppsContainerView.getFloatingHeaderView().reset(false /* animate */);
                 mAllAppsContainerView.getAppsRecyclerViewContainer().setTranslationY(0);
             }));
         }
-        mSearchToAzAnimator.addListener(forSuccessCallback(onEndRunnable));
-
         mAllAppsContainerView.getFloatingHeaderView().setFloatingRowsCollapsed(true);
         mAllAppsContainerView.getFloatingHeaderView().setVisibility(VISIBLE);
         mAllAppsContainerView.getFloatingHeaderView().maybeSetTabVisibility(VISIBLE);
         mAllAppsContainerView.getAppsRecyclerViewContainer().setVisibility(VISIBLE);
-        getSearchRecyclerView().setVisibility(VISIBLE);
-        getSearchRecyclerView().setChildAttachedConsumer(this::onSearchChildAttached);
-        mSearchToAzAnimator.start();
+        getRecyclerView().setVisibility(VISIBLE);
     }
 
-    private SearchRecyclerView getSearchRecyclerView() {
+    @Override
+    protected SearchRecyclerView getRecyclerView() {
         return mAllAppsContainerView.getSearchRecyclerView();
     }
 
-    private void setSearchToAzProgress(float searchToAzProgress) {
-        mSearchToAzProgress = searchToAzProgress;
-        int searchHeight = updateSearchRecyclerViewProgress();
+    @Override
+    protected int onProgressUpdated(float searchToAzProgress) {
+        int searchHeight = super.onProgressUpdated(searchToAzProgress);
 
         FloatingHeaderView headerView = mAllAppsContainerView.getFloatingHeaderView();
 
@@ -171,179 +106,27 @@
         appsContainer.setTranslationY(appsTranslationY);
         // Fade apps out with tabs (in 20% of the total animation).
         appsContainer.setAlpha(clampToProgress(searchToAzProgress, 0.8f, 1f));
+        return searchHeight;
     }
 
     /**
-     * Updates the children views of SearchRecyclerView based on the current animation progress.
-     *
-     * @return the total height of animating views (excluding at most one row of app icons).
+     * Should only animate if the view is not an app icon or if the app row is complete.
      */
-    private int updateSearchRecyclerViewProgress() {
-        int numSearchResultsAnimated = 0;
-        int totalHeight = 0;
-        int appRowHeight = 0;
-        boolean appRowComplete = false;
-        Integer top = null;
-        SearchRecyclerView searchRecyclerView = getSearchRecyclerView();
-
-        for (int i = 0; i < searchRecyclerView.getChildCount(); i++) {
-            View searchResultView = searchRecyclerView.getChildAt(i);
-            if (searchResultView == null) {
-                continue;
-            }
-
-            if (top == null) {
-                top = searchResultView.getTop();
-            }
-
-            int adapterPosition = searchRecyclerView.getChildAdapterPosition(searchResultView);
-            int spanIndex = getSpanIndex(searchRecyclerView, adapterPosition);
-            appRowComplete |= appRowHeight > 0 && spanIndex == 0;
-            // We don't animate the first (currently only) app row we see, as that is assumed to be
-            // predicted/prefix-matched apps.
-            boolean shouldAnimate = !isAppIcon(searchResultView) || appRowComplete;
-
-            float contentAlpha = 1f;
-            float backgroundAlpha = 1f;
-            if (shouldAnimate) {
-                if (spanIndex > 0) {
-                    // Animate this item with the previous item on the same row.
-                    numSearchResultsAnimated--;
-                }
-
-                // Adjust content alpha based on start progress and stagger.
-                float startContentFadeProgress = Math.max(0,
-                        TOP_CONTENT_FADE_PROGRESS_START
-                                - CONTENT_STAGGER * numSearchResultsAnimated);
-                float endContentFadeProgress = Math.min(1,
-                        startContentFadeProgress + CONTENT_FADE_PROGRESS_DURATION);
-                contentAlpha = 1 - clampToProgress(mSearchToAzProgress,
-                        startContentFadeProgress, endContentFadeProgress);
-
-                // Adjust background (or decorator) alpha based on start progress and stagger.
-                float startBackgroundFadeProgress = Math.max(0,
-                        TOP_BACKGROUND_FADE_PROGRESS_START
-                                - CONTENT_STAGGER * numSearchResultsAnimated);
-                float endBackgroundFadeProgress = Math.min(1,
-                        startBackgroundFadeProgress + BACKGROUND_FADE_PROGRESS_DURATION);
-                backgroundAlpha = 1 - clampToProgress(mSearchToAzProgress,
-                        startBackgroundFadeProgress, endBackgroundFadeProgress);
-
-                numSearchResultsAnimated++;
-            }
-
-            Drawable background = searchResultView.getBackground();
-            if (background != null
-                    && searchResultView instanceof ViewGroup
-                    && FeatureFlags.ENABLE_SEARCH_RESULT_BACKGROUND_DRAWABLES.get()) {
-                searchResultView.setAlpha(1f);
-
-                // Apply content alpha to each child, since the view needs to be fully opaque for
-                // the background to show properly.
-                ViewGroup searchResultViewGroup = (ViewGroup) searchResultView;
-                for (int j = 0; j < searchResultViewGroup.getChildCount(); j++) {
-                    searchResultViewGroup.getChildAt(j).setAlpha(contentAlpha);
-                }
-
-                // Apply background alpha to the background drawable directly.
-                background.setAlpha((int) (255 * backgroundAlpha));
-            } else {
-                searchResultView.setAlpha(contentAlpha);
-
-                // Apply background alpha to decorator if possible.
-                if (adapterPosition != NO_POSITION) {
-                    searchRecyclerView.getApps().getAdapterItems().get(adapterPosition)
-                            .setDecorationFillAlpha((int) (255 * backgroundAlpha));
-                }
-
-                // Apply background alpha to view's background (e.g. for Search Edu card).
-                if (background != null) {
-                    background.setAlpha((int) (255 * backgroundAlpha));
-                }
-            }
-
-            float scaleY = 1;
-            if (shouldAnimate) {
-                scaleY = 1 - mSearchToAzProgress;
-            }
-            int scaledHeight = (int) (searchResultView.getHeight() * scaleY);
-            searchResultView.setScaleY(scaleY);
-
-            // For rows with multiple elements, only count the height once and translate elements to
-            // the same y position.
-            int y = top + totalHeight;
-            if (spanIndex > 0) {
-                // Continuation of an existing row; move this item into the row.
-                y -= scaledHeight;
-            } else {
-                // Start of a new row contributes to total height.
-                totalHeight += scaledHeight;
-                if (!shouldAnimate) {
-                    appRowHeight = scaledHeight;
-                }
-            }
-            searchResultView.setY(y);
-        }
-
-        return totalHeight - appRowHeight;
+    @Override
+    protected boolean shouldAnimate(View view, boolean hasDecorationInfo, boolean appRowComplete) {
+        return !isAppIcon(view) || appRowComplete;
     }
 
-    /** @return the column that the view at this position is found (0 assumed if indeterminate). */
-    private int getSpanIndex(SearchRecyclerView searchRecyclerView, int adapterPosition) {
-        if (adapterPosition == NO_POSITION) {
-            Log.w(LOG_TAG, "Can't determine span index - child not found in adapter");
-            return 0;
+    @Override
+    protected TimeInterpolator getInterpolator() {
+        TimeInterpolator timeInterpolator =
+                mAllAppsContainerView.isInAllApps()
+                        ? INTERPOLATOR_WITHIN_ALL_APPS : INTERPOLATOR_TRANSITIONING_TO_ALL_APPS;
+        if (mSkipNextAnimationWithinAllApps) {
+            timeInterpolator = INSTANT;
+            mSkipNextAnimationWithinAllApps = false;
         }
-        if (!(searchRecyclerView.getAdapter() instanceof AllAppsGridAdapter<?>)) {
-            Log.e(LOG_TAG, "Search RV doesn't have an AllAppsGridAdapter?");
-            // This case shouldn't happen, but for debug devices we will continue to create a more
-            // visible crash.
-            if (!Utilities.IS_DEBUG_DEVICE) {
-                return 0;
-            }
-        }
-        AllAppsGridAdapter<?> adapter = (AllAppsGridAdapter<?>) searchRecyclerView.getAdapter();
-        return adapter.getSpanIndex(adapterPosition);
-    }
-
-    private boolean isAppIcon(View item) {
-        return item instanceof BubbleTextView && item.getTag() instanceof ItemInfo
-                && ((ItemInfo) item.getTag()).itemType == ITEM_TYPE_APPLICATION;
-    }
-
-    /** Called just before a child is attached to the SearchRecyclerView. */
-    private void onSearchChildAttached(View child) {
-        // Avoid allocating hardware layers for alpha changes.
-        child.forceHasOverlappingRendering(false);
-        child.setPivotY(0);
-        if (mSearchToAzProgress > 0) {
-            // Before the child is rendered, apply the animation including it to avoid flicker.
-            updateSearchRecyclerViewProgress();
-        } else {
-            // Apply default states without processing the full layout.
-            child.setAlpha(1);
-            child.setScaleY(1);
-            child.setTranslationY(0);
-            int adapterPosition = getSearchRecyclerView().getChildAdapterPosition(child);
-            if (adapterPosition != NO_POSITION) {
-                getSearchRecyclerView().getApps().getAdapterItems().get(adapterPosition)
-                        .setDecorationFillAlpha(255);
-            }
-            if (child instanceof ViewGroup
-                    && FeatureFlags.ENABLE_SEARCH_RESULT_BACKGROUND_DRAWABLES.get()) {
-                ViewGroup childGroup = (ViewGroup) child;
-                for (int i = 0; i < childGroup.getChildCount(); i++) {
-                    childGroup.getChildAt(i).setAlpha(1f);
-                }
-            }
-            if (child.getBackground() != null) {
-                child.getBackground().setAlpha(255);
-            }
-        }
-    }
-
-    private float getSearchToAzProgress() {
-        return mSearchToAzProgress;
+        return timeInterpolator;
     }
 
     /**
diff --git a/src/com/android/launcher3/allapps/SectionDecorationHandler.java b/src/com/android/launcher3/allapps/SectionDecorationHandler.java
new file mode 100644
index 0000000..f79b82c
--- /dev/null
+++ b/src/com/android/launcher3/allapps/SectionDecorationHandler.java
@@ -0,0 +1,206 @@
+/*
+ * 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.allapps;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.RectF;
+import android.graphics.drawable.GradientDrawable;
+import android.graphics.drawable.InsetDrawable;
+import android.view.View;
+
+import androidx.annotation.Nullable;
+import androidx.core.content.ContextCompat;
+
+import com.android.launcher3.R;
+import com.android.launcher3.util.Themes;
+
+public class SectionDecorationHandler {
+
+    protected final Path mTmpPath = new Path();
+    protected final RectF mTmpRect = new RectF();
+
+    protected final int mCornerGroupRadius;
+    protected final int mCornerResultRadius;
+    protected final RectF mBounds = new RectF();
+    protected final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+
+    protected final int mFocusAlpha = 255; // main focused item alpha
+    protected int mFillColor; // grouping color
+    protected int mFocusColor; // main focused item color
+    protected float mFillSpacing;
+    protected int mInlineRadius;
+    protected Context mContext;
+    protected float[] mCorners;
+    protected int mFillAlpha;
+    protected boolean mIsTopLeftRound;
+    protected boolean mIsTopRightRound;
+    protected boolean mIsBottomLeftRound;
+    protected boolean mIsBottomRightRound;
+    protected boolean mIsBottomRound;
+    protected boolean mIsTopRound;
+
+    public SectionDecorationHandler(Context context, int fillAlpha, boolean isTopLeftRound,
+            boolean isTopRightRound, boolean isBottomLeftRound,
+            boolean isBottomRightRound) {
+
+        mContext = context;
+        mFillAlpha = fillAlpha;
+        mFocusColor = ContextCompat.getColor(context,
+                R.color.material_color_surface_bright); // UX recommended
+        mFillColor = ContextCompat.getColor(context,
+                R.color.material_color_surface_container_high); // UX recommended
+
+        mIsTopLeftRound = isTopLeftRound;
+        mIsTopRightRound = isTopRightRound;
+        mIsBottomLeftRound = isBottomLeftRound;
+        mIsBottomRightRound = isBottomRightRound;
+        mIsBottomRound = mIsBottomLeftRound && mIsBottomRightRound;
+        mIsTopRound = mIsTopLeftRound && mIsTopRightRound;
+
+        mCornerGroupRadius = context.getResources().getDimensionPixelSize(
+                R.dimen.all_apps_recycler_view_decorator_group_radius);
+        mCornerResultRadius = context.getResources().getDimensionPixelSize(
+                R.dimen.all_apps_recycler_view_decorator_result_radius);
+
+        mInlineRadius = 0;
+        mFillSpacing = 0;
+        initCorners();
+    }
+
+    protected void initCorners() {
+        mCorners = new float[]{
+                mIsTopLeftRound ? mCornerGroupRadius : 0,
+                mIsTopLeftRound ? mCornerGroupRadius : 0, // Top left radius in px
+                mIsTopRightRound ? mCornerGroupRadius : 0,
+                mIsTopRightRound ? mCornerGroupRadius : 0, // Top right radius in px
+                mIsBottomRightRound ? mCornerGroupRadius : 0,
+                mIsBottomRightRound ? mCornerGroupRadius : 0, // Bottom right
+                mIsBottomLeftRound ? mCornerGroupRadius : 0,
+                mIsBottomLeftRound ? mCornerGroupRadius : 0 // Bottom left
+        };
+    }
+
+    protected void setFillAlpha(int fillAlpha) {
+        mFillAlpha = fillAlpha;
+        mPaint.setAlpha(mFillAlpha);
+    }
+
+    protected void onFocusDraw(Canvas canvas, @Nullable View view) {
+        if (view == null) {
+            return;
+        }
+        mPaint.setColor(mFillColor);
+        mPaint.setAlpha(mFillAlpha);
+        int scaledHeight = (int) (view.getHeight() * view.getScaleY());
+        mBounds.set(view.getLeft(), view.getY(), view.getRight(), view.getY() + scaledHeight);
+        onDraw(canvas);
+    }
+
+    protected void onDraw(Canvas canvas) {
+        mTmpPath.reset();
+        mTmpRect.set(mBounds.left + mFillSpacing,
+                mBounds.top + mFillSpacing,
+                mBounds.right - mFillSpacing,
+                mBounds.bottom - mFillSpacing);
+        mTmpPath.addRoundRect(mTmpRect, mCorners, Path.Direction.CW);
+        canvas.drawPath(mTmpPath, mPaint);
+    }
+
+    /** Sets the right background drawable to the view based on the give decoration info. */
+    public void applyBackground(View view, Context context,
+           @Nullable SectionDecorationInfo decorationInfo, boolean isHighlighted) {
+        int inset = context.getResources().getDimensionPixelSize(
+                R.dimen.all_apps_recycler_view_decorator_padding);
+        float radiusBottom = (decorationInfo == null || decorationInfo.isBottomRound()) ?
+                mCornerGroupRadius : mCornerResultRadius;
+        float radiusTop =
+                (decorationInfo == null || decorationInfo.isTopRound()) ?
+                        mCornerGroupRadius : mCornerResultRadius;
+        int color = isHighlighted ? mFocusColor : mFillColor;
+
+        GradientDrawable shape = new GradientDrawable();
+        shape.setShape(GradientDrawable.RECTANGLE);
+        shape.setCornerRadii(new float[] {
+                radiusTop, radiusTop, // top-left
+                radiusTop, radiusTop, // top-right
+                radiusBottom, radiusBottom, // bottom-right
+                radiusBottom, radiusBottom // bottom-left
+        });
+        shape.setColor(color);
+
+        // Setting the background resets the padding, so we cache it and reset it afterwards.
+        int paddingLeft = view.getPaddingLeft();
+        int paddingTop = view.getPaddingTop();
+        int paddingRight = view.getPaddingRight();
+        int paddingBottom = view.getPaddingBottom();
+
+        view.setBackground(new InsetDrawable(shape, inset));
+
+        view.setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom);
+    }
+
+    /**
+     * Section decorator that combines views and draws a single block decoration
+     */
+    public static class UnionDecorationHandler extends SectionDecorationHandler {
+
+        private final int mPaddingLeft;
+        private final int mPaddingRight;
+
+        public UnionDecorationHandler(
+                SectionDecorationHandler decorationHandler,
+                int paddingLeft, int paddingRight) {
+            super(decorationHandler.mContext, decorationHandler.mFillAlpha,
+                    decorationHandler.mIsTopLeftRound, decorationHandler.mIsTopRightRound,
+                    decorationHandler.mIsBottomLeftRound, decorationHandler.mIsBottomRightRound);
+            mPaddingLeft = paddingLeft;
+            mPaddingRight = paddingRight;
+        }
+
+        /**
+         * Expands decoration bounds to include child {@link PrivateAppsSectionDecorator}
+         */
+        public void addChild(SectionDecorationHandler child, View view, boolean applyBackground) {
+            int scaledHeight = (int) (view.getHeight() * view.getScaleY());
+            mBounds.union(view.getLeft(), view.getY(),
+                    view.getRight(), view.getY() + scaledHeight);
+            if (applyBackground) {
+                applyBackground(view, mContext, null, false);
+            }
+            mIsBottomRound |= child.mIsBottomRound;
+            mIsBottomLeftRound |= child.mIsBottomLeftRound;
+            mIsBottomRightRound |= child.mIsBottomRightRound;
+            mIsTopRound |= child.mIsTopRound;
+            mIsTopLeftRound |= child.mIsTopLeftRound;
+            mIsTopRightRound |= child.mIsTopRightRound;
+        }
+
+        /**
+         * Draws group decoration to canvas
+         */
+        public void onGroupDecorate(Canvas canvas) {
+            initCorners();
+            mBounds.left = mPaddingLeft;
+            mBounds.right = canvas.getWidth() - mPaddingRight;
+            mPaint.setColor(mFillColor);
+            mPaint.setAlpha(mFillAlpha);
+            onDraw(canvas);
+        }
+    }
+}
diff --git a/src/com/android/launcher3/allapps/SectionDecorationInfo.java b/src/com/android/launcher3/allapps/SectionDecorationInfo.java
new file mode 100644
index 0000000..1fed2b6
--- /dev/null
+++ b/src/com/android/launcher3/allapps/SectionDecorationInfo.java
@@ -0,0 +1,77 @@
+/*
+ * 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.allapps;
+
+import android.content.Context;
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+
+public class SectionDecorationInfo {
+
+    public static final int ROUND_NOTHING = 1 << 1;
+    public static final int ROUND_TOP_LEFT = 1 << 2;
+    public static final int ROUND_TOP_RIGHT = 1 << 3;
+    public static final int ROUND_BOTTOM_LEFT = 1 << 4;
+    public static final int ROUND_BOTTOM_RIGHT = 1 << 5;
+    public static final int DECORATOR_ALPHA = 255;
+
+    protected boolean mShouldDecorateItemsTogether;
+    private SectionDecorationHandler mDecorationHandler;
+    protected boolean mIsTopRound;
+    protected boolean mIsBottomRound;
+
+    public SectionDecorationInfo(Context context, int roundRegions, boolean decorateTogether) {
+        mDecorationHandler =
+                new SectionDecorationHandler(context, DECORATOR_ALPHA,
+                        isFlagEnabled(roundRegions, ROUND_TOP_LEFT),
+                        isFlagEnabled(roundRegions, ROUND_TOP_RIGHT),
+                        isFlagEnabled(roundRegions, ROUND_BOTTOM_LEFT),
+                        isFlagEnabled(roundRegions, ROUND_BOTTOM_RIGHT));
+        mShouldDecorateItemsTogether = decorateTogether;
+        mIsTopRound = isFlagEnabled(roundRegions, ROUND_TOP_LEFT) &&
+                isFlagEnabled(roundRegions, ROUND_TOP_RIGHT);
+        mIsBottomRound = isFlagEnabled(roundRegions, ROUND_BOTTOM_LEFT) &&
+                isFlagEnabled(roundRegions, ROUND_BOTTOM_RIGHT);
+    }
+
+    public SectionDecorationInfo(Context context, @NonNull Bundle target,
+            String targetLayoutType, @NonNull Bundle prevTarget, @NonNull Bundle nextTarget) {}
+
+    public SectionDecorationHandler getDecorationHandler() {
+        return mDecorationHandler;
+    }
+
+    private boolean isFlagEnabled(int canonicalFlag, int comparison) {
+        return (canonicalFlag & comparison) != 0;
+    }
+
+    /**
+     * Returns whether multiple {@link SectionDecorationInfo}s with the same sectionId should
+     * be grouped together.
+     */
+    public boolean shouldDecorateItemsTogether() {
+        return mShouldDecorateItemsTogether;
+    }
+
+    public boolean isTopRound() {
+        return mIsTopRound;
+    }
+
+    public boolean isBottomRound() {
+        return mIsBottomRound;
+    }
+}
diff --git a/src/com/android/launcher3/backuprestore/LauncherRestoreEventLogger.kt b/src/com/android/launcher3/backuprestore/LauncherRestoreEventLogger.kt
new file mode 100644
index 0000000..16b1854
--- /dev/null
+++ b/src/com/android/launcher3/backuprestore/LauncherRestoreEventLogger.kt
@@ -0,0 +1,84 @@
+package com.android.launcher3.backuprestore
+
+import android.content.Context
+import com.android.launcher3.LauncherSettings.Favorites
+import com.android.launcher3.R
+import com.android.launcher3.util.ResourceBasedOverride
+
+/**
+ * Wrapper for logging Restore event metrics for both success and failure to restore the Launcher
+ * workspace from a backup.
+ */
+open class LauncherRestoreEventLogger : ResourceBasedOverride {
+
+    companion object {
+        const val TAG = "LauncherRestoreEventLogger"
+
+        fun newInstance(context: Context?): LauncherRestoreEventLogger {
+            return ResourceBasedOverride.Overrides.getObject(
+                LauncherRestoreEventLogger::class.java,
+                context,
+                R.string.launcher_restore_event_logger_class
+            )
+        }
+    }
+
+    /**
+     * For logging when multiple items of a given data type failed to restore.
+     *
+     * @param dataType The data type that was not restored.
+     * @param count the number of data items that were not restored.
+     * @param error error type for why the data was not restored.
+     */
+    open fun logLauncherItemsRestoreFailed(dataType: String, count: Int, error: String?) {
+        // no-op
+    }
+
+    /**
+     * For logging when multiple items of a given data type were successfully restored.
+     *
+     * @param dataType The data type that was restored.
+     * @param count the number of data items restored.
+     */
+    open fun logLauncherItemsRestored(dataType: String, count: Int) {
+        // no-op
+    }
+
+    /**
+     * Helper to log successfully restoring a single item from the Favorites table.
+     *
+     * @param favoritesId The id of the item type from [Favorites] that was restored.
+     */
+    open fun logSingleFavoritesItemRestored(favoritesId: Int) {
+        // no-op
+    }
+
+    /**
+     * Helper to log a failure to restore a single item from the Favorites table.
+     *
+     * @param favoritesId The id of the item type from [Favorites] that was not restored.
+     * @param error error type for why the data was not restored.
+     */
+    open fun logSingleFavoritesItemRestoreFailed(favoritesId: Int, error: String?) {
+        // no-op
+    }
+
+    /**
+     * Helper to log a failure to restore items from the Favorites table.
+     *
+     * @param favoritesId The id of the item type from [Favorites] that was not restored.
+     * @param count number of items that failed to restore.
+     * @param error error type for why the data was not restored.
+     */
+    open fun logFavoritesItemsRestoreFailed(favoritesId: Int, count: Int, error: String?) {
+        // no-op
+    }
+
+    /**
+     * Uses the current [restoreEventLogger] to report its results to the [backupManager]. Use when
+     * done restoring items for Launcher.
+     */
+    open fun reportLauncherRestoreResults() {
+        // no-op
+    }
+}
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index 65269d4..7dfce56 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -304,7 +304,7 @@
                     "Long press of nav handle is instantly triggered if deep press is detected.");
 
     public static final IntFlag LPNH_HAPTIC_HINT_DELAY =
-            getIntFlag(309972570, "LPNH_HAPTIC_HINT_ITERATIONS", 0,
+            getIntFlag(309972570, "LPNH_HAPTIC_HINT_DELAY", 0,
                     "Delay before haptic hint starts.");
 
     // TODO(Block 17): Clean up flags
diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java
index 6ea3e8a..a98ec64 100644
--- a/src/com/android/launcher3/model/LoaderTask.java
+++ b/src/com/android/launcher3/model/LoaderTask.java
@@ -411,7 +411,7 @@
                     mSessionHelper.getActiveSessions();
             installingPkgs.forEach(mApp.getIconCache()::updateSessionCache);
             FileLog.d(TAG, "loadWorkspace: Packages with active install sessions: "
-                    + installingPkgs.values());
+                    + installingPkgs.keySet().stream().map(info -> info.mPackageName).toList());
 
             final PackageUserKey tempPackageKey = new PackageUserKey(null, null);
             mFirstScreenBroadcast = new FirstScreenBroadcast(installingPkgs);
@@ -440,10 +440,15 @@
                                 mShortcutKeyToPinnedShortcuts.put(ShortcutKey.fromInfo(shortcut),
                                         shortcut);
                             }
+                            if (pinnedShortcuts.isEmpty()) {
+                                FileLog.d(TAG, "No pinned shortcuts found for user " + user);
+                            }
                         } else {
                             // Shortcut manager can fail due to some race condition when the
                             // lock state changes too frequently. For the purpose of the loading
                             // shortcuts, consider the user is still locked.
+                            FileLog.d(TAG, "Shortcut request failed for user "
+                                    + user + ", user may still be locked.");
                             userUnlocked = false;
                         }
                     }
@@ -590,17 +595,17 @@
                             // Package is not yet available but might be
                             // installed later.
                             FileLog.d(TAG, "package not yet restored: " + targetPkg);
-
                             tempPackageKey.update(targetPkg, c.user);
                             if (c.hasRestoreFlag(WorkspaceItemInfo.FLAG_RESTORE_STARTED)) {
                                 // Restore has started once.
                             } else if (installingPkgs.containsKey(tempPackageKey)) {
                                 // App restore has started. Update the flag
                                 c.restoreFlag |= WorkspaceItemInfo.FLAG_RESTORE_STARTED;
-                                c.updater().put(Favorites.RESTORED,
-                                        c.restoreFlag).commit();
+                                FileLog.d(TAG, "restore started for installing app: " + targetPkg);
+                                c.updater().put(Favorites.RESTORED, c.restoreFlag).commit();
                             } else {
-                                c.markDeleted("Unrestored app removed: " + targetPkg);
+                                c.markDeleted("removing app that is not restored and not "
+                                        + "installing. package: " + targetPkg);
                                 return;
                             }
                         } else if (pmHelper.isAppOnSdcard(targetPkg, c.user)) {
@@ -611,7 +616,7 @@
                         } else if (!isSdCardReady) {
                             // SdCard is not ready yet. Package might get available,
                             // once it is ready.
-                            Log.d(TAG, "Missing pkg, will check later: " + targetPkg);
+                            Log.d(TAG, "Missing package, will check later: " + targetPkg);
                             mPendingPackages.add(new PackageUserKey(targetPkg, c.user));
                             // Add the icon on the workspace anyway.
                             allowMissingTarget = true;
@@ -647,7 +652,8 @@
                             ShortcutInfo pinnedShortcut = mShortcutKeyToPinnedShortcuts.get(key);
                             if (pinnedShortcut == null) {
                                 // The shortcut is no longer valid.
-                                c.markDeleted("Pinned shortcut not found");
+                                c.markDeleted("Pinned shortcut not found for package: "
+                                        + key.getPackageName());
                                 return;
                             }
                             info = new WorkspaceItemInfo(pinnedShortcut, mApp.getContext());
@@ -817,7 +823,7 @@
                         } else {
                             Log.v(TAG, "Widget restore pending id=" + c.id
                                     + " appWidgetId=" + appWidgetId
-                                    + " status =" + c.restoreFlag);
+                                    + " status=" + c.restoreFlag);
                             appWidgetInfo = new LauncherAppWidgetInfo(appWidgetId, component);
                             appWidgetInfo.restoreStatus = c.restoreFlag;
 
diff --git a/src/com/android/launcher3/model/ModelDbController.java b/src/com/android/launcher3/model/ModelDbController.java
index 139efc3..d2b7161 100644
--- a/src/com/android/launcher3/model/ModelDbController.java
+++ b/src/com/android/launcher3/model/ModelDbController.java
@@ -62,6 +62,7 @@
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.logging.FileLog;
 import com.android.launcher3.pm.UserCache;
 import com.android.launcher3.provider.LauncherDbUtils;
 import com.android.launcher3.provider.LauncherDbUtils.SQLiteTransaction;
@@ -262,7 +263,7 @@
      */
     public void tryMigrateDB() {
         if (!migrateGridIfNeeded()) {
-            Log.d(TAG, "Migration failed: resetting launcher database");
+            FileLog.d(TAG, "Migration failed: resetting launcher database");
             createEmptyDB();
             LauncherPrefs.get(mContext).putSync(
                     getEmptyDbCreatedKey(mOpenHelper.getDatabaseName()).to(true));
@@ -282,15 +283,17 @@
         createDbIfNotExists();
         if (LauncherPrefs.get(mContext).get(getEmptyDbCreatedKey())) {
             // If we have already create a new DB, ignore migration
+            Log.d(TAG, "migrateGridIfNeeded: new DB already created, skipping migration");
             return false;
         }
         InvariantDeviceProfile idp = LauncherAppState.getIDP(mContext);
         if (!GridSizeMigrationUtil.needsToMigrate(mContext, idp)) {
+            Log.d(TAG, "migrateGridIfNeeded: no grid migration needed");
             return true;
         }
         String targetDbName = new DeviceGridState(idp).getDbFile();
         if (TextUtils.equals(targetDbName, mOpenHelper.getDatabaseName())) {
-            Log.e(TAG, "migrateGridIfNeeded - target db is same as current: " + targetDbName);
+            Log.e(TAG, "migrateGridIfNeeded: target db is same as current: " + targetDbName);
             return false;
         }
         DatabaseHelper oldHelper = mOpenHelper;
@@ -299,6 +302,9 @@
         try {
             return GridSizeMigrationUtil.migrateGridIfNeeded(mContext, idp, mOpenHelper,
                    oldHelper.getWritableDatabase());
+        } catch (Exception e) {
+            FileLog.e(TAG, "Failed to migrate grid", e);
+            return false;
         } finally {
             if (mOpenHelper != oldHelper) {
                 oldHelper.close();
diff --git a/src/com/android/launcher3/provider/RestoreDbTask.java b/src/com/android/launcher3/provider/RestoreDbTask.java
index dbd13b3..dc8cd3a 100644
--- a/src/com/android/launcher3/provider/RestoreDbTask.java
+++ b/src/com/android/launcher3/provider/RestoreDbTask.java
@@ -89,9 +89,9 @@
 
     public static final String APPWIDGET_OLD_IDS = "appwidget_old_ids";
     public static final String APPWIDGET_IDS = "appwidget_ids";
-
     private static final String[] DB_COLUMNS_TO_LOG = {"profileId", "title", "itemType", "screen",
-            "container", "cellX", "cellY", "spanX", "spanY", "intent"};
+            "container", "cellX", "cellY", "spanX", "spanY", "intent", "appWidgetProvider",
+            "appWidgetId", "restored"};
 
     /**
      * Tries to restore the backup DB if needed
@@ -141,16 +141,17 @@
      *   4. If restored from a single display backup, remove gaps between screenIds
      *   5. Override shortcuts that need to be replaced.
      *
-     * @return number of items deleted.
+     * @return number of items deleted
      */
     @VisibleForTesting
     protected int sanitizeDB(Context context, ModelDbController controller, SQLiteDatabase db,
             BackupManager backupManager) throws Exception {
-        FileLog.d(TAG, "Old Launcher Database before sanitizing:");
+        logFavoritesTable(db, "Old Launcher Database before sanitizing:", null, null);
         // Primary user ids
         long myProfileId = controller.getSerialNumberForUser(myUserHandle());
         long oldProfileId = getDefaultProfileId(db);
-        FileLog.d(TAG, "sanitizeDB: myProfileId=" + myProfileId + " oldProfileId=" + oldProfileId);
+        FileLog.d(TAG, "sanitizeDB: myProfileId= " + myProfileId
+                + ", oldProfileId= " + oldProfileId);
         LongSparseArray<Long> oldManagedProfileIds = getManagedProfileIds(db, oldProfileId);
         LongSparseArray<Long> profileMapping = new LongSparseArray<>(oldManagedProfileIds.size()
                 + 1);
@@ -182,7 +183,7 @@
         final String[] args = new String[profileIds.length];
         Arrays.fill(args, "?");
         final String where = "profileId NOT IN (" + TextUtils.join(", ", Arrays.asList(args)) + ")";
-        logUnrestoredItems(db, where, profileIds);
+        logFavoritesTable(db, "items to delete from unrestored profiles:", where, profileIds);
         int itemsDeletedCount = db.delete(Favorites.TABLE_NAME, where, profileIds);
         FileLog.d(TAG, itemsDeletedCount + " total items from unrestored user(s) were deleted");
 
@@ -242,47 +243,6 @@
     }
 
     /**
-     * Queries and logs the items we will delete from unrestored profiles in the launcher db.
-     * This is to understand why items might be missing during the restore process for Launcher.
-     * @param database the Launcher db to query from.
-     * @param where the SELECT statement to query items that will be deleted.
-     * @param profileIds the profile ID's the user will be migrating to.
-     */
-    private void logUnrestoredItems(SQLiteDatabase database, String where, String[] profileIds) {
-        try (Cursor itemsToDelete = database.query(
-                /* table */ Favorites.TABLE_NAME,
-                /* columns */ DB_COLUMNS_TO_LOG,
-                /* selection */ where,
-                /* selection args */ profileIds,
-                /* groupBy */ null,
-                /* having */ null,
-                /* orderBy */ null
-        )) {
-            if (itemsToDelete.moveToFirst()) {
-                String[] columnNames = itemsToDelete.getColumnNames();
-                StringBuilder stringBuilder = new StringBuilder(
-                        "items to be deleted from the Favorites Table during restore:\n"
-                );
-                do {
-                    for (String columnName : columnNames) {
-                        stringBuilder.append(columnName)
-                            .append("=")
-                            .append(itemsToDelete.getString(
-                                        itemsToDelete.getColumnIndex(columnName)))
-                            .append(" ");
-                    }
-                    stringBuilder.append("\n");
-                } while (itemsToDelete.moveToNext());
-                FileLog.d(TAG, stringBuilder.toString());
-            } else {
-                FileLog.d(TAG, "logDeletedItems: No items found to delete");
-            }
-        } catch (Exception e) {
-            FileLog.e(TAG, "logDeletedItems: Error reading from database", e);
-        }
-    }
-
-    /**
      * Remove gaps between screenIds to make sure no empty pages are left in between.
      *
      * e.g. [0, 3, 4, 6, 7] -> [0, 1, 2, 3, 4]
@@ -396,7 +356,7 @@
                     IntArray.fromConcatString(lp.get(APP_WIDGET_IDS)).toArray(),
                     host);
         } else {
-            FileLog.d(TAG, "No app widget ids to restore.");
+            FileLog.d(TAG, "No app widget ids were received from backup to restore.");
         }
 
         lp.remove(APP_WIDGET_IDS, OLD_APP_WIDGET_IDS);
@@ -409,16 +369,16 @@
     private void restoreAppWidgetIds(Context context, ModelDbController controller,
             int[] oldWidgetIds, int[] newWidgetIds, @NonNull AppWidgetHost host) {
         if (WidgetsModel.GO_DISABLE_WIDGETS) {
-            Log.e(TAG, "Skipping widget ID remap as widgets not supported");
+            FileLog.e(TAG, "Skipping widget ID remap as widgets not supported");
             host.deleteHost();
             return;
         }
         if (!RestoreDbTask.isPending(context)) {
             // Someone has already gone through our DB once, probably LoaderTask. Skip any further
             // modifications of the DB.
-            Log.e(TAG, "Skipping widget ID remap as DB already in use");
+            FileLog.e(TAG, "Skipping widget ID remap as DB already in use");
             for (int widgetId : newWidgetIds) {
-                Log.d(TAG, "Deleting widgetId: " + widgetId);
+                FileLog.d(TAG, "Deleting widgetId: " + widgetId);
                 host.deleteAppWidgetId(widgetId);
             }
             return;
@@ -426,7 +386,7 @@
 
         final AppWidgetManager widgets = AppWidgetManager.getInstance(context);
 
-        Log.d(TAG, "restoreAppWidgetIds: "
+        FileLog.d(TAG, "restoreAppWidgetIds: "
                 + "oldWidgetIds=" + IntArray.wrap(oldWidgetIds).toConcatString()
                 + ", newWidgetIds=" + IntArray.wrap(newWidgetIds).toConcatString());
 
@@ -434,7 +394,7 @@
         logDatabaseWidgetInfo(controller);
 
         for (int i = 0; i < oldWidgetIds.length; i++) {
-            Log.i(TAG, "Widget state restore id " + oldWidgetIds[i] + " => " + newWidgetIds[i]);
+            FileLog.i(TAG, "Widget state restore id " + oldWidgetIds[i] + " => " + newWidgetIds[i]);
 
             final AppWidgetProviderInfo provider = widgets.getAppWidgetInfo(newWidgetIds[i]);
             final int state;
@@ -454,7 +414,7 @@
             final String where = "appWidgetId=? and (restored & 1) = 1 and profileId=?";
             String profileId = Long.toString(mainProfileId);
             final String[] args = new String[] { oldWidgetId, profileId };
-            Log.d(TAG, "restoreAppWidgetIds: querying profile id=" + profileId
+            FileLog.d(TAG, "restoreAppWidgetIds: querying profile id=" + profileId
                     + " with controller profile ID=" + controllerProfileId);
             int result = new ContentWriter(context,
                     new ContentWriter.CommitParams(controller, where, args))
@@ -463,7 +423,7 @@
                     .commit();
             if (result == 0) {
                 // TODO(b/234700507): Remove the logs after the bug is fixed
-                Log.e(TAG, "restoreAppWidgetIds: remapping failed since the widget is not in"
+                FileLog.e(TAG, "restoreAppWidgetIds: remapping failed since the widget is not in"
                         + " the database anymore");
                 try (Cursor cursor = controller.getDb().query(
                         Favorites.TABLE_NAME,
@@ -471,7 +431,7 @@
                         "appWidgetId=?", new String[]{oldWidgetId}, null, null, null)) {
                     if (!cursor.moveToFirst()) {
                         // The widget no long exists.
-                        Log.d(TAG, "Deleting widgetId: " + newWidgetIds[i] + " with old id: "
+                        FileLog.d(TAG, "Deleting widgetId: " + newWidgetIds[i] + " with old id: "
                                 + oldWidgetId);
                         host.deleteAppWidgetId(newWidgetIds[i]);
                     }
@@ -523,7 +483,7 @@
             }
             builder.append("]");
             Log.d(TAG, "restoreAppWidgetIds: all widget ids in database: "
-                    + builder.toString());
+                    + builder);
         } catch (Exception ex) {
             Log.e(TAG, "Getting widget ids from the database failed", ex);
         }
@@ -572,4 +532,45 @@
                 Collectors.joining(" OR "));
     }
 
+    /**
+     * Queries and logs the items from the Favorites table in the launcher db.
+     * This is to understand why items might be missing during the restore process for Launcher.
+     * @param database The Launcher db to query from.
+     * @param logHeader First line in log statement, used to explain what is being logged.
+     * @param where The SELECT statement to query items.
+     * @param profileIds The profile ID's for each user profile.
+     */
+    public static void logFavoritesTable(SQLiteDatabase database, @NonNull String logHeader,
+            String where, String[] profileIds) {
+        try (Cursor itemsToDelete = database.query(
+                /* table */ Favorites.TABLE_NAME,
+                /* columns */ DB_COLUMNS_TO_LOG,
+                /* selection */ where,
+                /* selection args */ profileIds,
+                /* groupBy */ null,
+                /* having */ null,
+                /* orderBy */ null
+        )) {
+            if (itemsToDelete.moveToFirst()) {
+                String[] columnNames = itemsToDelete.getColumnNames();
+                StringBuilder stringBuilder = new StringBuilder(logHeader + "\n");
+                do {
+                    for (String columnName : columnNames) {
+                        stringBuilder.append(columnName)
+                                .append("=")
+                                .append(itemsToDelete.getString(
+                                        itemsToDelete.getColumnIndex(columnName)))
+                                .append(" ");
+                    }
+                    stringBuilder.append("\n");
+                } while (itemsToDelete.moveToNext());
+                FileLog.d(TAG, stringBuilder.toString());
+            } else {
+                FileLog.d(TAG, "logFavoritesTable: No items found from query for"
+                        + "\"" + logHeader + "\"");
+            }
+        } catch (Exception e) {
+            FileLog.e(TAG, "logFavoritesTable: Error reading from database", e);
+        }
+    }
 }
diff --git a/src/com/android/launcher3/responsive/HotseatSpecsProvider.kt b/src/com/android/launcher3/responsive/HotseatSpecsProvider.kt
index ebbff51..7502a43 100644
--- a/src/com/android/launcher3/responsive/HotseatSpecsProvider.kt
+++ b/src/com/android/launcher3/responsive/HotseatSpecsProvider.kt
@@ -40,13 +40,28 @@
         return specsGroup
     }
 
+    private fun getSpecIgnoringDimensionType(
+        availableSize: Int,
+        specsGroup: ResponsiveSpecGroup<HotseatSpec>
+    ): HotseatSpec? {
+        val specWidth = specsGroup.widthSpecs.firstOrNull { availableSize <= it.maxAvailableSize }
+        val specHeight = specsGroup.heightSpecs.firstOrNull { availableSize <= it.maxAvailableSize }
+        return specWidth ?: specHeight
+    }
+
     fun getCalculatedSpec(
         aspectRatio: Float,
         dimensionType: DimensionType,
-        availableSpace: Int
+        availableSpace: Int,
     ): CalculatedHotseatSpec {
         val specsGroup = getSpecsByAspectRatio(aspectRatio)
-        val spec = specsGroup.getSpec(dimensionType, availableSpace)
+
+        // TODO(b/315548992): Ignore the dimension type to prevent crash before launcher
+        //  data migration is finished. The restore process allows the initialization of
+        //  an invalid or disabled grid until the data is restored and migrated.
+        val spec = getSpecIgnoringDimensionType(availableSpace, specsGroup)
+        check(spec != null) { "No available spec found within $availableSpace. $specsGroup" }
+        // val spec = specsGroup.getSpec(dimensionType, availableSpace)
         return CalculatedHotseatSpec(availableSpace, spec)
     }
 
diff --git a/src/com/android/launcher3/responsive/ResponsiveSpecGroup.kt b/src/com/android/launcher3/responsive/ResponsiveSpecGroup.kt
index b233d7c..a758be8 100644
--- a/src/com/android/launcher3/responsive/ResponsiveSpecGroup.kt
+++ b/src/com/android/launcher3/responsive/ResponsiveSpecGroup.kt
@@ -18,6 +18,7 @@
 
 import android.content.res.TypedArray
 import com.android.launcher3.R
+import com.android.launcher3.responsive.ResponsiveSpec.Companion.ResponsiveSpecType
 import com.android.launcher3.responsive.ResponsiveSpec.DimensionType
 
 /**
@@ -54,10 +55,29 @@
             } else {
                 heightSpecs.firstOrNull { availableSize <= it.maxAvailableSize }
             }
-        check(spec != null) { "No available $type spec found within $availableSize." }
+        check(spec != null) { "No available $type spec found within $availableSize. $this" }
         return spec
     }
 
+    override fun toString(): String {
+        fun printSpec(spec: IResponsiveSpec) =
+            when (spec.specType) {
+                ResponsiveSpecType.AllApps,
+                ResponsiveSpecType.Folder,
+                ResponsiveSpecType.Workspace -> (spec as ResponsiveSpec).toString()
+                ResponsiveSpecType.Hotseat -> (spec as HotseatSpec).toString()
+                ResponsiveSpecType.Cell -> (spec as CellSpec).toString()
+            }
+
+        val widthSpecsString = widthSpecs.joinToString(", ") { printSpec(it) }
+        val heightSpecsString = heightSpecs.joinToString(", ") { printSpec(it) }
+        return "ResponsiveSpecGroup(" +
+            "aspectRatio=${aspectRatio}, " +
+            "widthSpecs=[${widthSpecsString}], " +
+            "heightSpecs=[${heightSpecsString}]" +
+            ")"
+    }
+
     companion object {
         const val XML_GROUP_NAME = "specs"
 
diff --git a/src/com/android/launcher3/shortcuts/ShortcutRequest.java b/src/com/android/launcher3/shortcuts/ShortcutRequest.java
index 5291ce4..21efceb 100644
--- a/src/com/android/launcher3/shortcuts/ShortcutRequest.java
+++ b/src/com/android/launcher3/shortcuts/ShortcutRequest.java
@@ -24,10 +24,11 @@
 import android.content.pm.LauncherApps.ShortcutQuery;
 import android.content.pm.ShortcutInfo;
 import android.os.UserHandle;
-import android.util.Log;
 
 import androidx.annotation.Nullable;
 
+import com.android.launcher3.logging.FileLog;
+
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -101,7 +102,7 @@
             return new QueryResult(mContext.getSystemService(LauncherApps.class)
                     .getShortcuts(mQuery, mUserHandle));
         } catch (SecurityException | IllegalStateException e) {
-            Log.e(TAG, "Failed to query for shortcuts", e);
+            FileLog.e(TAG, "Failed to query for shortcuts", e);
             return QueryResult.DEFAULT;
         }
     }
diff --git a/src/com/android/launcher3/util/DimensionUtils.kt b/src/com/android/launcher3/util/DimensionUtils.kt
index 0eb0e08..63e919a 100644
--- a/src/com/android/launcher3/util/DimensionUtils.kt
+++ b/src/com/android/launcher3/util/DimensionUtils.kt
@@ -51,12 +51,12 @@
         // Taskbar on phone, portrait
         if (!deviceProfile.isLandscape) {
             p.x = ViewGroup.LayoutParams.MATCH_PARENT
-            p.y = res.getDimensionPixelSize(R.dimen.taskbar_size)
+            p.y = res.getDimensionPixelSize(R.dimen.taskbar_phone_size)
             return p
         }
 
         // Taskbar on phone, landscape
-        p.x = res.getDimensionPixelSize(R.dimen.taskbar_size)
+        p.x = res.getDimensionPixelSize(R.dimen.taskbar_phone_size)
         p.y = ViewGroup.LayoutParams.MATCH_PARENT
         return p
     }
diff --git a/src/com/android/launcher3/util/VibratorWrapper.java b/src/com/android/launcher3/util/VibratorWrapper.java
index f283fb6..4f20bbc 100644
--- a/src/com/android/launcher3/util/VibratorWrapper.java
+++ b/src/com/android/launcher3/util/VibratorWrapper.java
@@ -62,7 +62,7 @@
     public static final VibrationEffect EFFECT_CLICK =
             createPredefined(VibrationEffect.EFFECT_CLICK);
 
-    private static final float LOW_TICK_SCALE = 0.7f;
+    private static final float LOW_TICK_SCALE = 0.9f;
     private static final float DRAG_TEXTURE_SCALE = 0.03f;
     private static final float DRAG_COMMIT_SCALE = 0.5f;
     private static final float DRAG_BUMP_SCALE = 0.4f;
diff --git a/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java b/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
index 1aa49c7..5d069ed 100644
--- a/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
+++ b/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
@@ -19,7 +19,6 @@
 import android.annotation.TargetApi;
 import android.appwidget.AppWidgetProviderInfo;
 import android.content.Context;
-import android.content.res.Configuration;
 import android.graphics.Rect;
 import android.os.Build;
 import android.os.Handler;
@@ -32,7 +31,6 @@
 import android.util.SparseIntArray;
 import android.view.MotionEvent;
 import android.view.View;
-import android.view.ViewDebug;
 import android.view.ViewGroup;
 import android.view.accessibility.AccessibilityNodeInfo;
 import android.widget.AdapterView;
@@ -79,9 +77,6 @@
     private final CheckLongPressHelper mLongPressHelper;
     protected final Launcher mLauncher;
 
-    @ViewDebug.ExportedProperty(category = "launcher")
-    private boolean mReinflateOnConfigChange;
-
     // Maintain the color manager.
     private final LocalColorExtractor mColorExtractor;
 
@@ -176,17 +171,6 @@
 
         // The provider info or the views might have changed.
         checkIfAutoAdvance();
-
-        // It is possible that widgets can receive updates while launcher is not in the foreground.
-        // Consequently, the widgets will be inflated for the orientation of the foreground activity
-        // (framework issue). On resuming, we ensure that any widgets are inflated for the current
-        // orientation.
-        mReinflateOnConfigChange = !isSameOrientation();
-    }
-
-    private boolean isSameOrientation() {
-        return mLauncher.getResources().getConfiguration().orientation ==
-                mLauncher.getOrientation();
     }
 
     private boolean checkScrollableRecursively(ViewGroup viewGroup) {
@@ -450,17 +434,6 @@
         scheduleNextAdvance();
     }
 
-    @Override
-    protected void onConfigurationChanged(Configuration newConfig) {
-        super.onConfigurationChanged(newConfig);
-
-        // Only reinflate when the final configuration is same as the required configuration
-        if (mReinflateOnConfigChange && isSameOrientation()) {
-            mReinflateOnConfigChange = false;
-            reInflate();
-        }
-    }
-
     public void reInflate() {
         if (!isAttachedToWindow()) {
             return;
diff --git a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
index 583ef1a..e9a590b 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
@@ -163,7 +163,7 @@
     private boolean mIsInSearchMode;
     private boolean mIsNoWidgetsViewNeeded;
     @Px private int mMaxSpanPerRow;
-    private DeviceProfile mDeviceProfile;
+    protected DeviceProfile mDeviceProfile;
 
     protected TextView mNoWidgetsView;
     protected StickyHeaderLayout mSearchScrollView;
@@ -690,12 +690,7 @@
                 // Enables two pane picker for unfolded foldables if the flag is on.
                 || (activity.getDeviceProfile().isTwoPanels && enableUnfoldedTwoPanePicker());
 
-        if (isTwoPane && activity.getDeviceProfile().isTwoPanels) {
-            return R.layout.widgets_two_pane_sheet_foldable;
-        } else if (isTwoPane) {
-            return R.layout.widgets_two_pane_sheet;
-        }
-        return R.layout.widgets_full_sheet;
+        return isTwoPane ? R.layout.widgets_two_pane_sheet : R.layout.widgets_full_sheet;
     }
 
     @Override
diff --git a/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java b/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java
index d85737b..c3ab08c 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java
@@ -15,6 +15,8 @@
  */
 package com.android.launcher3.widget.picker;
 
+import static com.android.launcher3.Flags.enableUnfoldedTwoPanePicker;
+
 import android.content.Context;
 import android.graphics.Outline;
 import android.os.Process;
@@ -23,12 +25,15 @@
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewOutlineProvider;
+import android.widget.FrameLayout;
 import android.widget.LinearLayout;
 import android.widget.ScrollView;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Px;
 
 import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
 import com.android.launcher3.model.data.PackageItemInfo;
 import com.android.launcher3.recyclerview.ViewHolderBinder;
 import com.android.launcher3.util.PackageUserKey;
@@ -46,6 +51,8 @@
 
     private static final int PERSONAL_TAB = 0;
     private static final int WORK_TAB = 1;
+    private static final int MINIMUM_WIDTH_LEFT_PANE_FOLDABLE_DP = 268;
+    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;
@@ -117,6 +124,29 @@
     }
 
     @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        super.onLayout(changed, l, t, r, b);
+        if (changed && mDeviceProfile.isTwoPanels && enableUnfoldedTwoPanePicker()) {
+            LinearLayout layout = mContent.findViewById(R.id.linear_layout_container);
+            FrameLayout leftPane = layout.findViewById(R.id.recycler_view_container);
+            LinearLayout.LayoutParams layoutParams = (LayoutParams) leftPane.getLayoutParams();
+            // Width is 1/3 of the sheet unless it's less than min width or max width
+            int leftPaneWidth = layout.getMeasuredWidth() / 3;
+            @Px int minLeftPaneWidthPx = Utilities.dpToPx(MINIMUM_WIDTH_LEFT_PANE_FOLDABLE_DP);
+            @Px int maxLeftPaneWidthPx = Utilities.dpToPx(MAXIMUM_WIDTH_LEFT_PANE_FOLDABLE_DP);
+            if (leftPaneWidth < minLeftPaneWidthPx) {
+                layoutParams.width = minLeftPaneWidthPx;
+            } else if (leftPaneWidth > maxLeftPaneWidthPx) {
+                layoutParams.width = maxLeftPaneWidthPx;
+            } else {
+                layoutParams.width = 0;
+            }
+            layoutParams.weight = layoutParams.width == 0 ? 0.33F : 0;
+            leftPane.setLayoutParams(layoutParams);
+        }
+    }
+
+    @Override
     public void onRecommendedWidgetsBound() {
         super.onRecommendedWidgetsBound();
 
diff --git a/tests/src/com/android/launcher3/allapps/TaplOpenCloseAllApps.java b/tests/src/com/android/launcher3/allapps/TaplOpenCloseAllApps.java
index 4a42887..b4a5169 100644
--- a/tests/src/com/android/launcher3/allapps/TaplOpenCloseAllApps.java
+++ b/tests/src/com/android/launcher3/allapps/TaplOpenCloseAllApps.java
@@ -208,9 +208,10 @@
         InstrumentationRegistry.getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(
                 READ_DEVICE_CONFIG_PERMISSION);
         assumeFalse(FeatureFlags.ENABLE_BACK_SWIPE_LAUNCHER_ANIMATION.get());
-        mLauncher.getWorkspace().switchToAllApps();
-        mLauncher.pressBack();
-        mLauncher.getWorkspace();
+        mLauncher
+                .getWorkspace()
+                .switchToAllApps()
+                .pressBackToWorkspace();
         waitForState("Launcher internal state didn't switch to Home", () -> LauncherState.NORMAL);
         startAppFast(resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR));
         mLauncher.pressBack();
diff --git a/tests/src/com/android/launcher3/dragging/TaplUninstallRemove.java b/tests/src/com/android/launcher3/dragging/TaplUninstallRemove.java
index ed34307..568fc9f 100644
--- a/tests/src/com/android/launcher3/dragging/TaplUninstallRemove.java
+++ b/tests/src/com/android/launcher3/dragging/TaplUninstallRemove.java
@@ -166,9 +166,11 @@
             mLauncher.getWorkspace().verifyWorkspaceAppIconIsGone(
                     DUMMY_APP_NAME + " was expected to disappear after uninstall.", DUMMY_APP_NAME);
 
-            Map<String, Point> finalPositions =
-                    mLauncher.getWorkspace().getWorkspaceIconsPositions();
-            assertThat(finalPositions).doesNotContainKey(DUMMY_APP_NAME);
+            if (!TestStabilityRule.isPresubmit()) { // b/315847371
+                Map<String, Point> finalPositions =
+                        mLauncher.getWorkspace().getWorkspaceIconsPositions();
+                assertThat(finalPositions).doesNotContainKey(DUMMY_APP_NAME);
+            }
         } finally {
             TestUtil.uninstallDummyApp();
         }
diff --git a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
index 95ed401..79d8c60 100644
--- a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
+++ b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
@@ -257,7 +257,7 @@
         final RuleChain inner = RuleChain
                 .outerRule(new PortraitLandscapeRunner(this))
                 .around(new FailureWatcher(mLauncher, viewCaptureRule::getViewCaptureData))
-                .around(viewCaptureRule)
+                // .around(viewCaptureRule) // b/315482167
                 .around(new TestIsolationRule(mLauncher, true));
 
         return TestHelpers.isInLauncherProcess()
diff --git a/tests/tapl/com/android/launcher3/tapl/HomeAllApps.java b/tests/tapl/com/android/launcher3/tapl/HomeAllApps.java
index d9b179c..9ca2dc8 100644
--- a/tests/tapl/com/android/launcher3/tapl/HomeAllApps.java
+++ b/tests/tapl/com/android/launcher3/tapl/HomeAllApps.java
@@ -118,4 +118,17 @@
     public boolean isHomeState() {
         return true;
     }
+
+    /** Send the "back" gesture to go to workspace. */
+    public Workspace pressBackToWorkspace() {
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+             LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                     "want to press back from all apps to workspace")) {
+            mLauncher.runToState(
+                    () -> mLauncher.pressBackImpl(),
+                    NORMAL_STATE_ORDINAL,
+                    "pressing back");
+            return new Workspace(mLauncher);
+        }
+    }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java b/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java
index 6d58a35..184ece7 100644
--- a/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java
+++ b/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java
@@ -340,4 +340,17 @@
             }
         }
     }
+
+    /** Send the "back" gesture to go to workspace. */
+    public Workspace pressBackToWorkspace() {
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+             LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                     "want to press back from launched app to workspace")) {
+            mLauncher.executeAndWaitForWallpaperAnimation(
+                    () -> mLauncher.pressBackImpl(),
+                    "pressing back"
+            );
+            return new Workspace(mLauncher);
+        }
+    }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index 0db6eb7..91ef472 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -1121,30 +1121,34 @@
      */
     public void pressBack() {
         try (Closable e = eventsCheck(); Closable c = addContextLayer("want to press back")) {
-            waitForLauncherInitialized();
-            final boolean launcherVisible =
-                    isTablet() ? isLauncherContainerVisible() : isLauncherVisible();
-            boolean isThreeFingerTrackpadGesture =
-                    mTrackpadGestureType == TrackpadGestureType.THREE_FINGER;
-            if (getNavigationModel() == NavigationModel.ZERO_BUTTON
-                    || isThreeFingerTrackpadGesture) {
-                final Point displaySize = getRealDisplaySize();
-                // TODO(b/225505986): change startY and endY back to displaySize.y / 2 once the
-                //  issue is solved.
-                int startX = isThreeFingerTrackpadGesture ? displaySize.x / 4 : 0;
-                int endX = isThreeFingerTrackpadGesture ? displaySize.x * 3 / 4 : displaySize.x / 2;
-                linearGesture(startX, displaySize.y / 4, endX, displaySize.y / 4,
-                        10, false, GestureScope.DONT_EXPECT_PILFER);
+            pressBackImpl();
+        }
+    }
+
+    void pressBackImpl() {
+        waitForLauncherInitialized();
+        final boolean launcherVisible =
+                isTablet() ? isLauncherContainerVisible() : isLauncherVisible();
+        boolean isThreeFingerTrackpadGesture =
+                mTrackpadGestureType == TrackpadGestureType.THREE_FINGER;
+        if (getNavigationModel() == NavigationModel.ZERO_BUTTON
+                || isThreeFingerTrackpadGesture) {
+            final Point displaySize = getRealDisplaySize();
+            // TODO(b/225505986): change startY and endY back to displaySize.y / 2 once the
+            //  issue is solved.
+            int startX = isThreeFingerTrackpadGesture ? displaySize.x / 4 : 0;
+            int endX = isThreeFingerTrackpadGesture ? displaySize.x * 3 / 4 : displaySize.x / 2;
+            linearGesture(startX, displaySize.y / 4, endX, displaySize.y / 4,
+                    10, false, GestureScope.DONT_EXPECT_PILFER);
+        } else {
+            waitForNavigationUiObject("back").click();
+        }
+        if (launcherVisible) {
+            if (getContext().getApplicationInfo().isOnBackInvokedCallbackEnabled()) {
+                expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_ON_BACK_INVOKED);
             } else {
-                waitForNavigationUiObject("back").click();
-            }
-            if (launcherVisible) {
-                if (getContext().getApplicationInfo().isOnBackInvokedCallbackEnabled()) {
-                    expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_ON_BACK_INVOKED);
-                } else {
-                    expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_KEY_BACK_DOWN);
-                    expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_KEY_BACK_UP);
-                }
+                expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_KEY_BACK_DOWN);
+                expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_KEY_BACK_UP);
             }
         }
     }