Merge "Add standalone taskbar edu tooltip for the pinnable taskbar feature" into main
diff --git a/Android.bp b/Android.bp
index 010ea45..e2dd48d 100644
--- a/Android.bp
+++ b/Android.bp
@@ -23,42 +23,66 @@
 // All sources are split so they can be reused in many other libraries/apps in other folders
 filegroup {
     name: "launcher-src",
-    srcs: [ "src/**/*.java", "src/**/*.kt" ],
+    srcs: [
+        "src/**/*.java",
+        "src/**/*.kt",
+    ],
 }
 
 filegroup {
     name: "launcher-quickstep-src",
-    srcs: [ "quickstep/src/**/*.java", "quickstep/src/**/*.kt" ],
+    srcs: [
+        "quickstep/src/**/*.java",
+        "quickstep/src/**/*.kt",
+    ],
 }
 
 filegroup {
     name: "launcher-go-src",
-    srcs: [ "go/src/**/*.java", "go/src/**/*.kt" ],
+    srcs: [
+        "go/src/**/*.java",
+        "go/src/**/*.kt",
+    ],
 }
 
 filegroup {
     name: "launcher-go-quickstep-src",
-    srcs: [ "go/quickstep/src/**/*.java", "go/quickstep/src/**/*.kt" ],
+    srcs: [
+        "go/quickstep/src/**/*.java",
+        "go/quickstep/src/**/*.kt",
+    ],
 }
 
 filegroup {
     name: "launcher-src_shortcuts_overrides",
-    srcs: [ "src_shortcuts_overrides/**/*.java", "src_shortcuts_overrides/**/*.kt" ],
+    srcs: [
+        "src_shortcuts_overrides/**/*.java",
+        "src_shortcuts_overrides/**/*.kt",
+    ],
 }
 
 filegroup {
     name: "launcher-src_ui_overrides",
-    srcs: [ "src_ui_overrides/**/*.java", "src_ui_overrides/**/*.kt" ],
+    srcs: [
+        "src_ui_overrides/**/*.java",
+        "src_ui_overrides/**/*.kt",
+    ],
 }
 
 filegroup {
     name: "launcher-ext_tests",
-    srcs: [ "ext_tests/**/*.java", "ext_tests/**/*.kt" ],
+    srcs: [
+        "ext_tests/**/*.java",
+        "ext_tests/**/*.kt",
+    ],
 }
 
 filegroup {
     name: "launcher-quickstep-ext_tests",
-    srcs: [ "quickstep/ext_tests/**/*.java", "quickstep/ext_tests/**/*.kt" ],
+    srcs: [
+        "quickstep/ext_tests/**/*.java",
+        "quickstep/ext_tests/**/*.kt",
+    ],
 }
 
 // Proguard files for Launcher3
@@ -85,7 +109,7 @@
     srcs: [
         "tests/tapl/**/*.java",
     ],
-    resource_dirs: [ ],
+    resource_dirs: [],
     manifest: "tests/tapl/AndroidManifest.xml",
     platform_apis: true,
 }
@@ -99,7 +123,7 @@
     sdk_version: "current",
     proto: {
         type: "lite",
-        local_include_dirs:[
+        local_include_dirs: [
             "protos",
             "protos_overrides",
         ],
@@ -115,14 +139,14 @@
     sdk_version: "current",
     proto: {
         type: "lite",
-        local_include_dirs:[
+        local_include_dirs: [
             "quickstep/protos_overrides",
         ],
     },
     static_libs: [
-      "libprotobuf-java-lite",
-      "launcher_log_protos_lite"
-      ],
+        "libprotobuf-java-lite",
+        "launcher_log_protos_lite",
+    ],
 }
 
 java_library {
@@ -139,7 +163,7 @@
 // Library with all the dependencies for building Launcher3
 android_library {
     name: "Launcher3ResLib",
-    srcs: [ ],
+    srcs: [],
     resource_dirs: ["res"],
     static_libs: [
         "LauncherPluginLib",
@@ -154,13 +178,13 @@
         "com.google.android.material_material",
         "iconloader_base",
         "view_capture",
-        "animationlib"
+        "animationlib",
     ],
     manifest: "AndroidManifest-common.xml",
     sdk_version: "current",
     min_sdk_version: min_launcher3_sdk_version,
     lint: {
-        baseline_filename: "lint-baseline-res-lib.xml",
+        baseline_filename: "lint-baseline2.xml",
     },
 }
 
@@ -182,7 +206,7 @@
     min_sdk_version: min_launcher3_sdk_version,
     manifest: "AndroidManifest-common.xml",
     lint: {
-        baseline_filename: "lint-baseline-common-deps-lib.xml",
+        baseline_filename: "lint-baseline2.xml",
     },
 }
 
@@ -229,14 +253,14 @@
         "AndroidManifest-common.xml",
     ],
     lint: {
-        baseline_filename: "lint-baseline-launcher3.xml",
+        baseline_filename: "lint-baseline.xml",
     },
 }
 
 // Library with all the dependencies for building quickstep
 android_library {
     name: "QuickstepResLib",
-    srcs: [ ],
+    srcs: [],
     resource_dirs: [
         "quickstep/res",
     ],
@@ -255,7 +279,6 @@
     min_sdk_version: "current",
 }
 
-
 // Library with all the dependencies for building Launcher Go
 android_library {
     name: "LauncherGoResLib",
@@ -285,9 +308,6 @@
         "AndroidManifest-common.xml",
     ],
     min_sdk_version: "current",
-    lint: {
-        baseline_filename: "lint-baseline-go-res-lib.xml",
-    },
 }
 
 // Build rule for Quickstep library
@@ -316,9 +336,6 @@
     manifest: "quickstep/AndroidManifest.xml",
     platform_apis: true,
     min_sdk_version: "current",
-    lint: {
-        baseline_filename: "lint-baseline-launcher3.xml",
-    },
 }
 
 // Build rule for Launcher3 Go app for Android Go devices.
@@ -360,7 +377,7 @@
     manifest: "go/AndroidManifest.xml",
     jacoco: {
         include_filter: ["com.android.launcher3.*"],
-    }
+    },
 
 }
 
@@ -396,7 +413,7 @@
     manifest: "quickstep/AndroidManifest.xml",
     jacoco: {
         include_filter: ["com.android.launcher3.*"],
-    }
+    },
 
 }
 
@@ -414,7 +431,7 @@
     min_sdk_version: "current",
     target_sdk_version: "current",
 
-    srcs: [ ],
+    srcs: [],
 
     resource_dirs: [
         "go/quickstep/res",
@@ -446,7 +463,6 @@
     manifest: "quickstep/AndroidManifest.xml",
     jacoco: {
         include_filter: ["com.android.launcher3.*"],
-    }
+    },
 
 }
-
diff --git a/OWNERS b/OWNERS
index 38963d0..b8aae78 100644
--- a/OWNERS
+++ b/OWNERS
@@ -12,7 +12,6 @@
 jonmiranda@google.com
 alexchau@google.com
 patmanning@google.com
-tsuharesu@google.com
 awickham@google.com
 
 # Launcher workspace eng team
diff --git a/aconfig/launcher.aconfig b/aconfig/launcher.aconfig
index af175ce..98dd4d6 100644
--- a/aconfig/launcher.aconfig
+++ b/aconfig/launcher.aconfig
@@ -43,6 +43,13 @@
 }
 
 flag {
+    name: "enable_focus_outline"
+    namespace: "launcher"
+    description: "Enables focus states outline for launcher."
+    bug: "310953377"
+}
+
+flag {
     name: "enable_taskbar_no_recreate"
     namespace: "launcher"
     description: "Enables taskbar with no recreation from lifecycle changes of TaskbarActivityContext."
@@ -60,21 +67,7 @@
     name: "enable_taskbar_pinning"
     namespace: "launcher"
     description: "Enables taskbar pinning to allow user to switch between transient and persistent taskbar flavors."
-    bug: "270396583"
-}
-
-flag {
-    name: "enable_split_from_fullscreen_with_keyboard_shortcuts"
-    namespace: "launcher"
-    description: "Enables initiating split from a fullscreen app using keyboard shortcuts"
-    bug: "270394122"
-}
-
-flag {
-    name: "enable_launcher_br_metrics"
-    namespace: "launcher"
-    description: "Enables logging of Launcher restore metrics to the Backup & Restore team"
-    bug: "307527314"
+    bug: "296231746"
 }
 
 flag {
@@ -90,3 +83,46 @@
     description: "Enables full width two pane widget picker for tablets in landscape and portrait"
     bug: "315055849"
 }
+
+flag {
+    name: "enable_shortcut_dont_suggest_app"
+    namespace: "launcher"
+    description: "Enables don't suggest app shortcut for suggested apps"
+    bug: "319250810"
+}
+
+flag {
+    name: "enable_support_for_archiving"
+    namespace: "launcher"
+    description: "Enables support for archived apps in Launcher3, such as empty progress bar etc."
+    bug: "210590852"
+}
+
+flag {
+    name: "enable_private_space_install_shortcut"
+    namespace: "launcher"
+    description: "Enables long-press shortcut to install a copy of an app to Private space"
+    bug: "316118005"
+}
+
+flag {
+    name: "enable_launcher_br_metrics_fixed"
+    namespace: "launcher"
+    description: "Enables logging of Launcher restore metrics to the Backup & Restore team"
+    bug: "307527314"
+    is_fixed_read_only: true
+}
+
+flag {
+    name: "enable_reboot_unlock_animation"
+    namespace: "launcher"
+    description: "Enables unlock animation after device reboot"
+    bug: "298231234"
+}
+
+flag {
+    name: "enable_workspace_inflation"
+    namespace: "launcher"
+    description: "Enables asnc inflation of workspace icons"
+    bug: "318539160"
+}
diff --git a/aconfig/launcher_search.aconfig b/aconfig/launcher_search.aconfig
index fc79200..97e56b7 100644
--- a/aconfig/launcher_search.aconfig
+++ b/aconfig/launcher_search.aconfig
@@ -12,4 +12,18 @@
     namespace: "launcher_search"
     description: "This flag enables the animation of the Private Space container"
     bug: "299294792"
+}
+
+flag {
+    name: "private_space_sys_apps_separation"
+    namespace: "launcher_search"
+    description: "This flag enables showing system apps separate in Private Space container."
+    bug: "308054233"
+}
+
+flag {
+    name: "private_space_app_installer_button"
+    namespace: "launcher_search"
+    description: "This flag enables addition of App Installer button in Private Space container."
+    bug: "308064949"
 }
\ No newline at end of file
diff --git a/go/quickstep/src/com/android/launcher3/AppSharing.java b/go/quickstep/src/com/android/launcher3/AppSharing.java
index cb1f1c7..78524d1 100644
--- a/go/quickstep/src/com/android/launcher3/AppSharing.java
+++ b/go/quickstep/src/com/android/launcher3/AppSharing.java
@@ -27,6 +27,7 @@
 import android.os.Process;
 import android.os.UserHandle;
 import android.os.UserManager;
+import android.provider.Settings;
 import android.text.TextUtils;
 import android.util.Log;
 import android.view.View;
@@ -64,14 +65,17 @@
 
     private static final String TAG = "AppSharing";
     private static final String FILE_PROVIDER_SUFFIX = ".overview.fileprovider";
-    private static final String APP_EXSTENSION = ".apk";
+    private static final String APP_EXTENSION = ".apk";
     private static final String APP_MIME_TYPE = "application/application";
 
     private final String mSharingComponent;
     private AppShareabilityManager mShareabilityMgr;
 
     private AppSharing(Launcher launcher) {
-        mSharingComponent = launcher.getText(R.string.app_sharing_component).toString();
+        String sharingComponent = Settings.Secure.getString(launcher.getContentResolver(),
+                Settings.Secure.NEARBY_SHARING_COMPONENT);
+        mSharingComponent = TextUtils.isEmpty(sharingComponent) ? launcher.getText(
+                R.string.app_sharing_component).toString() : sharingComponent;
     }
 
     private Uri getShareableUri(Context context, String path, String displayName) {
@@ -147,7 +151,7 @@
                 PackageInfo packageInfo = packageManager.getPackageInfo(packageName, 0);
                 sourceDir = packageInfo.applicationInfo.sourceDir;
                 appLabel = packageManager.getApplicationLabel(packageInfo.applicationInfo)
-                        .toString() + APP_EXSTENSION;
+                        + APP_EXTENSION;
             } catch (Exception e) {
                 Log.e(TAG, "Could not find info for package \"" + packageName + "\"");
                 return;
@@ -175,9 +179,7 @@
                 return;
             }
             checkShareability(/* requestUpdateIfUnknown */ false);
-            mTarget.runOnUiThread(() -> {
-                mPopupDataProvider.redrawSystemShortcuts();
-            });
+            mTarget.runOnUiThread(mPopupDataProvider::redrawSystemShortcuts);
         }
 
         private void checkShareability(boolean requestUpdateIfUnknown) {
diff --git a/go/src/com/android/launcher3/model/WidgetsModel.java b/go/src/com/android/launcher3/model/WidgetsModel.java
index 1aa5d03..3a28444 100644
--- a/go/src/com/android/launcher3/model/WidgetsModel.java
+++ b/go/src/com/android/launcher3/model/WidgetsModel.java
@@ -33,6 +33,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.function.Predicate;
 
 /**
  * Widgets data model that is used by the adapters of the widget views and controllers.
@@ -48,6 +49,19 @@
     private static final ArrayList<WidgetsListBaseEntry> EMPTY_WIDGET_LIST = new ArrayList<>();
 
     /**
+     * Returns a list of {@link WidgetsListBaseEntry} filtered using given widget item filter. All
+     * {@link WidgetItem}s in a single row are sorted (based on label and user), but the overall
+     * list of {@link WidgetsListBaseEntry}s is not sorted.
+     *
+     * @see com.android.launcher3.widget.picker.WidgetsListAdapter#setWidgets(List)
+     */
+    public synchronized ArrayList<WidgetsListBaseEntry> getFilteredWidgetsListForPicker(
+            Context context,
+            Predicate<WidgetItem> widgetItemFilter) {
+        return EMPTY_WIDGET_LIST;
+    }
+
+    /**
      * Returns a list of {@link WidgetsListBaseEntry}. All {@link WidgetItem} in a single row are
      * sorted (based on label and user), but the overall list of {@link WidgetsListBaseEntry}s is
      * not sorted. This list is sorted at the UI when using
@@ -88,4 +102,4 @@
             Context context, ComponentName provider, UserHandle userHandle) {
         return new PackageItemInfo(provider.getPackageName(), userHandle);
     }
-}
\ No newline at end of file
+}
diff --git a/lint-baseline-common-deps-lib.xml b/lint-baseline-common-deps-lib.xml
deleted file mode 100644
index e52f8fb..0000000
--- a/lint-baseline-common-deps-lib.xml
+++ /dev/null
@@ -1,48 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.1.0" client="cli" variant="all" version="4.1.0">
-
-    <issue
-        id="NewApi"
-        message="`?android:attr/dialogCornerRadius` requires API level 28 (current min is 26)"
-        errorLine1="        android:topLeftRadius=&quot;?android:attr/dialogCornerRadius&quot;"
-        errorLine2="        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/res/drawable/add_item_dialog_background.xml"
-            line="6"
-            column="9"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="`?android:attr/dialogCornerRadius` requires API level 28 (current min is 26)"
-        errorLine1="        android:topRightRadius=&quot;?android:attr/dialogCornerRadius&quot; />"
-        errorLine2="        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/res/drawable/add_item_dialog_background.xml"
-            line="7"
-            column="9"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="`@android:style/Widget.DeviceDefault.Button.Colored` requires API level 28 (current min is 26)"
-        errorLine1="    &lt;style name=&quot;Widget.DeviceDefault.Button.Rounded.Colored&quot; parent=&quot;@android:style/Widget.DeviceDefault.Button.Colored&quot;>"
-        errorLine2="                                                              ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/res/values/styles.xml"
-            line="287"
-            column="63"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="`@android:dimen/system_app_widget_background_radius` requires API level 31 (current min is 26)"
-        errorLine1="    &lt;corners android:radius=&quot;@android:dimen/system_app_widget_background_radius&quot; />"
-        errorLine2="             ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/res/drawable/widget_resize_frame.xml"
-            line="20"
-            column="14"/>
-    </issue>
-
-</issues>
diff --git a/lint-baseline-go-res-lib.xml b/lint-baseline-go-res-lib.xml
deleted file mode 100644
index c5669f2..0000000
--- a/lint-baseline-go-res-lib.xml
+++ /dev/null
@@ -1,576 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.1.0" client="cli" variant="all" version="4.1.0">
-
-    <issue
-        id="NewApi"
-        message="Call requires API level R (current min is 29): `android.view.View#getWindowInsetsController`"
-        errorLine1="        getWindowInsetsController().hide(WindowInsets.Type.ime());"
-        errorLine2="        ~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/allapps/AllAppsContainerView.java"
-            line="203"
-            column="9"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level R (current min is 29): `android.view.WindowInsets.Type#ime`"
-        errorLine1="        getWindowInsetsController().hide(WindowInsets.Type.ime());"
-        errorLine2="                                                           ~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/allapps/AllAppsContainerView.java"
-            line="203"
-            column="60"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level R (current min is 29): `android.view.WindowInsetsController#hide`"
-        errorLine1="        getWindowInsetsController().hide(WindowInsets.Type.ime());"
-        errorLine2="                                    ~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/allapps/AllAppsContainerView.java"
-            line="203"
-            column="37"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level R (current min is 29): `android.view.View#getWindowInsetsController`"
-        errorLine1="                getWindowInsetsController().hide(WindowInsets.Type.ime());"
-        errorLine2="                ~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/allapps/AllAppsRecyclerView.java"
-            line="193"
-            column="17"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level R (current min is 29): `android.view.WindowInsets.Type#ime`"
-        errorLine1="                getWindowInsetsController().hide(WindowInsets.Type.ime());"
-        errorLine2="                                                                   ~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/allapps/AllAppsRecyclerView.java"
-            line="193"
-            column="68"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level R (current min is 29): `android.view.WindowInsetsController#hide`"
-        errorLine1="                getWindowInsetsController().hide(WindowInsets.Type.ime());"
-        errorLine2="                                            ~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/allapps/AllAppsRecyclerView.java"
-            line="193"
-            column="45"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 29): `android.appwidget.AppWidgetHostView#updateAppWidgetSize`"
-        errorLine1="            widgetView.updateAppWidgetSize(new Bundle(), sizes);"
-        errorLine2="                       ~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/AppWidgetResizeFrame.java"
-            line="399"
-            column="24"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 29): `android.graphics.Rect#inset`"
-        errorLine1="        potentialTaskRect.inset(insets.left, insets.top, insets.right, insets.bottom);"
-        errorLine2="                          ~~~~~">
-        <location
-            file="packages/apps/Launcher3/quickstep/src/com/android/quickstep/BaseActivityInterface.java"
-            line="248"
-            column="27"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 29): `android.graphics.Rect#inset`"
-        errorLine1="        potentialTaskRect.inset("
-        errorLine2="                          ~~~~~">
-        <location
-            file="packages/apps/Launcher3/quickstep/src/com/android/quickstep/BaseActivityInterface.java"
-            line="249"
-            column="27"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 29): `android.graphics.Rect#inset`"
-        errorLine1="        outRect.inset(Math.max(insets.left, sideMargin), Math.max(insets.top, topMargin),"
-        errorLine2="                ~~~~~">
-        <location
-            file="packages/apps/Launcher3/quickstep/src/com/android/quickstep/BaseActivityInterface.java"
-            line="291"
-            column="17"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 29): `android.graphics.Rect#inset`"
-        errorLine1="        gridRect.inset(0, dp.overviewTaskThumbnailTopMarginPx, 0, 0);"
-        errorLine2="                 ~~~~~">
-        <location
-            file="packages/apps/Launcher3/quickstep/src/com/android/quickstep/BaseActivityInterface.java"
-            line="315"
-            column="18"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level R (current min is 29): `android.view.WindowManager#getCurrentWindowMetrics`"
-        errorLine1="                    .getCurrentWindowMetrics().getWindowInsets();"
-        errorLine2="                     ~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/DeviceProfile.java"
-            line="236"
-            column="22"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level R (current min is 29): `android.view.WindowMetrics#getWindowInsets`"
-        errorLine1="                    .getCurrentWindowMetrics().getWindowInsets();"
-        errorLine2="                                               ~~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/DeviceProfile.java"
-            line="236"
-            column="48"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level R (current min is 29): `android.content.Context#getDisplay`"
-        errorLine1="            if (mContext.getDisplay() != null) {"
-        errorLine2="                         ~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/quickstep/src/com/android/quickstep/interaction/EdgeBackGestureHandler.java"
-            line="115"
-            column="26"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level R (current min is 29): `android.content.Context#getDisplay`"
-        errorLine1="                mContext.getDisplay().getRealSize(mDisplaySize);"
-        errorLine2="                         ~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/quickstep/src/com/android/quickstep/interaction/EdgeBackGestureHandler.java"
-            line="116"
-            column="26"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level R (current min is 29): `android.content.ContextWrapper#getDisplay`"
-        errorLine1="        Display display = getDisplay();"
-        errorLine2="                          ~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java"
-            line="153"
-            column="27"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level R (current min is 29): `java.util.List#of`"
-        errorLine1="                    List.of(new Rect(0, 0, metrics.widthPixels, metrics.heightPixels)));"
-        errorLine2="                         ~~">
-        <location
-            file="packages/apps/Launcher3/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java"
-            line="158"
-            column="26"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 29): `android.appwidget.AppWidgetHostView#resetColorResources`"
-        errorLine1="            resetColorResources();"
-        errorLine2="            ~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java"
-            line="137"
-            column="13"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level R (current min is 29): `java.util.List#of`"
-        errorLine1="            mColorExtractor.addLocation(List.of(mLastLocationRegistered));"
-        errorLine2="                                             ~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java"
-            line="367"
-            column="46"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level R (current min is 29): `java.util.List#of`"
-        errorLine1="                mColorExtractor.addLocation(List.of(mLastLocationRegistered));"
-        errorLine2="                                                 ~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java"
-            line="390"
-            column="50"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Field requires API level 31 (current min is 29): `android.appwidget.AppWidgetProviderInfo#maxResizeWidth`"
-        errorLine1="                (ATLEAST_S &amp;&amp; maxResizeWidth > 0)"
-        errorLine2="                              ~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java"
-            line="91"
-            column="31"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Field requires API level 31 (current min is 29): `android.appwidget.AppWidgetProviderInfo#maxResizeWidth`"
-        errorLine1="                        ? getSpanX(widgetPadding, maxResizeWidth, smallestCellWidth)"
-        errorLine2="                                                  ~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java"
-            line="92"
-            column="51"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Field requires API level 31 (current min is 29): `android.appwidget.AppWidgetProviderInfo#maxResizeHeight`"
-        errorLine1="                (ATLEAST_S &amp;&amp; maxResizeHeight > 0)"
-        errorLine2="                              ~~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java"
-            line="95"
-            column="31"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Field requires API level 31 (current min is 29): `android.appwidget.AppWidgetProviderInfo#maxResizeHeight`"
-        errorLine1="                        ? getSpanY(widgetPadding, maxResizeHeight, smallestCellHeight)"
-        errorLine2="                                                  ~~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java"
-            line="96"
-            column="51"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Field requires API level 31 (current min is 29): `android.appwidget.AppWidgetProviderInfo#targetCellWidth`"
-        errorLine1="        if (ATLEAST_S &amp;&amp; targetCellWidth >= minSpanX &amp;&amp; targetCellWidth &lt;= maxSpanX"
-        errorLine2="                         ~~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java"
-            line="101"
-            column="26"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Field requires API level 31 (current min is 29): `android.appwidget.AppWidgetProviderInfo#targetCellWidth`"
-        errorLine1="        if (ATLEAST_S &amp;&amp; targetCellWidth >= minSpanX &amp;&amp; targetCellWidth &lt;= maxSpanX"
-        errorLine2="                                                        ~~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java"
-            line="101"
-            column="57"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Field requires API level 31 (current min is 29): `android.appwidget.AppWidgetProviderInfo#targetCellHeight`"
-        errorLine1="                &amp;&amp; targetCellHeight >= minSpanY &amp;&amp; targetCellHeight &lt;= maxSpanY) {"
-        errorLine2="                   ~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java"
-            line="102"
-            column="20"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Field requires API level 31 (current min is 29): `android.appwidget.AppWidgetProviderInfo#targetCellHeight`"
-        errorLine1="                &amp;&amp; targetCellHeight >= minSpanY &amp;&amp; targetCellHeight &lt;= maxSpanY) {"
-        errorLine2="                                                   ~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java"
-            line="102"
-            column="52"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Field requires API level 31 (current min is 29): `android.appwidget.AppWidgetProviderInfo#targetCellWidth`"
-        errorLine1="            spanX = targetCellWidth;"
-        errorLine2="                    ~~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java"
-            line="103"
-            column="21"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Field requires API level 31 (current min is 29): `android.appwidget.AppWidgetProviderInfo#targetCellHeight`"
-        errorLine1="            spanY = targetCellHeight;"
-        errorLine2="                    ~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java"
-            line="104"
-            column="21"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level R (current min is 29): `android.content.Context#getDisplay`"
-        errorLine1="        final Display display = mContext.getDisplay();"
-        errorLine2="                                         ~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/quickstep/src/com/android/quickstep/interaction/NavBarGestureHandler.java"
-            line="94"
-            column="42"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 29): `android.content.pm.LauncherActivityInfo#getLoadingProgress`"
-        errorLine1="            return (int) (100 * info.getLoadingProgress());"
-        errorLine2="                                     ~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/util/PackageManagerHelper.java"
-            line="338"
-            column="38"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level R (current min is 29): `java.util.List#of`"
-        errorLine1="    private List&lt;WidgetsListBaseEntry> mAllWidgets = List.of();"
-        errorLine2="                                                          ~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/popup/PopupDataProvider.java"
-            line="64"
-            column="59"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level R (current min is 29): `java.util.List#of`"
-        errorLine1="    private List&lt;ItemInfo> mRecommendedWidgets = List.of();"
-        errorLine2="                                                      ~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/popup/PopupDataProvider.java"
-            line="66"
-            column="55"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level R (current min is 29): `new android.view.SurfaceControlViewHost`"
-        errorLine1="                    .submit(() -> new SurfaceControlViewHost(mContext, mDisplay, mHostToken))"
-        errorLine2="                                  ~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java"
-            line="91"
-            column="35"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level R (current min is 29): `android.view.SurfaceControlViewHost#getSurfacePackage`"
-        errorLine1="            surfacePackage = mSurfaceControlViewHost.getSurfacePackage();"
-        errorLine2="                                                     ~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java"
-            line="93"
-            column="54"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level R (current min is 29): `android.view.SurfaceControlViewHost#setView`"
-        errorLine1="                host.setView(view, view.getMeasuredWidth(), view.getMeasuredHeight());"
-        errorLine2="                     ~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java"
-            line="127"
-            column="22"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Cast from `SurfacePackage` to `Parcelable` requires API level 30 (current min is 29)"
-        errorLine1="        result.putParcelable(KEY_SURFACE_PACKAGE, surfacePackage);"
-        errorLine2="                                                  ~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java"
-            line="132"
-            column="51"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level R (current min is 29): `android.view.SurfaceControlViewHost#release`"
-        errorLine1="                mSurfaceControlViewHost.release();"
-        errorLine2="                                        ~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java"
-            line="149"
-            column="41"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level R (current min is 29): `android.graphics.Outline#setPath`"
-        errorLine1="        outline.setPath(mPath);"
-        errorLine2="                ~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/popup/RoundedArrowDrawable.java"
-            line="88"
-            column="17"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 29): `new android.widget.EdgeEffect`"
-        errorLine1="                ? new EdgeEffect(context, attrs) : new EdgeEffect(context);"
-        errorLine2="                  ~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/views/SpringRelativeLayout.java"
-            line="49"
-            column="19"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 29): `new android.widget.EdgeEffect`"
-        errorLine1="                ? new EdgeEffect(context, attrs) : new EdgeEffect(context);"
-        errorLine2="                  ~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/views/SpringRelativeLayout.java"
-            line="51"
-            column="19"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level R (current min is 29): `android.view.WindowManager.LayoutParams#setFitInsetsTypes`"
-        errorLine1="        mWindowLayoutParams.setFitInsetsTypes(0);"
-        errorLine2="                            ~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/quickstep/src/com/android/launcher3/taskbar/TaskbarController.java"
-            line="316"
-            column="29"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level R (current min is 29): `android.view.WindowInsets#getInsets`"
-        errorLine1="            Insets systemInsets = insets.getInsets(WindowInsets.Type.systemBars());"
-        errorLine2="                                         ~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java"
-            line="118"
-            column="42"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level R (current min is 29): `android.view.WindowInsets.Type#systemBars`"
-        errorLine1="            Insets systemInsets = insets.getInsets(WindowInsets.Type.systemBars());"
-        errorLine2="                                                                     ~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java"
-            line="118"
-            column="70"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 29): `android.appwidget.AppWidgetProviderInfo#loadDescription`"
-        errorLine1="            CharSequence description = mItem.widgetInfo.loadDescription(getContext());"
-        errorLine2="                                                        ~~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/widget/WidgetCell.java"
-            line="193"
-            column="57"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Field requires API level 31 (current min is 29): `android.appwidget.AppWidgetProviderInfo#previewLayout`"
-        errorLine1="                &amp;&amp; item.widgetInfo.previewLayout != Resources.ID_NULL) {"
-        errorLine2="                   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/widget/WidgetCell.java"
-            line="214"
-            column="20"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Field requires API level 31 (current min is 29): `android.appwidget.AppWidgetProviderInfo#previewLayout`"
-        errorLine1="            launcherAppWidgetProviderInfo.initialLayout = item.widgetInfo.previewLayout;"
-        errorLine2="                                                          ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/widget/WidgetCell.java"
-            line="222"
-            column="59"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level R (current min is 29): `android.view.View#getWindowInsetsController`"
-        errorLine1="        getWindowInsetsController().hide(WindowInsets.Type.ime());"
-        errorLine2="        ~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java"
-            line="558"
-            column="9"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level R (current min is 29): `android.view.WindowInsets.Type#ime`"
-        errorLine1="        getWindowInsetsController().hide(WindowInsets.Type.ime());"
-        errorLine2="                                                           ~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java"
-            line="558"
-            column="60"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level R (current min is 29): `android.view.WindowInsetsController#hide`"
-        errorLine1="        getWindowInsetsController().hide(WindowInsets.Type.ime());"
-        errorLine2="                                    ~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java"
-            line="558"
-            column="37"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level R (current min is 29): `java.util.List#of`"
-        errorLine1="            return new RecommendationTableData(List.of(), previewScale);"
-        errorLine2="                                                    ~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java"
-            line="139"
-            column="53"/>
-    </issue>
-
-</issues>
diff --git a/lint-baseline-launcher3.xml b/lint-baseline-launcher3.xml
deleted file mode 100644
index 107a346..0000000
--- a/lint-baseline-launcher3.xml
+++ /dev/null
@@ -1,609 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.1.0" client="cli" variant="all" version="4.1.0">
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 28 (current min is 26): `android.os.UserManager#requestQuietModeEnabled`"
-        errorLine1="            showConfirm |= !userManager.requestQuietModeEnabled(!toState, userProfile);"
-        errorLine2="                                        ~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/allapps/WorkModeSwitch.java"
-            line="110"
-            column="41"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level R (current min is 26): `android.view.View#getWindowInsetsController`"
-        errorLine1="        getWindowInsetsController().hide(WindowInsets.Type.ime());"
-        errorLine2="        ~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/allapps/AllAppsContainerView.java"
-            line="203"
-            column="9"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level R (current min is 26): `android.view.WindowInsets.Type#ime`"
-        errorLine1="        getWindowInsetsController().hide(WindowInsets.Type.ime());"
-        errorLine2="                                                           ~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/allapps/AllAppsContainerView.java"
-            line="203"
-            column="60"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level R (current min is 26): `android.view.WindowInsetsController#hide`"
-        errorLine1="        getWindowInsetsController().hide(WindowInsets.Type.ime());"
-        errorLine2="                                    ~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/allapps/AllAppsContainerView.java"
-            line="203"
-            column="37"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level R (current min is 26): `android.view.View#getWindowInsetsController`"
-        errorLine1="                getWindowInsetsController().hide(WindowInsets.Type.ime());"
-        errorLine2="                ~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/allapps/AllAppsRecyclerView.java"
-            line="193"
-            column="17"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level R (current min is 26): `android.view.WindowInsets.Type#ime`"
-        errorLine1="                getWindowInsetsController().hide(WindowInsets.Type.ime());"
-        errorLine2="                                                                   ~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/allapps/AllAppsRecyclerView.java"
-            line="193"
-            column="68"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level R (current min is 26): `android.view.WindowInsetsController#hide`"
-        errorLine1="                getWindowInsetsController().hide(WindowInsets.Type.ime());"
-        errorLine2="                                            ~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/allapps/AllAppsRecyclerView.java"
-            line="193"
-            column="45"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 26): `android.appwidget.AppWidgetHostView#updateAppWidgetSize`"
-        errorLine1="            widgetView.updateAppWidgetSize(new Bundle(), sizes);"
-        errorLine2="                       ~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/AppWidgetResizeFrame.java"
-            line="399"
-            column="24"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level R (current min is 26): `android.view.WindowManager#getCurrentWindowMetrics`"
-        errorLine1="                    .getCurrentWindowMetrics().getWindowInsets();"
-        errorLine2="                     ~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/DeviceProfile.java"
-            line="236"
-            column="22"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level R (current min is 26): `android.view.WindowMetrics#getWindowInsets`"
-        errorLine1="                    .getCurrentWindowMetrics().getWindowInsets();"
-        errorLine2="                                               ~~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/DeviceProfile.java"
-            line="236"
-            column="48"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 29 (current min is 26): `android.content.res.Resources#getFloat`"
-        errorLine1="        folderLabelTextScale = res.getFloat(R.dimen.folder_label_text_scale);"
-        errorLine2="                                   ~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/DeviceProfile.java"
-            line="256"
-            column="36"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 29 (current min is 26): `android.content.res.Resources#getFloat`"
-        errorLine1="        return mContext.getResources().getFloat(resId);"
-        errorLine2="                                       ~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/util/DynamicResource.java"
-            line="73"
-            column="40"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 26): `android.appwidget.AppWidgetHostView#resetColorResources`"
-        errorLine1="            resetColorResources();"
-        errorLine2="            ~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java"
-            line="137"
-            column="13"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level R (current min is 26): `java.util.List#of`"
-        errorLine1="            mColorExtractor.addLocation(List.of(mLastLocationRegistered));"
-        errorLine2="                                             ~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java"
-            line="367"
-            column="46"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level R (current min is 26): `java.util.List#of`"
-        errorLine1="                mColorExtractor.addLocation(List.of(mLastLocationRegistered));"
-        errorLine2="                                                 ~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java"
-            line="390"
-            column="50"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Field requires API level 31 (current min is 26): `android.appwidget.AppWidgetProviderInfo#maxResizeWidth`"
-        errorLine1="                (ATLEAST_S &amp;&amp; maxResizeWidth > 0)"
-        errorLine2="                              ~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java"
-            line="91"
-            column="31"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Field requires API level 31 (current min is 26): `android.appwidget.AppWidgetProviderInfo#maxResizeWidth`"
-        errorLine1="                        ? getSpanX(widgetPadding, maxResizeWidth, smallestCellWidth)"
-        errorLine2="                                                  ~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java"
-            line="92"
-            column="51"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Field requires API level 31 (current min is 26): `android.appwidget.AppWidgetProviderInfo#maxResizeHeight`"
-        errorLine1="                (ATLEAST_S &amp;&amp; maxResizeHeight > 0)"
-        errorLine2="                              ~~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java"
-            line="95"
-            column="31"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Field requires API level 31 (current min is 26): `android.appwidget.AppWidgetProviderInfo#maxResizeHeight`"
-        errorLine1="                        ? getSpanY(widgetPadding, maxResizeHeight, smallestCellHeight)"
-        errorLine2="                                                  ~~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java"
-            line="96"
-            column="51"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Field requires API level 31 (current min is 26): `android.appwidget.AppWidgetProviderInfo#targetCellWidth`"
-        errorLine1="        if (ATLEAST_S &amp;&amp; targetCellWidth >= minSpanX &amp;&amp; targetCellWidth &lt;= maxSpanX"
-        errorLine2="                         ~~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java"
-            line="101"
-            column="26"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Field requires API level 31 (current min is 26): `android.appwidget.AppWidgetProviderInfo#targetCellWidth`"
-        errorLine1="        if (ATLEAST_S &amp;&amp; targetCellWidth >= minSpanX &amp;&amp; targetCellWidth &lt;= maxSpanX"
-        errorLine2="                                                        ~~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java"
-            line="101"
-            column="57"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Field requires API level 31 (current min is 26): `android.appwidget.AppWidgetProviderInfo#targetCellHeight`"
-        errorLine1="                &amp;&amp; targetCellHeight >= minSpanY &amp;&amp; targetCellHeight &lt;= maxSpanY) {"
-        errorLine2="                   ~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java"
-            line="102"
-            column="20"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Field requires API level 31 (current min is 26): `android.appwidget.AppWidgetProviderInfo#targetCellHeight`"
-        errorLine1="                &amp;&amp; targetCellHeight >= minSpanY &amp;&amp; targetCellHeight &lt;= maxSpanY) {"
-        errorLine2="                                                   ~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java"
-            line="102"
-            column="52"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Field requires API level 31 (current min is 26): `android.appwidget.AppWidgetProviderInfo#targetCellWidth`"
-        errorLine1="            spanX = targetCellWidth;"
-        errorLine2="                    ~~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java"
-            line="103"
-            column="21"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Field requires API level 31 (current min is 26): `android.appwidget.AppWidgetProviderInfo#targetCellHeight`"
-        errorLine1="            spanY = targetCellHeight;"
-        errorLine2="                    ~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java"
-            line="104"
-            column="21"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 28 (current min is 26): `android.app.Person#getKey`"
-        errorLine1="        return people.stream().filter(person -> person.getKey() != null)"
-        errorLine2="                                                       ~~~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/notification/NotificationKeyData.java"
-            line="72"
-            column="56"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Method reference requires API level 28 (current min is 26): `Person::getKey`"
-        errorLine1="                .map(Person::getKey).sorted().toArray(String[]::new);"
-        errorLine2="                     ~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/notification/NotificationKeyData.java"
-            line="73"
-            column="22"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 26): `android.content.pm.LauncherActivityInfo#getLoadingProgress`"
-        errorLine1="            return (int) (100 * info.getLoadingProgress());"
-        errorLine2="                                     ~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/util/PackageManagerHelper.java"
-            line="338"
-            column="38"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Field requires API level 29 (current min is 26): `android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction#ACTION_PAGE_LEFT`"
-        errorLine1="                AccessibilityNodeInfo.AccessibilityAction.ACTION_PAGE_LEFT"
-        errorLine2="                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/PagedView.java"
-            line="1752"
-            column="17"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Field requires API level 29 (current min is 26): `android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction#ACTION_PAGE_RIGHT`"
-        errorLine1="                : AccessibilityNodeInfo.AccessibilityAction.ACTION_PAGE_RIGHT);"
-        errorLine2="                  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/PagedView.java"
-            line="1753"
-            column="19"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Field requires API level 29 (current min is 26): `android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction#ACTION_PAGE_RIGHT`"
-        errorLine1="                AccessibilityNodeInfo.AccessibilityAction.ACTION_PAGE_RIGHT"
-        errorLine2="                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/PagedView.java"
-            line="1760"
-            column="17"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Field requires API level 29 (current min is 26): `android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction#ACTION_PAGE_LEFT`"
-        errorLine1="                : AccessibilityNodeInfo.AccessibilityAction.ACTION_PAGE_LEFT);"
-        errorLine2="                  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/PagedView.java"
-            line="1761"
-            column="19"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level R (current min is 26): `java.util.List#of`"
-        errorLine1="    private List&lt;WidgetsListBaseEntry> mAllWidgets = List.of();"
-        errorLine2="                                                          ~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/popup/PopupDataProvider.java"
-            line="64"
-            column="59"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level R (current min is 26): `java.util.List#of`"
-        errorLine1="    private List&lt;ItemInfo> mRecommendedWidgets = List.of();"
-        errorLine2="                                                      ~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/popup/PopupDataProvider.java"
-            line="66"
-            column="55"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level R (current min is 26): `new android.view.SurfaceControlViewHost`"
-        errorLine1="                    .submit(() -> new SurfaceControlViewHost(mContext, mDisplay, mHostToken))"
-        errorLine2="                                  ~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java"
-            line="91"
-            column="35"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level R (current min is 26): `android.view.SurfaceControlViewHost#getSurfacePackage`"
-        errorLine1="            surfacePackage = mSurfaceControlViewHost.getSurfacePackage();"
-        errorLine2="                                                     ~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java"
-            line="93"
-            column="54"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level R (current min is 26): `android.view.SurfaceControlViewHost#setView`"
-        errorLine1="                host.setView(view, view.getMeasuredWidth(), view.getMeasuredHeight());"
-        errorLine2="                     ~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java"
-            line="127"
-            column="22"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Cast from `SurfacePackage` to `Parcelable` requires API level 30 (current min is 26)"
-        errorLine1="        result.putParcelable(KEY_SURFACE_PACKAGE, surfacePackage);"
-        errorLine2="                                                  ~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java"
-            line="132"
-            column="51"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level R (current min is 26): `android.view.SurfaceControlViewHost#release`"
-        errorLine1="                mSurfaceControlViewHost.release();"
-        errorLine2="                                        ~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java"
-            line="149"
-            column="41"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level R (current min is 26): `android.graphics.Outline#setPath`"
-        errorLine1="        outline.setPath(mPath);"
-        errorLine2="                ~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/popup/RoundedArrowDrawable.java"
-            line="88"
-            column="17"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 26): `new android.widget.EdgeEffect`"
-        errorLine1="                ? new EdgeEffect(context, attrs) : new EdgeEffect(context);"
-        errorLine2="                  ~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/views/SpringRelativeLayout.java"
-            line="49"
-            column="19"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 26): `new android.widget.EdgeEffect`"
-        errorLine1="                ? new EdgeEffect(context, attrs) : new EdgeEffect(context);"
-        errorLine2="                  ~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/views/SpringRelativeLayout.java"
-            line="51"
-            column="19"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 29 (current min is 26): `android.view.WindowInsets#getTappableElementInsets`"
-        errorLine1="            return windowInsets.getTappableElementInsets().bottom > 0;"
-        errorLine2="                                ~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/graphics/SysUiScrim.java"
-            line="190"
-            column="33"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Field requires API level 29 (current min is 26): `android.graphics.Insets#bottom`"
-        errorLine1="            return windowInsets.getTappableElementInsets().bottom > 0;"
-        errorLine2="                   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/graphics/SysUiScrim.java"
-            line="190"
-            column="20"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Field requires API level 28 (current min is 26): `android.appwidget.AppWidgetProviderInfo#widgetFeatures`"
-        errorLine1="        int featureFlags = mProviderInfo.widgetFeatures;"
-        errorLine2="                           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/widget/WidgetAddFlowHandler.java"
-            line="93"
-            column="28"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 26): `android.appwidget.AppWidgetProviderInfo#loadDescription`"
-        errorLine1="            CharSequence description = mItem.widgetInfo.loadDescription(getContext());"
-        errorLine2="                                                        ~~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/widget/WidgetCell.java"
-            line="193"
-            column="57"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Field requires API level 31 (current min is 26): `android.appwidget.AppWidgetProviderInfo#previewLayout`"
-        errorLine1="                &amp;&amp; item.widgetInfo.previewLayout != Resources.ID_NULL) {"
-        errorLine2="                   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/widget/WidgetCell.java"
-            line="214"
-            column="20"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Field requires API level 31 (current min is 26): `android.appwidget.AppWidgetProviderInfo#previewLayout`"
-        errorLine1="            launcherAppWidgetProviderInfo.initialLayout = item.widgetInfo.previewLayout;"
-        errorLine2="                                                          ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/widget/WidgetCell.java"
-            line="222"
-            column="59"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level R (current min is 26): `android.view.View#getWindowInsetsController`"
-        errorLine1="        getWindowInsetsController().hide(WindowInsets.Type.ime());"
-        errorLine2="        ~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java"
-            line="558"
-            column="9"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level R (current min is 26): `android.view.WindowInsets.Type#ime`"
-        errorLine1="        getWindowInsetsController().hide(WindowInsets.Type.ime());"
-        errorLine2="                                                           ~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java"
-            line="558"
-            column="60"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level R (current min is 26): `android.view.WindowInsetsController#hide`"
-        errorLine1="        getWindowInsetsController().hide(WindowInsets.Type.ime());"
-        errorLine2="                                    ~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java"
-            line="558"
-            column="37"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level R (current min is 26): `java.util.List#of`"
-        errorLine1="            return new RecommendationTableData(List.of(), previewScale);"
-        errorLine2="                                                    ~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java"
-            line="139"
-            column="53"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Method reference requires API level 28 (current min is 26): `Person::getKey`"
-        errorLine1="            : Arrays.stream(persons).map(Person::getKey).sorted().toArray(String[]::new);"
-        errorLine2="                                         ~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/model/data/WorkspaceItemInfo.java"
-            line="178"
-            column="42"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 26): `android.appwidget.AppWidgetHostView#setColorResources`"
-        errorLine1="                setColorResources(mWallpaperColorResources);"
-        errorLine2="                ~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java"
-            line="528"
-            column="17"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 27 (current min is 26): `android.app.WallpaperManager#getWallpaperColors`"
-        errorLine1="                    : WallpaperManager.getInstance(context).getWallpaperColors(FLAG_SYSTEM);"
-        errorLine2="                                                            ~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java"
-            line="288"
-            column="61"/>
-    </issue>
-
-</issues>
diff --git a/lint-baseline-res-lib.xml b/lint-baseline-res-lib.xml
deleted file mode 100644
index e52f8fb..0000000
--- a/lint-baseline-res-lib.xml
+++ /dev/null
@@ -1,48 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.1.0" client="cli" variant="all" version="4.1.0">
-
-    <issue
-        id="NewApi"
-        message="`?android:attr/dialogCornerRadius` requires API level 28 (current min is 26)"
-        errorLine1="        android:topLeftRadius=&quot;?android:attr/dialogCornerRadius&quot;"
-        errorLine2="        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/res/drawable/add_item_dialog_background.xml"
-            line="6"
-            column="9"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="`?android:attr/dialogCornerRadius` requires API level 28 (current min is 26)"
-        errorLine1="        android:topRightRadius=&quot;?android:attr/dialogCornerRadius&quot; />"
-        errorLine2="        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/res/drawable/add_item_dialog_background.xml"
-            line="7"
-            column="9"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="`@android:style/Widget.DeviceDefault.Button.Colored` requires API level 28 (current min is 26)"
-        errorLine1="    &lt;style name=&quot;Widget.DeviceDefault.Button.Rounded.Colored&quot; parent=&quot;@android:style/Widget.DeviceDefault.Button.Colored&quot;>"
-        errorLine2="                                                              ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/res/values/styles.xml"
-            line="287"
-            column="63"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="`@android:dimen/system_app_widget_background_radius` requires API level 31 (current min is 26)"
-        errorLine1="    &lt;corners android:radius=&quot;@android:dimen/system_app_widget_background_radius&quot; />"
-        errorLine2="             ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/apps/Launcher3/res/drawable/widget_resize_frame.xml"
-            line="20"
-            column="14"/>
-    </issue>
-
-</issues>
diff --git a/lint-baseline.xml b/lint-baseline.xml
index 23a22be..fe005ca 100644
--- a/lint-baseline.xml
+++ b/lint-baseline.xml
@@ -1,148 +1,169 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.0.0-dev" type="baseline" dependencies="true" variant="all" version="8.0.0-dev">
+<issues format="6" by="lint 8.4.0-alpha01" type="baseline" client="" dependencies="true" name="" variant="all" version="8.4.0-alpha01">
 
     <issue
         id="NewApi"
-        message="Call requires API level 28 (current min is 26): `android.app.Person#getKey`">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/notification/NotificationKeyData.java"
-            line="72"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 29 (current min is 26): `android.content.res.Resources#getFloat`">
+        message="Call requires API level 29 (current min is 26): `android.content.res.Resources#getFloat`"
+        errorLine1="        return mContext.getResources().getFloat(resId);"
+        errorLine2="                                       ~~~~~~~~">
         <location
             file="packages/apps/Launcher3/src/com/android/launcher3/util/DynamicResource.java"
-            line="73"/>
+            line="73"
+            column="40"/>
     </issue>
 
     <issue
         id="NewApi"
-        message="Call requires API level 30 (current min is 26): `android.graphics.Outline#setPath`">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/popup/RoundedArrowDrawable.java"
-            line="114"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 30 (current min is 26): `android.view.View#getWindowInsetsController`">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java"
-            line="902"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 30 (current min is 26): `android.view.WindowInsets.Type#ime`">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java"
-            line="902"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 30 (current min is 26): `android.view.WindowInsetsController#hide`">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java"
-            line="902"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 26): `android.appwidget.AppWidgetHostView#resetColorResources`">
+        message="Call requires API level 31 (current min is 26): `android.appwidget.AppWidgetHostView#resetColorResources`"
+        errorLine1="            resetColorResources();"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~">
         <location
             file="packages/apps/Launcher3/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java"
-            line="114"/>
+            line="117"
+            column="13"/>
     </issue>
 
     <issue
         id="NewApi"
-        message="Call requires API level 31 (current min is 30): `android.appwidget.AppWidgetHostView#setColorResources`">
+        message="Call requires API level 31 (current min is 30): `android.appwidget.AppWidgetHostView#setColorResources`"
+        errorLine1="            view.setColorResources(mWallpaperColorResources);"
+        errorLine2="                 ~~~~~~~~~~~~~~~~~">
         <location
             file="packages/apps/Launcher3/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java"
-            line="415"/>
+            line="433"
+            column="18"/>
     </issue>
 
     <issue
         id="NewApi"
-        message="Field requires API level 28 (current min is 26): `android.appwidget.AppWidgetProviderInfo#widgetFeatures`">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/widget/WidgetAddFlowHandler.java"
-            line="93"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Field requires API level 29 (current min is 26): `android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction#ACTION_PAGE_LEFT`">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/PagedView.java"
-            line="1814"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Field requires API level 29 (current min is 26): `android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction#ACTION_PAGE_LEFT`">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/PagedView.java"
-            line="1824"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Field requires API level 29 (current min is 26): `android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction#ACTION_PAGE_RIGHT`">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/PagedView.java"
-            line="1815"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Field requires API level 29 (current min is 26): `android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction#ACTION_PAGE_RIGHT`">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/PagedView.java"
-            line="1823"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Method reference requires API level 28 (current min is 26): `Person::getKey`">
-        <location
-            file="packages/apps/Launcher3/src/com/android/launcher3/model/data/WorkspaceItemInfo.java"
-            line="195"/>
-    </issue>
-
-    <issue
-        id="NewApi"
-        message="Method reference requires API level 28 (current min is 26): `Person::getKey`">
+        message="Call requires API level 28 (current min is 26): `android.app.Person#getKey`"
+        errorLine1="        return people.stream().filter(person -&gt; person.getKey() != null)"
+        errorLine2="                                                       ~~~~~~">
         <location
             file="packages/apps/Launcher3/src/com/android/launcher3/notification/NotificationKeyData.java"
-            line="73"/>
+            line="72"
+            column="56"/>
     </issue>
 
     <issue
         id="NewApi"
-        message="`?android:attr/dialogCornerRadius` requires API level 28 (current min is 26)">
+        message="Method reference requires API level 28 (current min is 26): `Person::getKey`"
+        errorLine1="                .map(Person::getKey).sorted().toArray(String[]::new);"
+        errorLine2="                     ~~~~~~~~~~~~~~">
         <location
-            file="packages/apps/Launcher3/res/drawable/add_item_dialog_background.xml"
-            line="6"/>
+            file="packages/apps/Launcher3/src/com/android/launcher3/notification/NotificationKeyData.java"
+            line="73"
+            column="22"/>
     </issue>
 
     <issue
         id="NewApi"
-        message="`?android:attr/dialogCornerRadius` requires API level 28 (current min is 26)">
+        message="Field requires API level 29 (current min is 26): `android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction#ACTION_PAGE_LEFT`"
+        errorLine1="                AccessibilityNodeInfo.AccessibilityAction.ACTION_PAGE_LEFT"
+        errorLine2="                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
-            file="packages/apps/Launcher3/res/drawable/add_item_dialog_background.xml"
-            line="7"/>
+            file="packages/apps/Launcher3/src/com/android/launcher3/PagedView.java"
+            line="1814"
+            column="17"/>
     </issue>
 
     <issue
         id="NewApi"
-        message="`@android:dimen/system_app_widget_background_radius` requires API level 31 (current min is 26)">
+        message="Field requires API level 29 (current min is 26): `android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction#ACTION_PAGE_RIGHT`"
+        errorLine1="                : AccessibilityNodeInfo.AccessibilityAction.ACTION_PAGE_RIGHT);"
+        errorLine2="                  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
-            file="packages/apps/Launcher3/res/drawable/widget_resize_frame.xml"
-            line="20"/>
+            file="packages/apps/Launcher3/src/com/android/launcher3/PagedView.java"
+            line="1815"
+            column="19"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Field requires API level 29 (current min is 26): `android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction#ACTION_PAGE_RIGHT`"
+        errorLine1="                AccessibilityNodeInfo.AccessibilityAction.ACTION_PAGE_RIGHT"
+        errorLine2="                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/apps/Launcher3/src/com/android/launcher3/PagedView.java"
+            line="1823"
+            column="17"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Field requires API level 29 (current min is 26): `android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction#ACTION_PAGE_LEFT`"
+        errorLine1="                : AccessibilityNodeInfo.AccessibilityAction.ACTION_PAGE_LEFT);"
+        errorLine2="                  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/apps/Launcher3/src/com/android/launcher3/PagedView.java"
+            line="1824"
+            column="19"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 30 (current min is 26): `android.graphics.Outline#setPath`"
+        errorLine1="        outline.setPath(mPath);"
+        errorLine2="                ~~~~~~~">
+        <location
+            file="packages/apps/Launcher3/src/com/android/launcher3/popup/RoundedArrowDrawable.java"
+            line="114"
+            column="17"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Field requires API level 28 (current min is 26): `android.appwidget.AppWidgetProviderInfo#widgetFeatures`"
+        errorLine1="        int featureFlags = mProviderInfo.widgetFeatures;"
+        errorLine2="                           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/apps/Launcher3/src/com/android/launcher3/widget/WidgetAddFlowHandler.java"
+            line="93"
+            column="28"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 30 (current min is 26): `android.view.View#getWindowInsetsController`"
+        errorLine1="        WindowInsetsController insetsController = getWindowInsetsController();"
+        errorLine2="                                                  ~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/apps/Launcher3/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java"
+            line="820"
+            column="51"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 30 (current min is 26): `android.view.WindowInsets.Type#ime`"
+        errorLine1="            insetsController.hide(WindowInsets.Type.ime());"
+        errorLine2="                                                    ~~~">
+        <location
+            file="packages/apps/Launcher3/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java"
+            line="822"
+            column="53"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 30 (current min is 26): `android.view.WindowInsetsController#hide`"
+        errorLine1="            insetsController.hide(WindowInsets.Type.ime());"
+        errorLine2="                             ~~~~">
+        <location
+            file="packages/apps/Launcher3/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java"
+            line="822"
+            column="30"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Method reference requires API level 28 (current min is 26): `Person::getKey`"
+        errorLine1="            : Arrays.stream(persons).map(Person::getKey).sorted().toArray(String[]::new);"
+        errorLine2="                                         ~~~~~~~~~~~~~~">
+        <location
+            file="packages/apps/Launcher3/src/com/android/launcher3/model/data/WorkspaceItemInfo.java"
+            line="194"
+            column="42"/>
     </issue>
 
 </issues>
\ No newline at end of file
diff --git a/lint-baseline2.xml b/lint-baseline2.xml
new file mode 100644
index 0000000..84f1b15
--- /dev/null
+++ b/lint-baseline2.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="6" by="lint 8.4.0-alpha01" type="baseline" client="" dependencies="true" name="" variant="all" version="8.4.0-alpha01">
+
+    <issue
+        id="NewApi"
+        message="`?android:attr/dialogCornerRadius` requires API level 28 (current min is 26)"
+        errorLine1='        android:topLeftRadius="?android:attr/dialogCornerRadius"'
+        errorLine2="        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/apps/Launcher3/res/drawable/add_item_dialog_background.xml"
+            line="6"
+            column="9"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="`?android:attr/dialogCornerRadius` requires API level 28 (current min is 26)"
+        errorLine1='        android:topRightRadius="?android:attr/dialogCornerRadius" /&gt;'
+        errorLine2="        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/apps/Launcher3/res/drawable/add_item_dialog_background.xml"
+            line="7"
+            column="9"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="`@android:dimen/system_app_widget_background_radius` requires API level 31 (current min is 26)"
+        errorLine1='    &lt;corners android:radius="@android:dimen/system_app_widget_background_radius" /&gt;'
+        errorLine2="             ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/apps/Launcher3/res/drawable/widget_resize_frame.xml"
+            line="20"
+            column="14"/>
+    </issue>
+
+</issues>
\ No newline at end of file
diff --git a/quickstep/res/drawable-sw600dp-land/gesture_tutorial_back_step_shape.xml b/quickstep/res/drawable-sw600dp-land/gesture_tutorial_back_step_shape.xml
index a07aeaa..8b4127a 100644
--- a/quickstep/res/drawable-sw600dp-land/gesture_tutorial_back_step_shape.xml
+++ b/quickstep/res/drawable-sw600dp-land/gesture_tutorial_back_step_shape.xml
@@ -16,7 +16,8 @@
     android:width="84dp"
     android:height="208dp"
     android:viewportWidth="84"
-    android:viewportHeight="208">
+    android:viewportHeight="208"
+    android:autoMirrored="true">
   <path
       android:pathData="M24.53,169.2L32.09,165.56C77.7,143.55 77.7,64.45 32.09,42.35L24.53,38.71C14.55,33.95 6.06,25.56 0,14.92V193.08C6.06,182.44 14.55,174.05 24.53,169.2Z"
       android:fillColor="?attr/onSurfaceBack"/>
diff --git a/quickstep/res/drawable-sw720dp-land/gesture_tutorial_back_step_shape.xml b/quickstep/res/drawable-sw720dp-land/gesture_tutorial_back_step_shape.xml
index e20458e..3a11f21 100644
--- a/quickstep/res/drawable-sw720dp-land/gesture_tutorial_back_step_shape.xml
+++ b/quickstep/res/drawable-sw720dp-land/gesture_tutorial_back_step_shape.xml
@@ -16,7 +16,8 @@
     android:width="122dp"
     android:height="303dp"
     android:viewportWidth="122"
-    android:viewportHeight="303">
+    android:viewportHeight="303"
+    android:autoMirrored="true">
   <path
       android:pathData="M35.65,245.9L46.63,240.61C112.92,208.62 112.92,93.67 46.63,61.54L35.65,56.26C21.15,49.34 8.81,37.14 0,21.69V280.6C8.81,265.15 21.15,252.95 35.65,245.9Z"
       android:fillColor="?attr/onSurfaceBack"/>
diff --git a/quickstep/res/drawable/gesture_tutorial_back_step_shape.xml b/quickstep/res/drawable/gesture_tutorial_back_step_shape.xml
index 9389340..c217be2 100644
--- a/quickstep/res/drawable/gesture_tutorial_back_step_shape.xml
+++ b/quickstep/res/drawable/gesture_tutorial_back_step_shape.xml
@@ -16,7 +16,8 @@
     android:width="83dp"
     android:height="208dp"
     android:viewportWidth="83"
-    android:viewportHeight="208">
+    android:viewportHeight="208"
+    android:autoMirrored="true">
   <path
       android:pathData="M23.53,169.2L31.09,165.56C76.7,143.55 76.7,64.45 31.09,42.35L23.53,38.71C13.55,33.95 5.06,25.56 -1,14.92V193.08C5.06,182.44 13.55,174.05 23.53,169.2Z"
       android:fillColor="?attr/onSurfaceBack"/>
diff --git a/quickstep/res/values-de/strings.xml b/quickstep/res/values-de/strings.xml
index 3d4ce43..12c72e1 100644
--- a/quickstep/res/values-de/strings.xml
+++ b/quickstep/res/values-de/strings.xml
@@ -65,7 +65,7 @@
     <string name="home_gesture_intro_title" msgid="836590312858441830">"Den Startbildschirm aufrufen"</string>
     <string name="home_gesture_intro_subtitle" msgid="2632238748497975326">"Wenn du zum Startbildschirm gehen möchtest, wische einfach vom unteren Displayrand nach oben."</string>
     <string name="home_gesture_spoken_intro_subtitle" msgid="1030987707382031750">"Wische mit zwei Fingern vom unteren Displayrand nach oben. So gelangst du immer zum Startbildschirm."</string>
-    <string name="home_gesture_tutorial_title" msgid="3126834347496917376">"Zum StartU+00AD­bildschirm"</string>
+    <string name="home_gesture_tutorial_title" msgid="3126834347496917376">"Zum Start­bildschirm"</string>
     <string name="home_gesture_tutorial_subtitle" msgid="7245995490408668778">"Wische vom unteren Displayrand nach oben"</string>
     <string name="home_gesture_tutorial_success" msgid="1736295017642244751">"Gut gemacht!"</string>
     <string name="overview_gesture_feedback_swipe_too_far_from_edge" msgid="6402349235265407385">"Wische vom unteren Displayrand nach oben"</string>
diff --git a/quickstep/res/values-es/strings.xml b/quickstep/res/values-es/strings.xml
index bb2f533..893b1c8 100644
--- a/quickstep/res/values-es/strings.xml
+++ b/quickstep/res/values-es/strings.xml
@@ -86,7 +86,7 @@
     <string name="gesture_tutorial_nice" msgid="2936275692616928280">"¡Muy bien!"</string>
     <string name="gesture_tutorial_step" msgid="1279786122817620968">"Tutorial <xliff:g id="CURRENT">%1$d</xliff:g>/<xliff:g id="TOTAL">%2$d</xliff:g>"</string>
     <string name="allset_title" msgid="5021126669778966707">"¡Ya está!"</string>
-    <string name="allset_hint" msgid="459504134589971527">"Desliza el dedo hacia arriba para ir a la pantalla de inicio"</string>
+    <string name="allset_hint" msgid="459504134589971527">"Desliza hacia arriba para ir a la pantalla de inicio"</string>
     <string name="allset_button_hint" msgid="2395219947744706291">"Toca el botón de inicio para ir a la pantalla de inicio"</string>
     <string name="allset_description_generic" msgid="5385500062202019855">"Ya puedes empezar a usar tu <xliff:g id="DEVICE">%1$s</xliff:g>"</string>
     <string name="default_device_name" msgid="6660656727127422487">"dispositivo"</string>
diff --git a/quickstep/res/values-fa/strings.xml b/quickstep/res/values-fa/strings.xml
index c8da620..91e4ad5 100644
--- a/quickstep/res/values-fa/strings.xml
+++ b/quickstep/res/values-fa/strings.xml
@@ -109,7 +109,7 @@
     <string name="taskbar_edu_splitscreen" msgid="5605512479258053350">"برای استفاده هم‌زمان از ۲ برنامه، یک برنامه را به کناری بکشید"</string>
     <string name="taskbar_edu_stashing" msgid="5645461372669217294">"برای نمایش «نوار وظیفه»، انگشتتان را آهسته به‌بالا بکشید"</string>
     <string name="taskbar_edu_suggestions" msgid="8215044496435527982">"براساس روال‌هایتان، پیشنهاد برنامه دریافت کنید"</string>
-    <string name="taskbar_edu_pinning" msgid="6708550858580071558">"برای سنجاق کردن «نوار وظیفه»، جداکننده را چند ثانیه فشار دهید"</string>
+    <string name="taskbar_edu_pinning" msgid="6708550858580071558">"جداکننده را چند ثانیه فشار دهید تا «نوار وظیفه» سنجاق شود"</string>
     <string name="taskbar_edu_features" msgid="3320337287472848162">"با «نوار وظیفه» می‌توانید کارهای بیشتر انجام دهید"</string>
     <string name="taskbar_edu_close" msgid="887022990168191073">"بستن"</string>
     <string name="taskbar_edu_done" msgid="6880178093977704569">"تمام"</string>
diff --git a/quickstep/res/values-fr/strings.xml b/quickstep/res/values-fr/strings.xml
index df6cad4..789ef13 100644
--- a/quickstep/res/values-fr/strings.xml
+++ b/quickstep/res/values-fr/strings.xml
@@ -109,7 +109,7 @@
     <string name="taskbar_edu_splitscreen" msgid="5605512479258053350">"Faites glisser une appli sur le côté pour en utiliser 2 à la fois"</string>
     <string name="taskbar_edu_stashing" msgid="5645461372669217294">"Balayez lentement vers haut pour afficher barre des tâches"</string>
     <string name="taskbar_edu_suggestions" msgid="8215044496435527982">"Obtenez des suggestions d\'applis basées sur vos habitudes"</string>
-    <string name="taskbar_edu_pinning" msgid="6708550858580071558">"Appui de manière prolongée sur le séparateur pour épingler la barre des tâches"</string>
+    <string name="taskbar_edu_pinning" msgid="6708550858580071558">"Appui prolongé sur le séparateur pour épingler la barre des tâches"</string>
     <string name="taskbar_edu_features" msgid="3320337287472848162">"Faites-en plus avec la barre des tâches"</string>
     <string name="taskbar_edu_close" msgid="887022990168191073">"Fermer"</string>
     <string name="taskbar_edu_done" msgid="6880178093977704569">"OK"</string>
diff --git a/quickstep/res/values-zh-rCN/strings.xml b/quickstep/res/values-zh-rCN/strings.xml
index 2c740b7..4d21108 100644
--- a/quickstep/res/values-zh-rCN/strings.xml
+++ b/quickstep/res/values-zh-rCN/strings.xml
@@ -114,7 +114,7 @@
     <string name="taskbar_edu_close" msgid="887022990168191073">"关闭"</string>
     <string name="taskbar_edu_done" msgid="6880178093977704569">"完成"</string>
     <string name="taskbar_button_home" msgid="2151398979630664652">"主屏幕"</string>
-    <string name="taskbar_button_a11y" msgid="5241161324875094465">"无障碍"</string>
+    <string name="taskbar_button_a11y" msgid="5241161324875094465">"无障碍功能"</string>
     <string name="taskbar_button_back" msgid="8558862226461164514">"返回"</string>
     <string name="taskbar_button_ime_switcher" msgid="1730244360907588541">"IME 切换器"</string>
     <string name="taskbar_button_recents" msgid="7273376136216613134">"最近用过"</string>
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/HomeTransitionController.java b/quickstep/src/com/android/launcher3/HomeTransitionController.java
index b264115..2b50283 100644
--- a/quickstep/src/com/android/launcher3/HomeTransitionController.java
+++ b/quickstep/src/com/android/launcher3/HomeTransitionController.java
@@ -28,19 +28,16 @@
  */
 public class HomeTransitionController {
 
-    private final QuickstepLauncher mLauncher;
+    @Nullable private QuickstepLauncher mLauncher;
     @Nullable private IHomeTransitionListener mHomeTransitionListener;
 
-    public HomeTransitionController(QuickstepLauncher launcher) {
+    public void registerHomeTransitionListener(QuickstepLauncher launcher) {
         mLauncher = launcher;
-    }
-
-    public void registerHomeTransitionListener() {
         mHomeTransitionListener = new IHomeTransitionListener.Stub() {
             @Override
             public void onHomeVisibilityChanged(boolean isVisible) {
                 MAIN_EXECUTOR.execute(() -> {
-                    if (mLauncher.getTaskbarUIController() != null) {
+                    if (mLauncher != null && mLauncher.getTaskbarUIController() != null) {
                         mLauncher.getTaskbarUIController().onLauncherVisibilityChanged(isVisible);
                     }
                 });
@@ -53,5 +50,6 @@
     public void unregisterHomeTransitionListener() {
         SystemUiProxy.INSTANCE.get(mLauncher).setHomeTransitionListener(null);
         mHomeTransitionListener = null;
+        mLauncher = null;
     }
 }
diff --git a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
index 4898761..8c9dc6a 100644
--- a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
+++ b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
@@ -64,8 +64,6 @@
 import static com.android.launcher3.views.FloatingIconView.getFloatingIconView;
 import static com.android.quickstep.TaskAnimationManager.ENABLE_SHELL_TRANSITIONS;
 import static com.android.quickstep.TaskViewUtils.findTaskViewToLaunch;
-import static com.android.systemui.shared.system.InteractionJankMonitorWrapper.CUJ_APP_CLOSE_TO_HOME;
-import static com.android.systemui.shared.system.InteractionJankMonitorWrapper.CUJ_APP_CLOSE_TO_HOME_FALLBACK;
 import static com.android.systemui.shared.system.QuickStepContract.getWindowCornerRadius;
 import static com.android.systemui.shared.system.QuickStepContract.supportsRoundedCornersOnWindows;
 
@@ -117,6 +115,7 @@
 import androidx.annotation.Nullable;
 import androidx.core.graphics.ColorUtils;
 
+import com.android.internal.jank.Cuj;
 import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener;
 import com.android.launcher3.LauncherAnimationRunner.RemoteAnimationFactory;
 import com.android.launcher3.anim.AnimationSuccessListener;
@@ -1088,7 +1087,12 @@
         }
 
         backgroundRadiusAnim.addListener(
-                AnimatorListeners.forEndCallback(depthController::dispose));
+                AnimatorListeners.forEndCallback(() -> {
+                    // reset the depth to match the main depth controller's depth
+                    depthController.stateDepth
+                            .setValue(mLauncher.getDepthController().stateDepth.getValue());
+                    depthController.dispose();
+                }));
 
         return backgroundRadiusAnim;
     }
@@ -1162,6 +1166,7 @@
         SystemUiProxy.INSTANCE.get(mLauncher)
                 .registerRemoteTransition(mLauncherOpenTransition, homeCheck);
         if (mBackAnimationController != null) {
+            mBackAnimationController.registerComponentCallbacks();
             mBackAnimationController.registerBackCallbacks(mHandler);
         }
     }
@@ -1169,6 +1174,7 @@
     public void onActivityDestroyed() {
         unregisterRemoteAnimations();
         unregisterRemoteTransitions();
+        mLauncher.removeOnDeviceProfileChangeListener(this);
         SystemUiProxy.INSTANCE.get(mLauncher).setStartingWindowListener(null);
         ORDERED_BG_EXECUTOR.execute(() -> mLauncher.getContentResolver()
                 .unregisterContentObserver(mAnimationRemovalObserver));
@@ -1201,6 +1207,7 @@
         mWallpaperOpenTransitionRunner = null;
         if (mBackAnimationController != null) {
             mBackAnimationController.unregisterBackCallbacks();
+            mBackAnimationController.unregisterComponentCallbacks();
             mBackAnimationController = null;
         }
     }
@@ -1291,7 +1298,7 @@
     /**
      * Returns view on launcher that corresponds to the closing app in the list of app targets
      */
-    private @Nullable View findLauncherView(RemoteAnimationTarget[] appTargets) {
+    public @Nullable View findLauncherView(RemoteAnimationTarget[] appTargets) {
         for (RemoteAnimationTarget appTarget : appTargets) {
             if (appTarget.mode == MODE_CLOSING) {
                 View launcherView = findLauncherView(appTarget);
@@ -1608,7 +1615,7 @@
                 || mLauncher.getWorkspace().isOverlayShown()
                 || shouldPlayFallbackClosingAnimation(appTargets);
 
-        boolean playWorkspaceReveal = true;
+        boolean playWorkspaceReveal = !fromPredictiveBack;
         boolean skipAllAppsScale = false;
         if (fromUnlock) {
             anim.play(getUnlockWindowAnimator(appTargets, wallpaperTargets));
@@ -1650,9 +1657,10 @@
         // targets list because it is already visible). In that case, we force
         // invisibility on touch down, and only reset it after the animation to home
         // is initialized.
-        if (launcherIsForceInvisibleOrOpening) {
+        if (launcherIsForceInvisibleOrOpening || fromPredictiveBack) {
             addCujInstrumentation(anim, playFallBackAnimation
-                    ? CUJ_APP_CLOSE_TO_HOME_FALLBACK : CUJ_APP_CLOSE_TO_HOME);
+                    ? Cuj.CUJ_LAUNCHER_APP_CLOSE_TO_HOME_FALLBACK
+                    : Cuj.CUJ_LAUNCHER_APP_CLOSE_TO_HOME);
 
             anim.addListener(new AnimatorListenerAdapter() {
                 @Override
@@ -1666,7 +1674,7 @@
             // Only register the content animation for cancellation when state changes
             mLauncher.getStateManager().setCurrentAnimation(anim);
 
-            if (mLauncher.isInState(LauncherState.ALL_APPS)) {
+            if (mLauncher.isInState(LauncherState.ALL_APPS) && !fromPredictiveBack) {
                 Pair<AnimatorSet, Runnable> contentAnimator =
                         getLauncherContentAnimator(false, LAUNCHER_RESUME_START_DELAY,
                                 skipAllAppsScale);
@@ -1677,10 +1685,8 @@
                         contentAnimator.second.run();
                     }
                 });
-            } else {
-                if (playWorkspaceReveal) {
+            } else if (playWorkspaceReveal) {
                     anim.play(new WorkspaceRevealAnim(mLauncher, false).getAnimators());
-                }
             }
         }
 
@@ -1768,19 +1774,18 @@
             if (launchingFromWidget) {
                 composeWidgetLaunchAnimator(anim, (LauncherAppWidgetHostView) mV, appTargets,
                         wallpaperTargets, nonAppTargets, launcherClosing);
-                addCujInstrumentation(
-                        anim, InteractionJankMonitorWrapper.CUJ_APP_LAUNCH_FROM_WIDGET);
+                addCujInstrumentation(anim, Cuj.CUJ_LAUNCHER_APP_LAUNCH_FROM_WIDGET);
                 skipFirstFrame = true;
             } else if (launchingFromRecents) {
                 composeRecentsLaunchAnimator(anim, mV, appTargets, wallpaperTargets, nonAppTargets,
                         launcherClosing);
                 addCujInstrumentation(
-                        anim, InteractionJankMonitorWrapper.CUJ_APP_LAUNCH_FROM_RECENTS);
+                        anim, Cuj.CUJ_LAUNCHER_APP_LAUNCH_FROM_RECENTS);
                 skipFirstFrame = true;
             } else {
                 composeIconLaunchAnimator(anim, mV, appTargets, wallpaperTargets, nonAppTargets,
                         launcherClosing);
-                addCujInstrumentation(anim, InteractionJankMonitorWrapper.CUJ_APP_LAUNCH_FROM_ICON);
+                addCujInstrumentation(anim, Cuj.CUJ_LAUNCHER_APP_LAUNCH_FROM_ICON);
                 skipFirstFrame = false;
             }
 
diff --git a/quickstep/src/com/android/launcher3/WidgetPickerActivity.java b/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
index 43716ab..b9b4461 100644
--- a/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
+++ b/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
@@ -16,13 +16,19 @@
 
 package com.android.launcher3;
 
+import static android.content.ClipDescription.MIMETYPE_TEXT_INTENT;
 import static android.view.WindowInsets.Type.navigationBars;
 import static android.view.WindowInsets.Type.statusBars;
 
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
 
+import android.appwidget.AppWidgetProviderInfo;
+import android.content.ClipData;
+import android.content.ClipDescription;
+import android.content.Intent;
 import android.os.Bundle;
+import android.view.View;
 import android.view.WindowInsetsController;
 import android.view.WindowManager;
 
@@ -32,6 +38,7 @@
 import com.android.launcher3.model.WidgetsModel;
 import com.android.launcher3.popup.PopupDataProvider;
 import com.android.launcher3.widget.BaseWidgetSheet;
+import com.android.launcher3.widget.WidgetCell;
 import com.android.launcher3.widget.model.WidgetsListBaseEntry;
 import com.android.launcher3.widget.picker.WidgetsFullSheet;
 
@@ -39,6 +46,14 @@
 
 /** An Activity that can host Launcher's widget picker. */
 public class WidgetPickerActivity extends BaseActivity {
+    /**
+     * Name of the extra that indicates that a widget being dragged.
+     *
+     * <p>When set to "true" in the result of startActivityForResult, the client that launched the
+     * picker knows that activity was closed due to pending drag.
+     */
+    private static final String EXTRA_IS_PENDING_WIDGET_DRAG = "is_pending_widget_drag";
+
     private SimpleDragLayer<WidgetPickerActivity> mDragLayer;
     private WidgetsModel mModel;
     private final PopupDataProvider mPopupDataProvider = new PopupDataProvider(i -> {});
@@ -81,12 +96,72 @@
         return mDragLayer;
     }
 
+    @Override
+    public View.OnClickListener getItemOnClickListener() {
+        return v -> {
+            final AppWidgetProviderInfo info =
+                    (v instanceof WidgetCell) ? ((WidgetCell) v).getWidgetItem().widgetInfo : null;
+            if (info == null || info.provider == null) {
+                return;
+            }
+
+            setResult(RESULT_OK, new Intent()
+                    .putExtra(Intent.EXTRA_COMPONENT_NAME, info.provider)
+                    .putExtra(Intent.EXTRA_USER, info.getProfile()));
+
+            finish();
+        };
+    }
+
+    @Override
+    public View.OnLongClickListener getAllAppsItemLongClickListener() {
+        return view -> {
+            if (!(view instanceof WidgetCell widgetCell)) return false;
+
+            if (widgetCell.getWidgetView().getDrawable() == null
+                    && widgetCell.getAppWidgetHostViewPreview() == null) {
+                // The widget preview hasn't been loaded; so, we abort the drag.
+                return false;
+            }
+
+            final AppWidgetProviderInfo info = widgetCell.getWidgetItem().widgetInfo;
+            if (info == null || info.provider == null) {
+                return false;
+            }
+
+            ClipData clipData = new ClipData(
+                    new ClipDescription(
+                            /* label= */ "", // not displayed anywhere; so, set to empty.
+                            new String[]{MIMETYPE_TEXT_INTENT}
+                    ),
+                    new ClipData.Item(new Intent()
+                            .putExtra(Intent.EXTRA_USER, info.getProfile())
+                            .putExtra(Intent.EXTRA_COMPONENT_NAME, info.provider))
+            );
+
+            // Set result indicating activity was closed due a widget being dragged.
+            setResult(RESULT_OK, new Intent()
+                    .putExtra(EXTRA_IS_PENDING_WIDGET_DRAG, true));
+
+            // DRAG_FLAG_GLOBAL permits dragging data beyond app window.
+            return view.startDragAndDrop(
+                    clipData,
+                    new View.DragShadowBuilder(view),
+                    /* myLocalState= */ null,
+                    View.DRAG_FLAG_GLOBAL
+            );
+        };
+    }
+
     private void refreshAndBindWidgets() {
         MODEL_EXECUTOR.execute(() -> {
             LauncherAppState app = LauncherAppState.getInstance(this);
             mModel.update(app, null);
             final ArrayList<WidgetsListBaseEntry> widgets =
-                    mModel.getWidgetsListForPicker(app.getContext());
+                    mModel.getFilteredWidgetsListForPicker(
+                            app.getContext(),
+                            /*widgetItemFilter=*/ item -> item.widgetInfo != null
+                    );
             MAIN_EXECUTOR.execute(() -> mPopupDataProvider.setAllWidgets(widgets));
         });
     }
diff --git a/quickstep/src/com/android/launcher3/appprediction/PredictionRowView.java b/quickstep/src/com/android/launcher3/appprediction/PredictionRowView.java
index 1440498..f012197 100644
--- a/quickstep/src/com/android/launcher3/appprediction/PredictionRowView.java
+++ b/quickstep/src/com/android/launcher3/appprediction/PredictionRowView.java
@@ -23,6 +23,7 @@
 import android.util.AttributeSet;
 import android.view.LayoutInflater;
 import android.view.View;
+import android.view.ViewGroup;
 import android.widget.LinearLayout;
 
 import androidx.annotation.NonNull;
@@ -31,6 +32,7 @@
 import com.android.launcher3.BubbleTextView;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener;
+import com.android.launcher3.Flags;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.allapps.FloatingHeaderRow;
@@ -54,6 +56,11 @@
 
     private final T mActivityContext;
     private int mNumPredictedAppsPerRow;
+    // Vertical padding of the icon that contributes to the expected cell height.
+    private final int mVerticalPadding;
+    // Extra padding that is used in the top app rows (prediction and search) that is not used in
+    // the regular A-Z list. This only applies to single line label.
+    private final int mTopRowExtraHeight;
 
     // Helper to drawing the focus indicator.
     private final FocusIndicatorHelper mFocusHelper;
@@ -76,6 +83,10 @@
         mFocusHelper = new SimpleFocusIndicatorHelper(this);
         mActivityContext = ActivityContext.lookupContext(context);
         mNumPredictedAppsPerRow = mActivityContext.getDeviceProfile().numShownAllAppsColumns;
+        mTopRowExtraHeight = getResources().getDimensionPixelSize(
+                R.dimen.all_apps_search_top_row_extra_height);
+        mVerticalPadding = getResources().getDimensionPixelSize(
+                R.dimen.all_apps_predicted_icon_vertical_padding);
         updateVisibility();
     }
 
@@ -124,13 +135,11 @@
         int iconHeight = deviceProfile.allAppsIconSizePx;
         int iconPadding = deviceProfile.allAppsIconDrawablePaddingPx;
         int textHeight = Utilities.calculateTextHeight(deviceProfile.allAppsIconTextSizePx);
-        int verticalPadding = getResources().getDimensionPixelSize(
-                R.dimen.all_apps_predicted_icon_vertical_padding);
-        int totalHeight = iconHeight + iconPadding + textHeight + verticalPadding * 2;
-        if (FeatureFlags.enableTwolineAllapps()) {
-            // Add extra textHeight to the existing total height.
-            totalHeight += textHeight;
-        }
+        int totalHeight = iconHeight + iconPadding + textHeight + mVerticalPadding * 2;
+        // Prediction row height will be 4dp bigger than the regular apps in A-Z list when two line
+        // is not enabled. Otherwise, the extra height will increase by just the textHeight.
+        int extraHeight = FeatureFlags.enableTwolineAllapps() ? textHeight : mTopRowExtraHeight;
+        totalHeight += extraHeight;
         return getVisibility() == GONE ? 0 : totalHeight + getPaddingTop() + getPaddingBottom();
     }
 
@@ -199,8 +208,12 @@
                 icon.setOnFocusChangeListener(mFocusHelper);
 
                 LayoutParams lp = (LayoutParams) icon.getLayoutParams();
-                // Ensure the all apps icon height matches the workspace icons in portrait mode.
-                lp.height = mActivityContext.getDeviceProfile().allAppsCellHeightPx;
+                if (Flags.enableFocusOutline()) {
+                    lp.height = ViewGroup.LayoutParams.MATCH_PARENT;
+                } else {
+                    // Ensure the all apps icon height matches the workspace icons in portrait mode.
+                    lp.height = mActivityContext.getDeviceProfile().allAppsCellHeightPx;
+                }
                 lp.width = 0;
                 lp.weight = 1;
                 addView(icon);
diff --git a/quickstep/src/com/android/launcher3/model/AppEventProducer.java b/quickstep/src/com/android/launcher3/model/AppEventProducer.java
index 0dde1bd..e17fc88 100644
--- a/quickstep/src/com/android/launcher3/model/AppEventProducer.java
+++ b/quickstep/src/com/android/launcher3/model/AppEventProducer.java
@@ -25,6 +25,7 @@
 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_PREDICTION;
 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION;
 import static com.android.launcher3.logger.LauncherAtomExtensions.ExtendedContainers.ContainerCase.DEVICE_SEARCH_RESULT_CONTAINER;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_APP_LAUNCH_DRAGDROP;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_APP_LAUNCH_TAP;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_DISMISS_PREDICTION_UNDO;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_FOLDER_CONVERTED_TO_ICON;
@@ -37,6 +38,7 @@
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ONRESUME;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_QUICKSWITCH_LEFT;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_QUICKSWITCH_RIGHT;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_DONT_SUGGEST_APP_TAP;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_LAUNCH_SWIPE_DOWN;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_LAUNCH_TAP;
 import static com.android.launcher3.model.PredictionHelper.isTrackedForHotseatPrediction;
@@ -71,12 +73,13 @@
 import com.android.launcher3.logging.StatsLogManager.EventEnum;
 import com.android.launcher3.pm.UserCache;
 import com.android.launcher3.shortcuts.ShortcutRequest;
+import com.android.launcher3.util.UserIconInfo;
 import com.android.quickstep.logging.StatsLogCompatManager.StatsLogConsumer;
+import com.android.systemui.shared.system.SysUiStatsLog;
 
 import java.util.Locale;
 import java.util.Optional;
 import java.util.function.ObjIntConsumer;
-import java.util.function.Predicate;
 
 /**
  * Utility class to track stats log and emit corresponding app events
@@ -132,9 +135,11 @@
                 || event == LAUNCHER_TASK_LAUNCH_SWIPE_DOWN
                 || event == LAUNCHER_TASK_LAUNCH_TAP
                 || event == LAUNCHER_QUICKSWITCH_RIGHT
-                || event == LAUNCHER_QUICKSWITCH_LEFT) {
+                || event == LAUNCHER_QUICKSWITCH_LEFT
+                || event == LAUNCHER_APP_LAUNCH_DRAGDROP) {
             sendEvent(atomInfo, ACTION_LAUNCH, CONTAINER_PREDICTION);
-        } else if (event == LAUNCHER_ITEM_DROPPED_ON_DONT_SUGGEST) {
+        } else if (event == LAUNCHER_ITEM_DROPPED_ON_DONT_SUGGEST
+                || event == LAUNCHER_SYSTEM_SHORTCUT_DONT_SUGGEST_APP_TAP) {
             sendEvent(atomInfo, ACTION_DISMISS, CONTAINER_PREDICTION);
         } else if (event == LAUNCHER_ITEM_DRAG_STARTED) {
             mLastDragItem = atomInfo;
@@ -186,14 +191,13 @@
     }
 
     @Nullable
-    private AppTarget toAppTarget(LauncherAtom.ItemInfo info) {
-        UserHandle userHandle = Process.myUserHandle();
-        if (info.getIsWork()) {
-            userHandle = UserCache.INSTANCE.get(mContext).getUserProfiles().stream()
-                    .filter(((Predicate<UserHandle>) userHandle::equals).negate())
-                    .findAny()
-                    .orElse(null);
-        }
+    AppTarget toAppTarget(LauncherAtom.ItemInfo info) {
+        int iconInfoType = getIconInfoTypeFromItemInfo(info);
+        UserCache userCache = UserCache.INSTANCE.get(mContext);
+        UserHandle userHandle = userCache.getUserProfiles().stream()
+                .filter(user -> userCache.getUserInfo(user).type == iconInfoType)
+                .findFirst()
+                .orElse(null);
         if (userHandle == null) {
             return null;
         }
@@ -287,6 +291,9 @@
             case SHORTCUTS_CONTAINER: {
                 return "deep-shortcuts";
             }
+            case TASK_BAR_CONTAINER: {
+                return "taskbar";
+            }
             case FOLDER: {
                 FolderContainer fc = ci.getFolder();
                 switch (fc.getParentContainerCase()) {
@@ -323,4 +330,16 @@
         return TextUtils.isEmpty(componentNameString)
                 ? null : ComponentName.unflattenFromString(componentNameString);
     }
+
+    private int getIconInfoTypeFromItemInfo(LauncherAtom.ItemInfo info) {
+        int userType = info.getUserType();
+        return switch (userType) {
+            case SysUiStatsLog.LAUNCHER_UICHANGED__USER_TYPE__TYPE_WORK -> UserIconInfo.TYPE_WORK;
+            case SysUiStatsLog.LAUNCHER_UICHANGED__USER_TYPE__TYPE_CLONED ->
+                    UserIconInfo.TYPE_CLONED;
+            case SysUiStatsLog.LAUNCHER_UICHANGED__USER_TYPE__TYPE_PRIVATE ->
+                    UserIconInfo.TYPE_PRIVATE;
+            default -> UserIconInfo.TYPE_MAIN;
+        };
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java
index a7e8118..b7e1092 100644
--- a/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java
+++ b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java
@@ -15,6 +15,8 @@
  */
 package com.android.launcher3.statehandlers;
 
+import static android.view.View.VISIBLE;
+
 import static com.android.launcher3.LauncherState.BACKGROUND_APP;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.quickstep.views.DesktopTaskView.isDesktopModeSupported;
@@ -269,6 +271,9 @@
         if (dragLayer != null) {
             dragLayer.setVisibility(visibility);
         }
+        if (mLauncher instanceof QuickstepLauncher ql && ql.getTaskbarUIController() != null) {
+            ql.getTaskbarUIController().onLauncherVisibilityChanged(visibility == VISIBLE);
+        }
     }
 
     private void markLauncherPaused() {
diff --git a/quickstep/src/com/android/launcher3/taskbar/DesktopNavbarButtonsViewController.java b/quickstep/src/com/android/launcher3/taskbar/DesktopNavbarButtonsViewController.java
index 29c5204..0a9dfff 100644
--- a/quickstep/src/com/android/launcher3/taskbar/DesktopNavbarButtonsViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/DesktopNavbarButtonsViewController.java
@@ -18,12 +18,15 @@
 import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_NOTIFICATIONS;
 import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_QUICK_SETTINGS;
 
+import android.content.Context;
 import android.content.pm.ActivityInfo.Config;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.FrameLayout;
 
+import androidx.annotation.Nullable;
+
 import com.android.launcher3.R;
 
 /**
@@ -40,8 +43,8 @@
     private TaskbarControllers mControllers;
 
     public DesktopNavbarButtonsViewController(TaskbarActivityContext context,
-            FrameLayout navButtonsView) {
-        super(context, navButtonsView);
+            @Nullable Context navigationBarPanelContext, FrameLayout navButtonsView) {
+        super(context, navigationBarPanelContext, navButtonsView);
         mContext = context;
         mNavButtonsView = navButtonsView;
         mNavButtonContainer = mNavButtonsView.findViewById(R.id.end_nav_buttons);
diff --git a/quickstep/src/com/android/launcher3/taskbar/FallbackTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/FallbackTaskbarUIController.java
index f981610..535c8ec 100644
--- a/quickstep/src/com/android/launcher3/taskbar/FallbackTaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/FallbackTaskbarUIController.java
@@ -105,6 +105,13 @@
     }
 
     @Override
+    protected boolean isInOverview() {
+        TopTaskTracker.CachedTaskInfo topTask = TopTaskTracker.INSTANCE
+                .get(mControllers.taskbarActivityContext).getCachedTopTask(true);
+        return topTask.isRecentsTask();
+    }
+
+    @Override
     public RecentsView getRecentsView() {
         return mRecentsActivity.getOverviewPanel();
     }
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
index f58fd45..5caf004 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
@@ -32,6 +32,7 @@
 import com.android.quickstep.util.GroupTask;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.recents.model.ThumbnailData;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
@@ -115,8 +116,15 @@
                         && desktopController.areFreeformTasksVisible();
 
         if (mModel.isTaskListValid(mTaskListChangeId)) {
-            mQuickSwitchViewController.openQuickSwitchView(mTasks,
-                    mNumHiddenTasks, /* updateTasks= */ false, currentFocusedIndex, onDesktop);
+            // When we are opening the KQS with no focus override, check if the first task is
+            // running. If not, focus that first task.
+            mQuickSwitchViewController.openQuickSwitchView(
+                    mTasks,
+                    mNumHiddenTasks,
+                    /* updateTasks= */ false,
+                    currentFocusedIndex == -1 && !mControllerCallbacks.isFirstTaskRunning()
+                            ? 0 : currentFocusedIndex,
+                    onDesktop);
             return;
         }
 
@@ -126,8 +134,15 @@
             } else {
                 processLoadedTasks(tasks);
             }
-            mQuickSwitchViewController.openQuickSwitchView(mTasks,
-                    mNumHiddenTasks, /* updateTasks= */ true, currentFocusedIndex, onDesktop);
+            // Check if the first task is running after the recents model has updated so that we use
+            // the correct index.
+            mQuickSwitchViewController.openQuickSwitchView(
+                    mTasks,
+                    mNumHiddenTasks,
+                    /* updateTasks= */ true,
+                    currentFocusedIndex == -1 && !mControllerCallbacks.isFirstTaskRunning()
+                            ? 0 : currentFocusedIndex,
+                    onDesktop);
         });
     }
 
@@ -246,5 +261,20 @@
         void onCloseComplete() {
             mQuickSwitchViewController = null;
         }
+
+        boolean isTaskRunning(@Nullable GroupTask task) {
+            if (task == null) {
+                return false;
+            }
+            int runningTaskId = ActivityManagerWrapper.getInstance().getRunningTask().taskId;
+            Task task2 = task.task2;
+
+            return runningTaskId == task.task1.key.id
+                    || (task2 != null && runningTaskId == task2.key.id);
+        }
+
+        boolean isFirstTaskRunning() {
+            return isTaskRunning(getTaskAt(0));
+        }
     }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
index b29ce6b..2421c94 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
@@ -15,23 +15,31 @@
  */
 package com.android.launcher3.taskbar;
 
+import static android.window.SplashScreen.SPLASH_SCREEN_STYLE_UNDEFINED;
+
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 
 import android.animation.Animator;
+import android.app.ActivityOptions;
 import android.view.KeyEvent;
 import android.view.View;
+import android.view.animation.AnimationUtils;
+import android.window.RemoteTransition;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
+import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimatorListeners;
 import com.android.launcher3.taskbar.overlay.TaskbarOverlayContext;
 import com.android.launcher3.taskbar.overlay.TaskbarOverlayDragLayer;
 import com.android.quickstep.SystemUiProxy;
 import com.android.quickstep.util.GroupTask;
+import com.android.quickstep.util.SlideInRemoteTransition;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.recents.model.ThumbnailData;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
+import com.android.systemui.shared.system.QuickStepContract;
 
 import java.io.PrintWriter;
 import java.util.List;
@@ -117,9 +125,12 @@
      * index will be focused.
      */
     protected int launchFocusedTask() {
-        // Launch the second-most recent task if the user quick switches too quickly, if possible.
-        return launchTaskAt(mCurrentFocusIndex == -1
-                ? (mControllerCallbacks.getTaskCount() > 1 ? 1 : 0) : mCurrentFocusIndex);
+        if (mCurrentFocusIndex != -1) {
+            return launchTaskAt(mCurrentFocusIndex);
+        }
+        // If the user quick switches too quickly, updateCurrentFocusIndex might not have run.
+        return launchTaskAt(mControllerCallbacks.isFirstTaskRunning()
+                && mControllerCallbacks.getTaskCount() > 1 ? 1 : 0);
     }
 
     private int launchTaskAt(int index) {
@@ -134,27 +145,33 @@
         if (task == null) {
             return Math.max(0, index);
         }
-        Task task2 = task.task2;
-        int runningTaskId = ActivityManagerWrapper.getInstance().getRunningTask().taskId;
-        if (runningTaskId == task.task1.key.id
-                || (task2 != null && runningTaskId == task2.key.id)) {
+        if (mControllerCallbacks.isTaskRunning(task)) {
             // Ignore attempts to run the selected task if it is already running.
             return -1;
         }
 
+        TaskbarActivityContext context = mControllers.taskbarActivityContext;
+        RemoteTransition remoteTransition = new RemoteTransition(new SlideInRemoteTransition(
+                Utilities.isRtl(mControllers.taskbarActivityContext.getResources()),
+                context.getDeviceProfile().overviewPageSpacing,
+                QuickStepContract.getWindowCornerRadius(context),
+                AnimationUtils.loadInterpolator(
+                        context, android.R.interpolator.fast_out_extra_slow_in)));
         if (mOnDesktop) {
             UI_HELPER_EXECUTOR.execute(() ->
                     SystemUiProxy.INSTANCE.get(mKeyboardQuickSwitchView.getContext())
                             .showDesktopApp(task.task1.key.id));
-        } else if (task2 == null) {
-            UI_HELPER_EXECUTOR.execute(() ->
-                    ActivityManagerWrapper.getInstance().startActivityFromRecents(
-                            task.task1.key,
-                            mControllers.taskbarActivityContext.getActivityLaunchOptions(
-                                    taskView == null ? mKeyboardQuickSwitchView : taskView, null)
-                                    .options));
+        } else if (task.task2 == null) {
+            UI_HELPER_EXECUTOR.execute(() -> {
+                ActivityOptions activityOptions = mControllers.taskbarActivityContext
+                        .makeDefaultActivityOptions(SPLASH_SCREEN_STYLE_UNDEFINED).options;
+                activityOptions.setRemoteTransition(remoteTransition);
+
+                ActivityManagerWrapper.getInstance().startActivityFromRecents(
+                        task.task1.key, activityOptions);
+            });
         } else {
-            mControllers.uiController.launchSplitTasks(task);
+            mControllers.uiController.launchSplitTasks(task, remoteTransition);
         }
         return -1;
     }
diff --git a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
index b4754c6..20e977b 100644
--- a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
@@ -20,6 +20,7 @@
 import static com.android.launcher3.taskbar.TaskbarEduTooltipControllerKt.TOOLTIP_STEP_FEATURES;
 import static com.android.launcher3.taskbar.TaskbarLauncherStateController.FLAG_VISIBLE;
 import static com.android.quickstep.TaskAnimationManager.ENABLE_SHELL_TRANSITIONS;
+import static com.android.quickstep.views.DesktopTaskView.isDesktopModeSupported;
 
 import android.animation.Animator;
 import android.animation.AnimatorSet;
@@ -27,6 +28,7 @@
 import android.util.Log;
 import android.view.TaskTransitionSpec;
 import android.view.WindowManagerGlobal;
+import android.window.RemoteTransition;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -40,11 +42,13 @@
 import com.android.launcher3.logging.InstanceId;
 import com.android.launcher3.logging.InstanceIdSequence;
 import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.statehandlers.DesktopVisibilityController;
 import com.android.launcher3.taskbar.bubbles.BubbleBarController;
 import com.android.launcher3.uioverrides.QuickstepLauncher;
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.MultiPropertyFactory;
 import com.android.launcher3.util.OnboardingPrefs;
+import com.android.quickstep.LauncherActivityInterface;
 import com.android.quickstep.RecentsAnimationCallbacks;
 import com.android.quickstep.util.GroupTask;
 import com.android.quickstep.views.RecentsView;
@@ -199,11 +203,26 @@
             return null;
         }
 
+        DesktopVisibilityController desktopController =
+                LauncherActivityInterface.INSTANCE.getDesktopVisibilityController();
+        final boolean onDesktop =
+                isDesktopModeSupported()
+                        && desktopController != null
+                        && desktopController.areFreeformTasksVisible();
+        if (onDesktop) {
+            isVisible = false;
+        }
+
         mTaskbarLauncherStateController.updateStateForFlag(FLAG_VISIBLE, isVisible);
         return mTaskbarLauncherStateController.applyState(fromInit ? 0 : duration, startAnimation);
     }
 
     @Override
+    public void onStateTransitionCompletedAfterSwipeToHome(LauncherState state) {
+        mTaskbarLauncherStateController.onStateTransitionCompletedAfterSwipeToHome(state);
+    }
+
+    @Override
     public void refreshResumedState() {
         onLauncherVisibilityChanged(mLauncher.hasBeenResumed());
     }
@@ -385,8 +404,9 @@
     }
 
     @Override
-    public void launchSplitTasks(@NonNull GroupTask groupTask) {
-        mLauncher.launchSplitTasks(groupTask);
+    public void launchSplitTasks(
+            @NonNull GroupTask groupTask, @Nullable RemoteTransition remoteTransition) {
+        mLauncher.launchSplitTasks(groupTask, remoteTransition);
     }
 
     @Override
diff --git a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
index 3514447..6d4fc18 100644
--- a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
@@ -25,8 +25,6 @@
 import static com.android.launcher3.Utilities.getDescendantCoordRelativeToAncestor;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_TASKBAR_NAVBAR_UNIFICATION;
 import static com.android.launcher3.taskbar.LauncherTaskbarUIController.SYSUI_SURFACE_PROGRESS_INDEX;
-import static com.android.launcher3.taskbar.TaskbarManager.isPhoneButtonNavMode;
-import static com.android.launcher3.taskbar.TaskbarManager.isPhoneMode;
 import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_A11Y;
 import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_BACK;
 import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_HOME;
@@ -53,6 +51,7 @@
 import android.annotation.DrawableRes;
 import android.annotation.IdRes;
 import android.annotation.LayoutRes;
+import android.content.Context;
 import android.content.pm.ActivityInfo.Config;
 import android.content.res.ColorStateList;
 import android.content.res.Resources;
@@ -62,6 +61,7 @@
 import android.graphics.Region;
 import android.graphics.Region.Op;
 import android.graphics.drawable.AnimatedVectorDrawable;
+import android.graphics.drawable.Drawable;
 import android.graphics.drawable.PaintDrawable;
 import android.graphics.drawable.RotateDrawable;
 import android.inputmethodservice.InputMethodService;
@@ -80,6 +80,8 @@
 import android.widget.ImageView;
 import android.widget.LinearLayout;
 
+import androidx.annotation.Nullable;
+
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.LauncherAnimUtils;
 import com.android.launcher3.R;
@@ -95,6 +97,7 @@
 import com.android.launcher3.util.TouchController;
 import com.android.launcher3.util.window.WindowManagerProxy;
 import com.android.launcher3.views.BaseDragLayer;
+import com.android.systemui.shared.navigationbar.KeyButtonRipple;
 import com.android.systemui.shared.rotation.FloatingRotationButton;
 import com.android.systemui.shared.rotation.RotationButton;
 import com.android.systemui.shared.rotation.RotationButtonController;
@@ -146,6 +149,7 @@
     private int mState;
 
     private final TaskbarActivityContext mContext;
+    private final @Nullable Context mNavigationBarPanelContext;
     private final WindowManagerProxy mWindowManagerProxy;
     private final FrameLayout mNavButtonsView;
     private final LinearLayout mNavButtonContainer;
@@ -203,8 +207,10 @@
     private final RecentsHitboxExtender mHitboxExtender = new RecentsHitboxExtender();
     private ImageView mRecentsButton;
 
-    public NavbarButtonsViewController(TaskbarActivityContext context, FrameLayout navButtonsView) {
+    public NavbarButtonsViewController(TaskbarActivityContext context,
+            @Nullable Context navigationBarPanelContext, FrameLayout navButtonsView) {
         mContext = context;
+        mNavigationBarPanelContext = navigationBarPanelContext;
         mWindowManagerProxy = WindowManagerProxy.INSTANCE.get(mContext);
         mNavButtonsView = navButtonsView;
         mNavButtonContainer = mNavButtonsView.findViewById(R.id.end_nav_buttons);
@@ -233,7 +239,7 @@
         Point p = !mContext.isUserSetupComplete()
                 ? new Point(0, mControllers.taskbarActivityContext.getSetupWindowHeight())
                 : DimensionUtils.getTaskbarPhoneDimensions(deviceProfile, resources,
-                        TaskbarManager.isPhoneMode(deviceProfile));
+                        mContext.isPhoneMode());
         mNavButtonsView.getLayoutParams().height = p.y;
 
         mIsImeRenderingNavButtons =
@@ -299,7 +305,7 @@
             initButtons(mNavButtonContainer, mEndContextualContainer,
                     mControllers.navButtonController);
             updateButtonLayoutSpacing();
-            updateStateForFlag(FLAG_SMALL_SCREEN, isPhoneButtonNavMode(mContext));
+            updateStateForFlag(FLAG_SMALL_SCREEN, mContext.isPhoneButtonNavMode());
 
             mPropertyHolders.add(new StatePropertyHolder(
                     mControllers.taskbarDragLayerController.getNavbarBackgroundAlpha(),
@@ -312,7 +318,8 @@
             rotationButton.hide();
             mControllers.rotationButtonController.setRotationButton(rotationButton, null);
         } else {
-            mFloatingRotationButton = new FloatingRotationButton(mContext,
+            mFloatingRotationButton = new FloatingRotationButton(
+                    ENABLE_TASKBAR_NAVBAR_UNIFICATION ? mNavigationBarPanelContext : mContext,
                     R.string.accessibility_rotate_button,
                     R.layout.rotate_suggestion,
                     R.id.rotate_suggestion,
@@ -381,7 +388,7 @@
         int navButtonSize = mContext.getResources().getDimensionPixelSize(
                 R.dimen.taskbar_nav_buttons_size);
         boolean isRtl = Utilities.isRtl(mContext.getResources());
-        if (!isPhoneMode(mContext.getDeviceProfile())) {
+        if (!mContext.isPhoneMode()) {
             mPropertyHolders.add(new StatePropertyHolder(
                     mBackButton, flags -> (flags & FLAG_ONLY_BACK_FOR_BOUNCER_VISIBLE) != 0
                             || (flags & FLAG_KEYGUARD_VISIBLE) != 0,
@@ -625,7 +632,7 @@
      * Sets the translationY of the nav buttons based on the current device state.
      */
     public void updateNavButtonTranslationY() {
-        if (isPhoneButtonNavMode(mContext)) {
+        if (mContext.isPhoneButtonNavMode()) {
             return;
         }
         final float normalTranslationY = mTaskbarNavButtonTranslationY.value;
@@ -660,6 +667,11 @@
 
         for (ImageView button : mAllButtons) {
             button.setImageTintList(ColorStateList.valueOf(iconColor));
+            Drawable background = button.getBackground();
+            if (background instanceof KeyButtonRipple) {
+                ((KeyButtonRipple) background).setDarkIntensity(
+                        mTaskbarNavButtonDarkIntensity.value);
+            }
         }
     }
 
@@ -744,9 +756,9 @@
                             dp, mNavButtonsView, mImeSwitcherButton,
                             mControllers.rotationButtonController.getRotationButton(),
                             mA11yButton, res, isInKidsMode, isInSetup, isThreeButtonNav,
-                            TaskbarManager.isPhoneMode(dp),
-                            mWindowManagerProxy.getRotation(mContext));
-            navButtonLayoutter.layoutButtons(dp, isA11yButtonPersistent());
+                            mContext.isPhoneMode(), mWindowManagerProxy.getRotation(mContext));
+            navButtonLayoutter.layoutButtons(mContext, isA11yButtonPersistent());
+            updateButtonsBackground();
             updateNavButtonColor();
             return;
         }
@@ -866,7 +878,27 @@
                 }
             }
         }
+    }
 
+    private void updateButtonsBackground() {
+        boolean clipped = !mContext.isPhoneButtonNavMode();
+        mNavButtonContainer.setClipToPadding(clipped);
+        mNavButtonContainer.setClipChildren(clipped);
+        mNavButtonsView.setClipToPadding(clipped);
+        mNavButtonsView.setClipChildren(clipped);
+
+        for (ImageView button : mAllButtons) {
+            updateButtonBackground(button, mContext.isPhoneButtonNavMode());
+        }
+    }
+
+    private static void updateButtonBackground(View view, boolean isPhoneButtonNavMode) {
+        if (isPhoneButtonNavMode) {
+            view.setBackground(new KeyButtonRipple(view.getContext(), view,
+                    R.dimen.key_button_ripple_max_width));
+        } else {
+            view.setBackgroundResource(R.drawable.taskbar_icon_click_feedback_roundrect);
+        }
     }
 
     public void onDestroy() {
diff --git a/quickstep/src/com/android/launcher3/taskbar/StashedHandleViewController.java b/quickstep/src/com/android/launcher3/taskbar/StashedHandleViewController.java
index c4255bf..fd0d655 100644
--- a/quickstep/src/com/android/launcher3/taskbar/StashedHandleViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/StashedHandleViewController.java
@@ -15,11 +15,14 @@
  */
 package com.android.launcher3.taskbar;
 
+import static android.view.Display.DEFAULT_DISPLAY;
+
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NAV_BAR_HIDDEN;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.ValueAnimator;
+import android.content.Context;
 import android.content.SharedPreferences;
 import android.content.res.Resources;
 import android.graphics.Outline;
@@ -37,6 +40,7 @@
 import com.android.launcher3.util.Executors;
 import com.android.launcher3.util.MultiPropertyFactory;
 import com.android.launcher3.util.MultiValueAlpha;
+import com.android.quickstep.NavHandle;
 import com.android.systemui.shared.navigationbar.RegionSamplingHelper;
 
 import java.io.PrintWriter;
@@ -44,7 +48,8 @@
 /**
  * Handles properties/data collection, then passes the results to our stashed handle View to render.
  */
-public class StashedHandleViewController implements TaskbarControllers.LoggableTaskbarController {
+public class StashedHandleViewController implements TaskbarControllers.LoggableTaskbarController,
+        NavHandle {
 
     public static final int ALPHA_INDEX_STASHED = 0;
     public static final int ALPHA_INDEX_HOME_DISABLED = 1;
@@ -83,6 +88,7 @@
 
     // States that affect whether region sampling is enabled or not
     private boolean mIsStashed;
+    private boolean mIsLumaSamplingEnabled;
     private boolean mTaskbarHidden;
 
     private float mTranslationYForSwipe;
@@ -107,8 +113,8 @@
         mControllers = controllers;
         DeviceProfile deviceProfile = mActivity.getDeviceProfile();
         Resources resources = mActivity.getResources();
-        if (isPhoneGestureNavMode(mActivity.getDeviceProfile())) {
-            mTaskbarSize = resources.getDimensionPixelSize(R.dimen.taskbar_size);
+        if (mActivity.isPhoneGestureNavMode()) {
+            mTaskbarSize = resources.getDimensionPixelSize(R.dimen.taskbar_phone_size);
             mStashedHandleWidth =
                     resources.getDimensionPixelSize(R.dimen.taskbar_stashed_small_screen);
         } else {
@@ -120,7 +126,7 @@
         mStashedHandleView.getLayoutParams().height = mTaskbarSize + taskbarBottomMargin;
 
         mTaskbarStashedHandleAlpha.get(ALPHA_INDEX_STASHED).setValue(
-                isPhoneGestureNavMode(deviceProfile) ? 1 : 0);
+                mActivity.isPhoneGestureNavMode() ? 1 : 0);
         mTaskbarStashedHandleHintScale.updateValue(1f);
 
         final int stashedTaskbarHeight = mControllers.taskbarStashController.getStashedHeight();
@@ -148,8 +154,8 @@
             view.setPivotY(stashedCenterY);
         });
         initRegionSampler();
-        if (isPhoneGestureNavMode(deviceProfile)) {
-            onIsStashedChanged(true);
+        if (mActivity.isPhoneGestureNavMode()) {
+            onIsStashedChanged();
         }
     }
 
@@ -184,10 +190,6 @@
         mRegionSamplingHelper = null;
     }
 
-    private boolean isPhoneGestureNavMode(DeviceProfile deviceProfile) {
-        return TaskbarManager.isPhoneMode(deviceProfile) && !mActivity.isThreeButtonNav();
-    }
-
     public MultiPropertyFactory<View> getStashedHandleAlpha() {
         return mTaskbarStashedHandleAlpha;
     }
@@ -236,10 +238,23 @@
     }
 
     /** Called when taskbar is stashed or unstashed. */
-    public void onIsStashedChanged(boolean isStashed) {
-        mIsStashed = isStashed;
+    public void onIsStashedChanged() {
+        mIsStashed = isStashedHandleVisible();
+        updateSamplingState();
+    }
+
+    public void onNavigationBarLumaSamplingEnabled(int displayId, boolean enable) {
+        if (DEFAULT_DISPLAY != displayId) {
+            return;
+        }
+
+        mIsLumaSamplingEnabled = enable;
+        updateSamplingState();
+    }
+
+    private void updateSamplingState() {
         updateRegionSamplingWindowVisibility();
-        if (isStashed) {
+        if (shouldSample()) {
             mStashedHandleView.updateSampledRegion(mStashedHandleBounds);
             mRegionSamplingHelper.start(mStashedHandleView.getSampledRegion());
         } else {
@@ -247,6 +262,10 @@
         }
     }
 
+    private boolean shouldSample() {
+        return mIsStashed && mIsLumaSamplingEnabled;
+    }
+
     protected void updateStashedHandleHintScale() {
         mStashedHandleView.setScaleX(mTaskbarStashedHandleHintScale.value);
         mStashedHandleView.setScaleY(mTaskbarStashedHandleHintScale.value);
@@ -286,7 +305,7 @@
     }
 
     private void updateRegionSamplingWindowVisibility() {
-        mRegionSamplingHelper.setWindowVisible(mIsStashed && !mTaskbarHidden);
+        mRegionSamplingHelper.setWindowVisible(shouldSample() && !mTaskbarHidden);
     }
 
     public boolean isStashedHandleVisible() {
@@ -302,4 +321,24 @@
         pw.println(prefix + "\tmStashedHandleHeight=" + mStashedHandleHeight);
         mRegionSamplingHelper.dump(prefix, pw);
     }
+
+    @Override
+    public void animateNavBarLongPress(boolean isTouchDown, boolean shrink, long durationMs) {
+        // TODO(b/308693847): Animate similarly to NavigationHandle.java (SysUI).
+    }
+
+    @Override
+    public boolean isNavHandleStashedTaskbar() {
+        return true;
+    }
+
+    @Override
+    public boolean canNavHandleBeLongPressed() {
+        return isStashedHandleVisible();
+    }
+
+    @Override
+    public int getNavHandleWidth(Context context) {
+        return mStashedHandleWidth;
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index e106506..4b95d7b 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -42,7 +42,9 @@
 
 import android.animation.AnimatorSet;
 import android.animation.ValueAnimator;
+import android.app.ActivityManager.RunningTaskInfo;
 import android.app.ActivityOptions;
+import android.app.ActivityTaskManager;
 import android.content.ActivityNotFoundException;
 import android.content.Context;
 import android.content.Intent;
@@ -74,9 +76,11 @@
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.BubbleTextView;
 import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.LauncherPrefs;
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.R;
 import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.apppairs.AppPairIcon;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.dot.DotInfo;
 import com.android.launcher3.folder.Folder;
@@ -105,6 +109,7 @@
 import com.android.launcher3.testing.shared.TestProtocol;
 import com.android.launcher3.touch.ItemClickHandler;
 import com.android.launcher3.touch.ItemClickHandler.ItemClickProxy;
+import com.android.launcher3.uioverrides.ApiWrapper;
 import com.android.launcher3.util.ActivityOptionsWrapper;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.DisplayController;
@@ -118,6 +123,7 @@
 import com.android.launcher3.util.VibratorWrapper;
 import com.android.launcher3.util.ViewCache;
 import com.android.launcher3.views.ActivityContext;
+import com.android.quickstep.NavHandle;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.TaskView;
 import com.android.systemui.shared.recents.model.Task;
@@ -127,10 +133,13 @@
 import com.android.systemui.unfold.util.ScopedUnfoldTransitionProgressProvider;
 
 import java.io.PrintWriter;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 import java.util.Optional;
+import java.util.Set;
 import java.util.function.Consumer;
+import java.util.stream.Collectors;
 
 /**
  * The {@link ActivityContext} with which we inflate Taskbar-related Views. This allows UI elements
@@ -145,6 +154,8 @@
 
     private static final String WINDOW_TITLE = "Taskbar";
 
+    private final @Nullable Context mNavigationBarPanelContext;
+
     private final TaskbarDragLayer mDragLayer;
     private final TaskbarControllers mControllers;
 
@@ -180,11 +191,15 @@
 
     private DeviceProfile mPersistentTaskbarDeviceProfile;
 
-    public TaskbarActivityContext(Context windowContext, DeviceProfile launcherDp,
+    private final LauncherPrefs mLauncherPrefs;
+
+    public TaskbarActivityContext(Context windowContext,
+            @Nullable Context navigationBarPanelContext, DeviceProfile launcherDp,
             TaskbarNavButtonController buttonController, ScopedUnfoldTransitionProgressProvider
             unfoldTransitionProgressProvider) {
         super(windowContext);
 
+        mNavigationBarPanelContext = navigationBarPanelContext;
         applyDeviceProfile(launcherDp);
         final Resources resources = getResources();
 
@@ -206,7 +221,7 @@
         Context c = getApplicationContext();
         mWindowManager = c.getSystemService(WindowManager.class);
 
-        boolean phoneMode = TaskbarManager.isPhoneMode(mDeviceProfile);
+        boolean phoneMode = isPhoneMode();
         mLeftCorner = phoneMode
                 ? null
                 : display.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_LEFT);
@@ -258,8 +273,10 @@
                 new TaskbarDragController(this),
                 buttonController,
                 isDesktopMode
-                        ? new DesktopNavbarButtonsViewController(this, navButtonsView)
-                        : new NavbarButtonsViewController(this, navButtonsView),
+                        ? new DesktopNavbarButtonsViewController(this, mNavigationBarPanelContext,
+                                navButtonsView)
+                        : new NavbarButtonsViewController(this, mNavigationBarPanelContext,
+                                navButtonsView),
                 rotationButtonController,
                 new TaskbarDragLayerController(this, mDragLayer),
                 new TaskbarViewController(this, taskbarView),
@@ -287,6 +304,8 @@
                 new KeyboardQuickSwitchController(),
                 new TaskbarPinningController(this),
                 bubbleControllersOptional);
+
+        mLauncherPrefs = LauncherPrefs.get(this);
     }
 
     /** Updates {@link DeviceProfile} instances for any Taskbar windows. */
@@ -359,6 +378,8 @@
         onSystemBarAttributesChanged(sharedState.systemBarAttrsDisplayId,
                 sharedState.systemBarAttrsBehavior);
         onNavButtonsDarkIntensityChanged(sharedState.navButtonsDarkIntensity);
+        onNavigationBarLumaSamplingEnabled(sharedState.mLumaSamplingDisplayId,
+                sharedState.mIsLumaSamplingEnabled);
 
         if (ENABLE_TASKBAR_NAVBAR_UNIFICATION) {
             // W/ the flag not set this entire class gets re-created, which resets the value of
@@ -376,6 +397,28 @@
     }
 
     /**
+     * @return {@code true} if the device profile isn't a large screen profile and we are using a
+     * single window for taskbar and navbar.
+     */
+    public boolean isPhoneMode() {
+        return ENABLE_TASKBAR_NAVBAR_UNIFICATION && mDeviceProfile.isPhone;
+    }
+
+    /**
+     * @return {@code true} if {@link #isPhoneMode()} is true and we're using 3 button-nav
+     */
+    public boolean isPhoneButtonNavMode() {
+        return isPhoneMode() && isThreeButtonNav();
+    }
+
+    /**
+     * @return {@code true} if {@link #isPhoneMode()} is true and we're using gesture nav
+     */
+    public boolean isPhoneGestureNavMode() {
+        return isPhoneMode() && !isThreeButtonNav();
+    }
+
+    /**
      * Show Taskbar upon receiving broadcast
      */
     public void showTaskbarFromBroadcast() {
@@ -404,6 +447,11 @@
                 getDeviceProfile().toSmallString());
     }
 
+    @NonNull
+    public LauncherPrefs getLauncherPrefs() {
+        return mLauncherPrefs;
+    }
+
     /**
      * Returns the View bounds of transient taskbar.
      */
@@ -411,6 +459,10 @@
         return mTransientTaskbarBounds;
     }
 
+    protected float getCurrentTaskbarWidth() {
+        return mControllers.taskbarViewController.getCurrentVisualTaskbarWidth();
+    }
+
     @Override
     public StatsLogManager getStatsLogManager() {
         // Used to mock, can't mock a default interface method directly
@@ -447,9 +499,7 @@
         windowLayoutParams.privateFlags =
                 WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION;
         windowLayoutParams.accessibilityTitle = getString(
-                TaskbarManager.isPhoneMode(mDeviceProfile)
-                        ? R.string.taskbar_phone_a11y_title
-                        : R.string.taskbar_a11y_title);
+                isPhoneMode() ? R.string.taskbar_phone_a11y_title : R.string.taskbar_a11y_title);
 
         return windowLayoutParams;
     }
@@ -463,8 +513,7 @@
                 ENABLE_TASKBAR_NAVBAR_UNIFICATION ? TYPE_NAVIGATION_BAR : TYPE_NAVIGATION_BAR_PANEL;
         WindowManager.LayoutParams windowLayoutParams =
                 createDefaultWindowLayoutParams(windowType, TaskbarActivityContext.WINDOW_TITLE);
-        boolean isPhoneNavMode = TaskbarManager.isPhoneButtonNavMode(this);
-        if (!isPhoneNavMode) {
+        if (!isPhoneButtonNavMode()) {
             return windowLayoutParams;
         }
 
@@ -563,6 +612,11 @@
         return mControllers.bubbleControllers.orElse(null);
     }
 
+    @NonNull
+    public NavHandle getNavHandle() {
+        return mControllers.stashedHandleViewController;
+    }
+
     @Override
     public ViewCache getViewCache() {
         return mViewCache;
@@ -799,6 +853,11 @@
                 .updateValue(darkIntensity);
     }
 
+    public void onNavigationBarLumaSamplingEnabled(int displayId, boolean enable) {
+        mControllers.stashedHandleViewController.onNavigationBarLumaSamplingEnabled(displayId,
+                enable);
+    }
+
     /**
      * Called to update a {@link AutohideSuspendFlag} with a new value.
      */
@@ -868,7 +927,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);
         }
 
@@ -998,7 +1057,7 @@
                         Toast.LENGTH_SHORT).show();
             } else {
                 // Else launch the selected app pair
-                launchFromTaskbarPreservingSplitIfVisible(recents, fi.contents);
+                launchFromTaskbarPreservingSplitIfVisible(recents, view, fi.contents);
                 mControllers.uiController.onTaskbarIconLaunched(fi);
                 mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(true);
             }
@@ -1020,9 +1079,8 @@
                         } else if (info.isPromise()) {
                             TestLogging.recordEvent(
                                     TestProtocol.SEQUENCE_MAIN, "start: taskbarPromiseIcon");
-                            intent = new PackageManagerHelper(this)
-                                    .getMarketIntent(info.getTargetPackage())
-                                    .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+                            intent = ApiWrapper.getAppMarketActivityIntent(this,
+                                    info.getTargetPackage(), Process.myUserHandle());
                             startActivity(intent);
 
                         } else if (info.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
@@ -1034,7 +1092,7 @@
                                     .startShortcut(packageName, id, null, null, info.user);
                         } else {
                             launchFromTaskbarPreservingSplitIfVisible(
-                                    recents, Collections.singletonList(info));
+                                    recents, view, Collections.singletonList(info));
                         }
 
                     } catch (NullPointerException
@@ -1072,7 +1130,8 @@
                 // If we are selecting a second app for split, launch the split tasks
                 taskbarUIController.triggerSecondAppForSplit(info, info.intent, view);
             } else {
-                launchFromTaskbarPreservingSplitIfVisible(recents, Collections.singletonList(info));
+                launchFromTaskbarPreservingSplitIfVisible(
+                        recents, view, Collections.singletonList(info));
             }
             mControllers.uiController.onTaskbarIconLaunched(info);
             mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(true);
@@ -1094,7 +1153,7 @@
      * (potentially breaking a split pair).
      */
     private void launchFromTaskbarPreservingSplitIfVisible(@Nullable RecentsView recents,
-            List<? extends ItemInfo> itemInfos) {
+            @Nullable View launchingIconView, List<? extends ItemInfo> itemInfos) {
         if (recents == null) {
             return;
         }
@@ -1107,23 +1166,32 @@
                 componentKeys,
                 findExactPairMatch,
                 foundTasks -> {
-                    @Nullable Task foundTask = foundTasks.get(0);
+                    @Nullable Task foundTask = foundTasks[0];
                     if (foundTask != null) {
                         TaskView foundTaskView = recents.getTaskViewByTaskId(foundTask.key.id);
-                        if (foundTaskView != null
-                                && foundTaskView.isVisibleToUser()) {
-                            TestLogging.recordEvent(
-                                    TestProtocol.SEQUENCE_MAIN, "start: taskbarAppIcon");
-                            foundTaskView.launchTasks();
-                            return;
+                        if (foundTaskView != null) {
+                            // The foundTaskView contains the 1-2 taskIds we are looking for.
+                            // If we are already in-app and running the correct tasks, no need
+                            // to do anything.
+                            if (FeatureFlags.enableAppPairs()
+                                    && isAlreadyInApp(foundTaskView.getTaskIds())) {
+                                return;
+                            }
+                            // If we are in Overview and the TaskView tile is visible, expand that
+                            // tile.
+                            if (foundTaskView.isVisibleToUser()) {
+                                TestLogging.recordEvent(
+                                        TestProtocol.SEQUENCE_MAIN, "start: taskbarAppIcon");
+                                foundTaskView.launchTasks();
+                                return;
+                            }
                         }
                     }
-
+                    // If none of the above cases apply, launch a new app or app pair.
                     if (findExactPairMatch) {
                         // We did not find the app pair we were looking for, so launch one.
                         recents.getSplitSelectController().getAppPairsController().launchAppPair(
-                                (WorkspaceItemInfo) itemInfos.get(0),
-                                (WorkspaceItemInfo) itemInfos.get(1));
+                                (AppPairIcon) launchingIconView);
                     } else {
                         startItemInfoActivity(itemInfos.get(0));
                     }
@@ -1131,6 +1199,27 @@
         );
     }
 
+    /**
+     * Checks if a given list of taskIds are all already running in-app.
+     */
+    private boolean isAlreadyInApp(int[] ids) {
+        if (mControllers.uiController.isInOverview()) {
+            return false;
+        }
+
+        RunningTaskInfo[] currentlyRunningTasks = ActivityManagerWrapper.getInstance()
+                .getRunningTasks(false /* filterOnlyVisibleRecents */);
+        Set<Integer> currentlyRunningIds = Arrays.stream(currentlyRunningTasks)
+                .map(task -> task.taskId).collect(Collectors.toSet());
+
+        for (int id : ids) {
+            if (id != ActivityTaskManager.INVALID_TASK_ID && !currentlyRunningIds.contains(id)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
     private void startItemInfoActivity(ItemInfo info) {
         Intent intent = new Intent(info.getIntent())
                 .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
@@ -1269,6 +1358,16 @@
         }
     }
 
+    /** Unstashes the Bubble Bar if it is stashed. */
+    @VisibleForTesting
+    public void unstashBubbleBarIfStashed() {
+        mControllers.bubbleControllers.ifPresent(bubbleControllers -> {
+            if (bubbleControllers.bubbleStashController.isStashed()) {
+                bubbleControllers.bubbleStashController.showBubbleBar(false);
+            }
+        });
+    }
+
     protected boolean isUserSetupComplete() {
         return mIsUserSetupComplete;
     }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt
index d6016f1..e290c3f 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt
@@ -23,7 +23,6 @@
 import android.graphics.Path
 import android.graphics.RectF
 import com.android.app.animation.Interpolators
-import com.android.launcher3.DeviceProfile
 import com.android.launcher3.R
 import com.android.launcher3.Utilities
 import com.android.launcher3.Utilities.mapRange
@@ -95,10 +94,10 @@
         setCornerRoundness(DEFAULT_ROUNDNESS)
     }
 
-    fun updateStashedHandleWidth(dp: DeviceProfile, res: Resources) {
+    fun updateStashedHandleWidth(context: TaskbarActivityContext, res: Resources) {
         stashedHandleWidth =
             res.getDimensionPixelSize(
-                if (TaskbarManager.isPhoneMode(dp)) R.dimen.taskbar_stashed_small_screen
+                if (context.isPhoneMode) R.dimen.taskbar_stashed_small_screen
                 else R.dimen.taskbar_stashed_handle_width
             )
     }
@@ -204,12 +203,7 @@
         val newBackgroundHeight =
             mapRange(progress, backgroundHeightWhileAnimating, maxTransientTaskbarHeight)
         val fullWidth = transientBackgroundBounds.width()
-
-        // .9f is here to restrict min width of the background while animating, so transient
-        // background keeps it pill shape until animation end.
-        val animationWidth =
-            if (DisplayController.isTransientTaskbar(context)) fullWidth.toFloat() * .9f
-            else fullWidth.toFloat()
+        val animationWidth = context.currentTaskbarWidth
         val backgroundWidthWhileAnimating =
             if (isAnimatingPinning) animationWidth else stashedHandleWidth.toFloat()
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDividerPopupView.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarDividerPopupView.kt
index 3f9b66a..12f1e63 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDividerPopupView.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDividerPopupView.kt
@@ -16,7 +16,6 @@
 package com.android.launcher3.taskbar
 
 import android.animation.Animator
-import android.animation.AnimatorListenerAdapter
 import android.animation.AnimatorSet
 import android.animation.ObjectAnimator
 import android.annotation.SuppressLint
@@ -182,7 +181,7 @@
                     measuredWidth.toFloat(),
                     measuredHeight.toFloat(),
                     (measuredWidth - arrowWidth) / 2, // arrowOffsetX
-                    0f, // arrowOffsetY
+                    -mArrowOffsetVertical.toFloat(), // arrowOffsetY
                     false, // isPointingUp
                     true, // leftAligned
                     context.getColor(R.color.popup_shade_first),
@@ -197,32 +196,19 @@
             mActivityContext.deviceProfile.taskbarIconSize) / 2 + verticalOffsetForPopupView
     }
 
-    override fun animateClose() {
-        if (!mIsOpen) {
-            return
+    override fun onCreateCloseAnimation(anim: AnimatorSet?) {
+        // If taskbar pinning preference changed insert custom close animation for popup menu.
+        if (didPreferenceChange) {
+            mOpenCloseAnimator = getCloseAnimator()
         }
-        if (mOpenCloseAnimator != null) {
-            mOpenCloseAnimator.cancel()
-        }
-        mIsOpen = false
-
-        mOpenCloseAnimator = getCloseAnimator()
-
-        mOpenCloseAnimator.addListener(
-            object : AnimatorListenerAdapter() {
-                override fun onAnimationEnd(animation: Animator) {
-                    mOpenCloseAnimator = null
-                    if (mDeferContainerRemoval) {
-                        setVisibility(INVISIBLE)
-                    } else {
-                        closeComplete()
-                    }
-                }
-            }
-        )
         onCloseCallback(didPreferenceChange)
         onCloseCallback = {}
-        mOpenCloseAnimator.start()
+    }
+
+    /** Aligning the view pivot to center for animation. */
+    override fun setPivotForOpenCloseAnimation() {
+        pivotX = measuredWidth / 2f
+        pivotY = measuredHeight.toFloat()
     }
 
     private fun getCloseAnimator(): AnimatorSet {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
index 6ddf9e9..faa67be 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
@@ -23,6 +23,7 @@
 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_SEARCH_ACTION;
 import static com.android.launcher3.logger.LauncherAtom.ContainerInfo.ContainerCase.EXTENDED_CONTAINERS;
 import static com.android.launcher3.logger.LauncherAtomExtensions.ExtendedContainers.ContainerCase.DEVICE_SEARCH_RESULT_CONTAINER;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_APP_LAUNCH_DRAGDROP;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -517,6 +518,9 @@
                 // Note, this must be done last to ensure no AutohideSuspendFlags are active, as
                 // that will prevent us from stashing until the timeout.
                 mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(true);
+
+                mActivity.getStatsLogManager().logger().withItemInfo(mDragObject.dragInfo)
+                        .log(LAUNCHER_APP_LAUNCH_DRAGDROP);
             }
         }
     }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java
index a24cf4b..491938d 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java
@@ -106,7 +106,7 @@
 
     public void init(TaskbarDragLayerController.TaskbarDragLayerCallbacks callbacks) {
         mControllerCallbacks = callbacks;
-        mBackgroundRenderer.updateStashedHandleWidth(mActivity.getDeviceProfile(), getResources());
+        mBackgroundRenderer.updateStashedHandleWidth(mActivity, getResources());
         recreateControllers();
     }
 
@@ -169,6 +169,7 @@
         mBackgroundRenderer.setBackgroundProgress(mTaskbarBackgroundProgress);
         mBackgroundRenderer.draw(canvas);
         super.dispatchDraw(canvas);
+        mControllerCallbacks.drawDebugUi(canvas);
     }
 
     /**
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java
index 73e32ab..3f5402f 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java
@@ -19,8 +19,10 @@
 import static com.android.launcher3.taskbar.TaskbarPinningController.PINNING_TRANSIENT;
 
 import android.content.res.Resources;
+import android.graphics.Canvas;
 import android.graphics.Point;
 import android.graphics.Rect;
+import android.os.SystemProperties;
 import android.view.ViewTreeObserver;
 
 import com.android.launcher3.DeviceProfile;
@@ -39,6 +41,9 @@
 public class TaskbarDragLayerController implements TaskbarControllers.LoggableTaskbarController,
         TaskbarControllers.BackgroundRendererController {
 
+    private static final boolean DEBUG = SystemProperties.getBoolean(
+            "persist.debug.draw_taskbar_debug_ui", false);
+
     private final TaskbarActivityContext mActivity;
     private final TaskbarDragLayer mTaskbarDragLayer;
     private final int mFolderMargin;
@@ -276,11 +281,10 @@
          */
         public int getTaskbarBackgroundHeight() {
             DeviceProfile deviceProfile = mActivity.getDeviceProfile();
-            if (TaskbarManager.isPhoneMode(deviceProfile)) {
+            if (mActivity.isPhoneMode()) {
                 Resources resources = mActivity.getResources();
-                Point taskbarDimensions =
-                        DimensionUtils.getTaskbarPhoneDimensions(deviceProfile, resources,
-                                TaskbarManager.isPhoneMode(deviceProfile));
+                Point taskbarDimensions = DimensionUtils.getTaskbarPhoneDimensions(deviceProfile,
+                        resources, true /* isPhoneMode */);
                 return taskbarDimensions.y == -1 ?
                         deviceProfile.getDisplayInfo().currentSize.y :
                         taskbarDimensions.y;
@@ -300,5 +304,15 @@
                     mTaskbarStashViaTouchController,
             };
         }
+
+        /**
+         * Draws debug UI on top of everything in TaskbarDragLayer.
+         */
+        public void drawDebugUi(Canvas canvas) {
+            if (!DEBUG) {
+                return;
+            }
+            mControllers.taskbarInsetsController.drawDebugTouchableRegionBounds(canvas);
+        }
     }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt
index b1f2d6c..eea543e 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt
@@ -33,7 +33,6 @@
 import com.android.launcher3.config.FeatureFlags.enableTaskbarPinning
 import com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_EDU_OPEN
 import com.android.launcher3.taskbar.TaskbarControllers.LoggableTaskbarController
-import com.android.launcher3.taskbar.TaskbarManager.isPhoneMode
 import com.android.launcher3.util.DisplayController
 import com.android.launcher3.util.OnboardingPrefs.TASKBAR_EDU_TOOLTIP_STEP
 import com.android.launcher3.views.BaseDragLayer
@@ -64,7 +63,7 @@
     LoggableTaskbarController {
 
     private val isTooltipEnabled: Boolean
-        get() = !Utilities.isRunningInTestHarness() && !isPhoneMode(activityContext.deviceProfile)
+        get() = !Utilities.isRunningInTestHarness() && !activityContext.isPhoneMode
     private val isOpen: Boolean
         get() = tooltip?.isOpen ?: false
     val isBeforeTooltipFeaturesStep: Boolean
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarForceVisibleImmersiveController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarForceVisibleImmersiveController.java
index 294925f..333c07b 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarForceVisibleImmersiveController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarForceVisibleImmersiveController.java
@@ -22,7 +22,7 @@
 import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK;
 
 import static com.android.launcher3.taskbar.NavbarButtonsViewController.ALPHA_INDEX_IMMERSIVE_MODE;
-import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IMMERSIVE_MODE;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY;
 
 import android.os.Bundle;
 import android.os.Handler;
@@ -84,7 +84,7 @@
 
     /** Update values tracked via sysui flags. */
     public void updateSysuiFlags(int sysuiFlags) {
-        mIsImmersiveMode = (sysuiFlags & SYSUI_STATE_IMMERSIVE_MODE) != 0;
+        mIsImmersiveMode = (sysuiFlags & SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY) == 0;
         if (mContext.isNavBarForceVisible()) {
             if (mIsImmersiveMode) {
                 startIconDimming();
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
index 1a34b7a..633383d 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
@@ -15,7 +15,11 @@
  */
 package com.android.launcher3.taskbar
 
+import android.graphics.Canvas
+import android.graphics.Color
 import android.graphics.Insets
+import android.graphics.Paint
+import android.graphics.Rect
 import android.graphics.Region
 import android.inputmethodservice.InputMethodService.ENABLE_HIDE_IME_CAPTION_BAR
 import android.os.Binder
@@ -47,6 +51,7 @@
 import com.android.launcher3.util.DisplayController
 import java.io.PrintWriter
 import kotlin.jvm.optionals.getOrNull
+import kotlin.math.max
 
 /** Handles the insets that Taskbar provides to underlying apps and the IME. */
 class TaskbarInsetsController(val context: TaskbarActivityContext) : LoggableTaskbarController {
@@ -58,7 +63,8 @@
 
     /** The bottom insets taskbar provides to the IME when IME is visible. */
     val taskbarHeightForIme: Int = context.resources.getDimensionPixelSize(R.dimen.taskbar_ime_size)
-    private val touchableRegion: Region = Region()
+    // The touchableRegion we will set unless some other state takes precedence.
+    private val defaultTouchableRegion: Region = Region()
     private val insetsOwner: IBinder = Binder()
     private val deviceProfileChangeListener = { _: DeviceProfile ->
         onTaskbarOrBubblebarWindowHeightOrInsetsChanged()
@@ -69,6 +75,7 @@
             context,
             this::onTaskbarOrBubblebarWindowHeightOrInsetsChanged
         )
+    private val debugTouchableRegion = DebugTouchableRegion()
 
     // Initialized in init.
     private lateinit var controllers: TaskbarControllers
@@ -100,9 +107,11 @@
             }
 
         windowLayoutParams.providedInsets =
-            if (enableTaskbarNoRecreate()) {
-                getProvidedInsets(controllers.sharedState!!.insetsFrameProviders!!,
-                        insetsRoundedCornerFlag)
+            if (enableTaskbarNoRecreate() && controllers.sharedState != null) {
+                getProvidedInsets(
+                    controllers.sharedState!!.insetsFrameProviders,
+                    insetsRoundedCornerFlag
+                )
             } else {
                 getProvidedInsets(insetsRoundedCornerFlag)
             }
@@ -122,7 +131,7 @@
             } else {
                 0
             }
-        val touchableHeight = Math.max(taskbarTouchableHeight, bubblesTouchableHeight)
+        val touchableHeight = max(taskbarTouchableHeight, bubblesTouchableHeight)
 
         if (
             controllers.bubbleControllers.isPresent &&
@@ -130,14 +139,14 @@
         ) {
             val iconBounds =
                 controllers.bubbleControllers.get().bubbleBarViewController.bubbleBarBounds
-            touchableRegion.set(
+            defaultTouchableRegion.set(
                 iconBounds.left,
                 iconBounds.top,
                 iconBounds.right,
                 iconBounds.bottom
             )
         } else {
-            touchableRegion.set(
+            defaultTouchableRegion.set(
                 0,
                 windowLayoutParams.height - touchableHeight,
                 context.deviceProfile.widthPx,
@@ -164,19 +173,18 @@
 
     /**
      * This is for when ENABLE_TASKBAR_NO_RECREATION is enabled. We generate one instance of
-     * providedInsets and use it across the entire lifecycle of TaskbarManager. The only thing
-     * we need to reset is nav bar flags based on insetsRoundedCornerFlag.
+     * providedInsets and use it across the entire lifecycle of TaskbarManager. The only thing we
+     * need to reset is nav bar flags based on insetsRoundedCornerFlag.
      */
-    private fun getProvidedInsets(providedInsets: Array<InsetsFrameProvider>,
-                                  insetsRoundedCornerFlag: Int): Array<InsetsFrameProvider> {
+    private fun getProvidedInsets(
+        providedInsets: Array<InsetsFrameProvider>,
+        insetsRoundedCornerFlag: Int
+    ): Array<InsetsFrameProvider> {
         val navBarsFlag =
-                (if (context.isGestureNav) FLAG_SUPPRESS_SCRIM else 0) or insetsRoundedCornerFlag
+            (if (context.isGestureNav) FLAG_SUPPRESS_SCRIM else 0) or insetsRoundedCornerFlag
         for (provider in providedInsets) {
             if (provider.type == navigationBars()) {
-                provider.setFlags(
-                        navBarsFlag,
-                        FLAG_SUPPRESS_SCRIM or FLAG_INSETS_ROUNDED_CORNER
-                )
+                provider.setFlags(navBarsFlag, FLAG_SUPPRESS_SCRIM or FLAG_INSETS_ROUNDED_CORNER)
             }
         }
         return providedInsets
@@ -184,25 +192,22 @@
 
     /**
      * The inset types and number of insets provided have to match for both gesture nav and button
-     * nav. The values and the order of the elements in array are allowed to differ.
-     * Reason being WM does not allow types and number of insets changing for a given window once it
-     * is added into the hierarchy for performance reasons.
+     * nav. The values and the order of the elements in array are allowed to differ. Reason being WM
+     * does not allow types and number of insets changing for a given window once it is added into
+     * the hierarchy for performance reasons.
      */
     private fun getProvidedInsets(insetsRoundedCornerFlag: Int): Array<InsetsFrameProvider> {
         val navBarsFlag =
-                (if (context.isGestureNav) FLAG_SUPPRESS_SCRIM else 0) or insetsRoundedCornerFlag
+            (if (context.isGestureNav) FLAG_SUPPRESS_SCRIM else 0) or insetsRoundedCornerFlag
         return arrayOf(
-                InsetsFrameProvider(insetsOwner, 0, navigationBars())
-                        .setFlags(
-                                navBarsFlag,
-                                FLAG_SUPPRESS_SCRIM or FLAG_INSETS_ROUNDED_CORNER
-                        ),
-                InsetsFrameProvider(insetsOwner, 0, tappableElement()),
-                InsetsFrameProvider(insetsOwner, 0, mandatorySystemGestures()),
-                InsetsFrameProvider(insetsOwner, INDEX_LEFT, systemGestures())
-                        .setSource(SOURCE_DISPLAY),
-                InsetsFrameProvider(insetsOwner, INDEX_RIGHT, systemGestures())
-                        .setSource(SOURCE_DISPLAY)
+            InsetsFrameProvider(insetsOwner, 0, navigationBars())
+                .setFlags(navBarsFlag, FLAG_SUPPRESS_SCRIM or FLAG_INSETS_ROUNDED_CORNER),
+            InsetsFrameProvider(insetsOwner, 0, tappableElement()),
+            InsetsFrameProvider(insetsOwner, 0, mandatorySystemGestures()),
+            InsetsFrameProvider(insetsOwner, INDEX_LEFT, systemGestures())
+                .setSource(SOURCE_DISPLAY),
+            InsetsFrameProvider(insetsOwner, INDEX_RIGHT, systemGestures())
+                .setSource(SOURCE_DISPLAY)
         )
     }
 
@@ -216,46 +221,52 @@
             provider.insetsSize = getInsetsForGravity(tappableHeight, gravity)
         } else if (provider.type == systemGestures() && provider.index == INDEX_LEFT) {
             val leftIndexInset =
-                    if (context.isThreeButtonNav) 0
-                    else gestureNavSettingsObserver.getLeftSensitivityForCallingUser(res)
+                if (context.isThreeButtonNav) 0
+                else gestureNavSettingsObserver.getLeftSensitivityForCallingUser(res)
             provider.insetsSize = Insets.of(leftIndexInset, 0, 0, 0)
         } else if (provider.type == systemGestures() && provider.index == INDEX_RIGHT) {
             val rightIndexInset =
-                    if (context.isThreeButtonNav) 0
-                    else gestureNavSettingsObserver.getRightSensitivityForCallingUser(res)
+                if (context.isThreeButtonNav) 0
+                else gestureNavSettingsObserver.getRightSensitivityForCallingUser(res)
             provider.insetsSize = Insets.of(0, 0, rightIndexInset, 0)
         }
 
         // When in gesture nav, report the stashed height to the IME, to allow hiding the
         // IME navigation bar.
-        val imeInsetsSize = if (ENABLE_HIDE_IME_CAPTION_BAR && context.isGestureNav) {
-            getInsetsForGravity(controllers.taskbarStashController.stashedHeight, gravity);
-        } else {
-            getInsetsForGravity(taskbarHeightForIme, gravity)
-        }
+        val imeInsetsSize =
+            if (ENABLE_HIDE_IME_CAPTION_BAR && context.isGestureNav) {
+                getInsetsForGravity(controllers.taskbarStashController.stashedHeight, gravity)
+            } else {
+                getInsetsForGravity(taskbarHeightForIme, gravity)
+            }
         val imeInsetsSizeOverride =
-                arrayOf(
-                        InsetsFrameProvider.InsetsSizeOverride(TYPE_INPUT_METHOD, imeInsetsSize),
-                        InsetsFrameProvider.InsetsSizeOverride(TYPE_VOICE_INTERACTION,
-                                // No-op override to keep the size and types in sync with the
-                                // override below (insetsSizeOverrides must have the same length and
-                                // types after the window is added according to
-                                // WindowManagerService#relayoutWindow)
-                                provider.insetsSize)
+            arrayOf(
+                InsetsFrameProvider.InsetsSizeOverride(TYPE_INPUT_METHOD, imeInsetsSize),
+                InsetsFrameProvider.InsetsSizeOverride(
+                    TYPE_VOICE_INTERACTION,
+                    // No-op override to keep the size and types in sync with the
+                    // override below (insetsSizeOverrides must have the same length and
+                    // types after the window is added according to
+                    // WindowManagerService#relayoutWindow)
+                    provider.insetsSize
                 )
+            )
         // Use 0 tappableElement insets for the VoiceInteractionWindow when gesture nav is enabled.
         val visInsetsSizeForTappableElement =
-                if (context.isGestureNav) getInsetsForGravity(0, gravity)
-                else getInsetsForGravity(tappableHeight, gravity)
+            if (context.isGestureNav) getInsetsForGravity(0, gravity)
+            else getInsetsForGravity(tappableHeight, gravity)
         val insetsSizeOverrideForTappableElement =
-                arrayOf(
-                        InsetsFrameProvider.InsetsSizeOverride(TYPE_INPUT_METHOD, imeInsetsSize),
-                        InsetsFrameProvider.InsetsSizeOverride(TYPE_VOICE_INTERACTION,
-                                visInsetsSizeForTappableElement
-                        ),
-                )
-        if ((context.isGestureNav || ENABLE_TASKBAR_NAVBAR_UNIFICATION)
-                && provider.type == tappableElement()) {
+            arrayOf(
+                InsetsFrameProvider.InsetsSizeOverride(TYPE_INPUT_METHOD, imeInsetsSize),
+                InsetsFrameProvider.InsetsSizeOverride(
+                    TYPE_VOICE_INTERACTION,
+                    visInsetsSizeForTappableElement
+                ),
+            )
+        if (
+            (context.isGestureNav || ENABLE_TASKBAR_NAVBAR_UNIFICATION) &&
+                provider.type == tappableElement()
+        ) {
             provider.insetsSizeOverrides = insetsSizeOverrideForTappableElement
         } else if (provider.type != systemGestures()) {
             // We only override insets at the bottom of the screen
@@ -264,8 +275,8 @@
     }
 
     /**
-     * @return [Insets] where the [inset] is either used as a bottom inset or
-     * right/left inset if using 3 button nav
+     * @return [Insets] where the [inset] is either used as a bottom inset or right/left inset if
+     *   using 3 button nav
      */
     private fun getInsetsForGravity(inset: Int, gravity: Int): Insets {
         if ((gravity and Gravity.BOTTOM) == Gravity.BOTTOM) {
@@ -277,7 +288,7 @@
         val isSeascape = (gravity and Gravity.START) == Gravity.START
         val leftInset = if (isSeascape) inset else 0
         val rightInset = if (isSeascape) 0 else inset
-        return Insets.of(leftInset , 0, rightInset, 0)
+        return Insets.of(leftInset, 0, rightInset, 0)
     }
 
     /**
@@ -292,6 +303,8 @@
             context.dragLayer,
             insetsInfo.touchableRegion
         )
+        debugTouchableRegion.lastSetTouchableBounds.set(insetsInfo.touchableRegion.bounds)
+
         val bubbleBarVisible =
             controllers.bubbleControllers.isPresent &&
                 controllers.bubbleControllers.get().bubbleBarViewController.isBubbleBarVisible()
@@ -299,21 +312,28 @@
         if (context.dragLayer.alpha < AlphaUpdateListener.ALPHA_CUTOFF_THRESHOLD) {
             // Let touches pass through us.
             insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_REGION)
+            debugTouchableRegion.lastSetTouchableReason = "Taskbar is invisible"
         } else if (
             controllers.navbarButtonsViewController.isImeVisible &&
                 controllers.taskbarStashController.isStashed
         ) {
+            // Let touches pass through us.
             insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_REGION)
+            debugTouchableRegion.lastSetTouchableReason = "Stashed over IME"
         } else if (!controllers.uiController.isTaskbarTouchable) {
             // Let touches pass through us.
             insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_REGION)
+            debugTouchableRegion.lastSetTouchableReason = "Taskbar is not touchable"
         } else if (controllers.taskbarDragController.isSystemDragInProgress) {
             // Let touches pass through us.
             insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_REGION)
+            debugTouchableRegion.lastSetTouchableReason = "System drag is in progress"
         } else if (context.isTaskbarWindowFullscreen) {
             // Intercept entire fullscreen window.
             insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_FRAME)
             insetsIsTouchableRegion = false
+            debugTouchableRegion.lastSetTouchableReason = "Taskbar is fullscreen"
+            context.dragLayer.getBoundsInWindow(debugTouchableRegion.lastSetTouchableBounds, false)
         } else if (
             controllers.taskbarViewController.areIconsVisible() ||
                 context.isNavBarKidsModeActive ||
@@ -342,19 +362,33 @@
                     region.op(bubbleBarBounds, Region.Op.UNION)
                 }
                 insetsInfo.touchableRegion.set(region)
+                debugTouchableRegion.lastSetTouchableReason = "Transient Taskbar is in Overview"
+                debugTouchableRegion.lastSetTouchableBounds.set(region.bounds)
             } else {
-                insetsInfo.touchableRegion.set(touchableRegion)
+                insetsInfo.touchableRegion.set(defaultTouchableRegion)
+                debugTouchableRegion.lastSetTouchableReason = "Using default touchable region"
+                debugTouchableRegion.lastSetTouchableBounds.set(defaultTouchableRegion.bounds)
             }
             insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_REGION)
             insetsIsTouchableRegion = false
         } else {
             insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_REGION)
+            debugTouchableRegion.lastSetTouchableReason =
+                "Icons are not visible, but other components such as 3 buttons might be"
         }
         context.excludeFromMagnificationRegion(insetsIsTouchableRegion)
     }
 
+    /** Draws the last set touchableRegion as a red rectangle onto the given Canvas. */
+    fun drawDebugTouchableRegionBounds(canvas: Canvas) {
+        val paint = Paint()
+        paint.color = Color.RED
+        paint.style = Paint.Style.STROKE
+        canvas.drawRect(debugTouchableRegion.lastSetTouchableBounds, paint)
+    }
+
     override fun dumpLogs(prefix: String, pw: PrintWriter) {
-        pw.println(prefix + "TaskbarInsetsController:")
+        pw.println("${prefix}TaskbarInsetsController:")
         pw.println("$prefix\twindowHeight=${windowLayoutParams.height}")
         for (provider in windowLayoutParams.providedInsets) {
             pw.print(
@@ -373,5 +407,12 @@
             }
             pw.println()
         }
+        pw.println("$prefix\tlastSetTouchableBounds=${debugTouchableRegion.lastSetTouchableBounds}")
+        pw.println("$prefix\tlastSetTouchableReason=${debugTouchableRegion.lastSetTouchableReason}")
+    }
+
+    class DebugTouchableRegion {
+        val lastSetTouchableBounds = Rect()
+        var lastSetTouchableReason = ""
     }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
index 9a37bcb..a850680 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
@@ -17,7 +17,6 @@
 
 import static com.android.app.animation.Interpolators.EMPHASIZED;
 import static com.android.launcher3.taskbar.TaskbarKeyguardController.MASK_ANY_SYSUI_LOCKED;
-import static com.android.launcher3.taskbar.TaskbarManager.isPhoneMode;
 import static com.android.launcher3.taskbar.TaskbarStashController.FLAG_IN_APP;
 import static com.android.launcher3.taskbar.TaskbarStashController.FLAG_IN_OVERVIEW;
 import static com.android.launcher3.taskbar.TaskbarStashController.FLAG_IN_STASHED_LAUNCHER_STATE;
@@ -205,13 +204,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 +215,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) {
@@ -726,7 +733,7 @@
         }
         mIconAlphaForHome.setValue(alpha);
         boolean hotseatVisible = alpha == 0
-                || isPhoneMode(mLauncher.getDeviceProfile())
+                || mControllers.taskbarActivityContext.isPhoneMode()
                 || (!mControllers.uiController.isHotseatIconOnTopWhenAligned()
                 && mIconAlignment.value > 0);
         /*
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
index c0b07e7..33641a4 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
@@ -25,6 +25,9 @@
 import static com.android.launcher3.LauncherState.OVERVIEW;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_TASKBAR_NAVBAR_UNIFICATION;
 import static com.android.launcher3.config.FeatureFlags.enableTaskbarNoRecreate;
+import static com.android.launcher3.util.DisplayController.CHANGE_DENSITY;
+import static com.android.launcher3.util.DisplayController.CHANGE_NAVIGATION_MODE;
+import static com.android.launcher3.util.DisplayController.CHANGE_TASKBAR_PINNING;
 import static com.android.launcher3.util.DisplayController.TASKBAR_NOT_DESTROYED_TAG;
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 import static com.android.launcher3.util.FlagDebugUtils.formatFlagChange;
@@ -55,7 +58,6 @@
 import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.InvariantDeviceProfile.OnIDPChangeListener;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.statemanager.StatefulActivity;
@@ -71,7 +73,6 @@
 import com.android.systemui.shared.system.QuickStepContract;
 import com.android.systemui.unfold.UnfoldTransitionProgressProvider;
 import com.android.systemui.unfold.util.ScopedUnfoldTransitionProgressProvider;
-import com.android.wm.shell.Flags;
 
 import java.io.PrintWriter;
 import java.util.StringJoiner;
@@ -106,6 +107,7 @@
             Settings.Secure.NAV_BAR_KIDS_MODE);
 
     private final Context mContext;
+    private final @Nullable Context mNavigationBarPanelContext;
     private WindowManager mWindowManager;
     private FrameLayout mTaskbarRootLayout;
     private boolean mAddedWindow;
@@ -134,7 +136,17 @@
      * We use WindowManager's ComponentCallbacks() for internal UI changes (similar to an Activity)
      * which comes via a different channel
      */
-    private final OnIDPChangeListener mIdpChangeListener = c -> recreateTaskbar();
+    private final RecreationListener mRecreationListener = new RecreationListener();
+
+    private class RecreationListener implements DisplayController.DisplayInfoChangeListener {
+        @Override
+        public void onDisplayInfoChanged(Context context, DisplayController.Info info, int flags) {
+            if ((flags & (CHANGE_DENSITY | CHANGE_NAVIGATION_MODE
+                    | CHANGE_TASKBAR_PINNING)) != 0) {
+                recreateTaskbar();
+            }
+        }
+    }
     private final SettingsCache.OnChangeListener mOnSettingsChangeListener = c -> recreateTaskbar();
 
     private boolean mUserUnlocked = false;
@@ -198,6 +210,9 @@
         mContext = service.createWindowContext(display,
                 ENABLE_TASKBAR_NAVBAR_UNIFICATION ? TYPE_NAVIGATION_BAR : TYPE_NAVIGATION_BAR_PANEL,
                 null);
+        mNavigationBarPanelContext = ENABLE_TASKBAR_NAVBAR_UNIFICATION
+                ? service.createWindowContext(display, TYPE_NAVIGATION_BAR_PANEL, null)
+                : null;
         if (enableTaskbarNoRecreate()) {
             mWindowManager = mContext.getSystemService(WindowManager.class);
             mTaskbarRootLayout = new FrameLayout(mContext) {
@@ -325,7 +340,10 @@
             return;
         }
 
-        if (mActivity != null && mActivity.isResumed() && !mActivity.isInState(OVERVIEW)) {
+        if (mActivity != null
+                && mActivity.isResumed()
+                && !mActivity.isInState(OVERVIEW)
+                && !(mActivity instanceof QuickstepLauncher l && l.areFreeformTasksVisible())) {
             mContext.startActivity(homeAllAppsIntent);
             return;
         }
@@ -349,7 +367,7 @@
      */
     public void onUserUnlocked() {
         mUserUnlocked = true;
-        LauncherAppState.getIDP(mContext).addOnChangeListener(mIdpChangeListener);
+        DisplayController.INSTANCE.get(mContext).addChangeListener(mRecreationListener);
         recreateTaskbar();
         addTaskbarRootViewToWindow();
     }
@@ -435,8 +453,9 @@
             }
 
             if (enableTaskbarNoRecreate() || mTaskbarActivityContext == null) {
-                mTaskbarActivityContext = new TaskbarActivityContext(mContext, dp,
-                        mNavButtonController, mUnfoldProgressProvider);
+                mTaskbarActivityContext = new TaskbarActivityContext(mContext,
+                        mNavigationBarPanelContext, dp, mNavButtonController,
+                        mUnfoldProgressProvider);
             } else {
                 mTaskbarActivityContext.updateDeviceProfile(dp);
             }
@@ -487,23 +506,7 @@
         }
     }
 
-    /**
-     * @return {@code true} if provided device profile isn't a large screen profile
-     *                      and we are using a single window for taskbar and navbar.
-     */
-    public static boolean isPhoneMode(DeviceProfile deviceProfile) {
-        return ENABLE_TASKBAR_NAVBAR_UNIFICATION && deviceProfile.isPhone;
-    }
-
-    /**
-     * @return {@code true} if {@link #isPhoneMode(DeviceProfile)} is true and we're using
-     *                      3 button-nav
-     */
-    public static boolean isPhoneButtonNavMode(TaskbarActivityContext context) {
-        return isPhoneMode(context.getDeviceProfile()) && context.isThreeButtonNav();
-    }
-
-    private boolean isTaskbarPresent(DeviceProfile deviceProfile) {
+    private static boolean isTaskbarPresent(DeviceProfile deviceProfile) {
         return ENABLE_TASKBAR_NAVBAR_UNIFICATION || deviceProfile.isTaskbarPresent;
     }
 
@@ -537,6 +540,14 @@
         }
     }
 
+    public void onNavigationBarLumaSamplingEnabled(int displayId, boolean enable) {
+        mSharedState.mLumaSamplingDisplayId = displayId;
+        mSharedState.mIsLumaSamplingEnabled = enable;
+        if (mTaskbarActivityContext != null) {
+            mTaskbarActivityContext.onNavigationBarLumaSamplingEnabled(displayId, enable);
+        }
+    }
+
     private void removeActivityCallbacksAndListeners() {
         if (mActivity != null) {
             mActivity.removeOnDeviceProfileChangeListener(mDebugActivityDeviceProfileChanged);
@@ -561,8 +572,9 @@
         UI_HELPER_EXECUTOR.execute(
                 () -> mTaskbarBroadcastReceiver.unregisterReceiverSafely(mContext));
         destroyExistingTaskbar();
+        removeTaskbarRootViewFromWindow();
         if (mUserUnlocked) {
-            LauncherAppState.getIDP(mContext).removeOnChangeListener(mIdpChangeListener);
+            DisplayController.INSTANCE.get(mContext).removeChangeListener(mRecreationListener);
         }
         SettingsCache.INSTANCE.get(mContext)
                 .unregister(USER_SETUP_COMPLETE_URI, mOnSettingsChangeListener);
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarPinningController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarPinningController.kt
index cbfa024..2f2d636 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarPinningController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarPinningController.kt
@@ -16,12 +16,17 @@
 package com.android.launcher3.taskbar
 
 import android.animation.AnimatorSet
+import android.annotation.SuppressLint
 import android.view.View
+import androidx.annotation.VisibleForTesting
 import androidx.core.animation.doOnEnd
+import com.android.app.animation.Interpolators
 import com.android.launcher3.LauncherPrefs
 import com.android.launcher3.LauncherPrefs.Companion.TASKBAR_PINNING
 import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_DIVIDER_MENU_CLOSE
 import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_DIVIDER_MENU_OPEN
+import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_PINNED
+import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_UNPINNED
 import com.android.launcher3.taskbar.TaskbarDividerPopupView.Companion.createAndPopulate
 import java.io.PrintWriter
 
@@ -31,46 +36,70 @@
 
     private lateinit var controllers: TaskbarControllers
     private lateinit var taskbarSharedState: TaskbarSharedState
-    private val launcherPrefs = LauncherPrefs.get(context)
+    private lateinit var launcherPrefs: LauncherPrefs
     private val statsLogManager = context.statsLogManager
-    private var isAnimatingTaskbarPinning = false
+    @VisibleForTesting var isAnimatingTaskbarPinning = false
+    @VisibleForTesting lateinit var onCloseCallback: (preferenceChanged: Boolean) -> Unit
 
+    @SuppressLint("VisibleForTests")
     fun init(taskbarControllers: TaskbarControllers, sharedState: TaskbarSharedState) {
         controllers = taskbarControllers
         taskbarSharedState = sharedState
+        launcherPrefs = context.launcherPrefs
+        onCloseCallback =
+            fun(didPreferenceChange: Boolean) {
+                statsLogManager.logger().log(LAUNCHER_TASKBAR_DIVIDER_MENU_CLOSE)
+                context.dragLayer.post { context.onPopupVisibilityChanged(false) }
+
+                if (!didPreferenceChange) {
+                    return
+                }
+                val animateToValue =
+                    if (!launcherPrefs.get(TASKBAR_PINNING)) {
+                        statsLogManager.logger().log(LAUNCHER_TASKBAR_PINNED)
+                        PINNING_PERSISTENT
+                    } else {
+                        statsLogManager.logger().log(LAUNCHER_TASKBAR_UNPINNED)
+                        PINNING_TRANSIENT
+                    }
+                taskbarSharedState.taskbarWasPinned = animateToValue == PINNING_TRANSIENT
+                animateTaskbarPinning(animateToValue)
+            }
     }
 
     fun showPinningView(view: View) {
         context.isTaskbarWindowFullscreen = true
-
         view.post {
-            val popupView = createAndPopulate(view, context)
+            val popupView = getPopupView(view)
             popupView.requestFocus()
-
-            popupView.onCloseCallback =
-                callback@{ didPreferenceChange ->
-                    statsLogManager.logger().log(LAUNCHER_TASKBAR_DIVIDER_MENU_CLOSE)
-                    context.dragLayer.post { context.onPopupVisibilityChanged(false) }
-
-                    if (!didPreferenceChange) {
-                        return@callback
-                    }
-                    val animateToValue =
-                        if (!launcherPrefs.get(TASKBAR_PINNING)) {
-                            PINNING_PERSISTENT
-                        } else {
-                            PINNING_TRANSIENT
-                        }
-                    taskbarSharedState.taskbarWasPinned = animateToValue == PINNING_TRANSIENT
-                    animateTaskbarPinning(animateToValue)
-                }
+            popupView.onCloseCallback = onCloseCallback
             context.onPopupVisibilityChanged(true)
             popupView.show()
             statsLogManager.logger().log(LAUNCHER_TASKBAR_DIVIDER_MENU_OPEN)
         }
     }
 
-    private fun animateTaskbarPinning(animateToValue: Float) {
+    @VisibleForTesting
+    fun getPopupView(view: View): TaskbarDividerPopupView<*> {
+        return createAndPopulate(view, context)
+    }
+
+    @VisibleForTesting
+    fun animateTaskbarPinning(animateToValue: Float) {
+        val taskbarViewController = controllers.taskbarViewController
+        val animatorSet =
+            getAnimatorSetForTaskbarPinningAnimation(animateToValue).apply {
+                doOnEnd { recreateTaskbarAndUpdatePinningValue() }
+                duration = PINNING_ANIMATION_DURATION
+            }
+        controllers.taskbarOverlayController.hideWindow()
+        updateIsAnimatingTaskbarPinningAndNotifyTaskbarDragLayer(true)
+        taskbarViewController.animateAwayNotificationDotsDuringTaskbarPinningAnimation()
+        animatorSet.start()
+    }
+
+    @VisibleForTesting
+    fun getAnimatorSetForTaskbarPinningAnimation(animateToValue: Float): AnimatorSet {
         val animatorSet = AnimatorSet()
         val taskbarViewController = controllers.taskbarViewController
         val dragLayerController = controllers.taskbarDragLayerController
@@ -82,13 +111,8 @@
             taskbarViewController.taskbarIconTranslationXForPinning.animateToValue(animateToValue)
         )
 
-        controllers.taskbarOverlayController.hideWindow()
-
-        animatorSet.doOnEnd { recreateTaskbarAndUpdatePinningValue() }
-        animatorSet.duration = PINNING_ANIMATION_DURATION
-        updateIsAnimatingTaskbarPinningAndNotifyTaskbarDragLayer(true)
-        taskbarViewController.animateAwayNotificationDotsDuringTaskbarPinningAnimation()
-        animatorSet.start()
+        animatorSet.interpolator = Interpolators.EMPHASIZED
+        return animatorSet
     }
 
     private fun updateIsAnimatingTaskbarPinningAndNotifyTaskbarDragLayer(isAnimating: Boolean) {
@@ -96,7 +120,8 @@
         context.dragLayer.setAnimatingTaskbarPinning(isAnimating)
     }
 
-    private fun recreateTaskbarAndUpdatePinningValue() {
+    @VisibleForTesting
+    fun recreateTaskbarAndUpdatePinningValue() {
         updateIsAnimatingTaskbarPinningAndNotifyTaskbarDragLayer(false)
         launcherPrefs.put(TASKBAR_PINNING, !launcherPrefs.get(TASKBAR_PINNING))
     }
@@ -110,6 +135,6 @@
     companion object {
         const val PINNING_PERSISTENT = 1f
         const val PINNING_TRANSIENT = 0f
-        const val PINNING_ANIMATION_DURATION = 500L
+        const val PINNING_ANIMATION_DURATION = 600L
     }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarSharedState.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarSharedState.java
index 176a8c5..41b777b 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarSharedState.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarSharedState.java
@@ -52,6 +52,10 @@
     // TaskbarManager#onNavButtonsDarkIntensityChanged()
     public float navButtonsDarkIntensity;
 
+    // TaskbarManager#onNavigationBarLumaSamplingEnabled()
+    public int mLumaSamplingDisplayId;
+    public boolean mIsLumaSamplingEnabled;
+
     public boolean setupUIVisible = false;
 
     public boolean allAppsVisible = false;
@@ -73,9 +77,21 @@
     };
 
     // Allows us to shift translation logic when doing taskbar pinning animation.
-    public Boolean startTaskbarVariantIsTransient = true;
+    public boolean startTaskbarVariantIsTransient = true;
 
     // To track if taskbar was pinned using taskbar pinning feature at the time of recreate,
     // so we can unstash transient taskbar when we un-pinning taskbar.
-    public Boolean taskbarWasPinned = false;
+    private boolean mTaskbarWasPinned = false;
+
+    public boolean getTaskbarWasPinned() {
+        return mTaskbarWasPinned;
+    }
+
+    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 9c532ec..eced202 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;
@@ -255,8 +256,9 @@
         mSystemUiProxy = SystemUiProxy.INSTANCE.get(activity);
         mAccessibilityManager = mActivity.getSystemService(AccessibilityManager.class);
 
-        if (isPhoneMode()) {
-            mUnstashedHeight = mActivity.getResources().getDimensionPixelSize(R.dimen.taskbar_size);
+        if (mActivity.isPhoneMode()) {
+            mUnstashedHeight = mActivity.getResources().getDimensionPixelSize(
+                    R.dimen.taskbar_phone_size);
             mStashedHeight = mActivity.getResources().getDimensionPixelSize(
                     R.dimen.taskbar_stashed_size);
         } else {
@@ -306,17 +308,22 @@
 
         boolean isTransientTaskbar = DisplayController.isTransientTaskbar(mActivity);
         boolean isInSetup = !mActivity.isUserSetupComplete() || setupUIVisible;
-        updateStateForFlag(FLAG_STASHED_IN_APP_AUTO,
-                isTransientTaskbar && !mTaskbarSharedState.taskbarWasPinned);
+        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()
+        updateStateForFlag(FLAG_STASHED_SMALL_SCREEN, mActivity.isPhoneMode()
                 && !mActivity.isThreeButtonNav());
         // For now, assume we're in an app, since LauncherTaskbarUIController won't be able to tell
         // 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.taskbarWasPinned) {
+        if (mTaskbarSharedState.getTaskbarWasPinned()
+                || !mTaskbarSharedState.taskbarWasStashedAuto) {
             tryStartTaskbarTimeout();
         }
         notifyStashChange(/* visible */ false, /* stashed */ isStashedInApp());
@@ -379,13 +386,6 @@
         return (hasAnyFlag(FLAG_IN_STASHED_LAUNCHER_STATE) && supportsVisualStashing());
     }
 
-    /**
-     * @return {@code true} if we're not on a large screen AND using gesture nav
-     */
-    private boolean isPhoneMode() {
-        return TaskbarManager.isPhoneMode(mActivity.getDeviceProfile());
-    }
-
     private boolean hasAnyFlag(int flagMask) {
         return hasAnyFlag(mState, flagMask);
     }
@@ -421,7 +421,7 @@
      * @see android.view.WindowInsets.Type#systemBars()
      */
     public int getContentHeightToReportToApps() {
-        if ((isPhoneMode() && !mActivity.isThreeButtonNav())
+        if ((mActivity.isPhoneMode() && !mActivity.isThreeButtonNav())
                 || DisplayController.isTransientTaskbar(mActivity)) {
             return getStashedHeight();
         }
@@ -491,6 +491,7 @@
         }
 
         if (hasAnyFlag(FLAG_STASHED_IN_APP_AUTO) != stash) {
+            mTaskbarSharedState.taskbarWasStashedAuto = stash;
             updateStateForFlag(FLAG_STASHED_IN_APP_AUTO, stash);
             applyState();
         }
@@ -571,7 +572,7 @@
         mAnimator = new AnimatorSet();
         addJankMonitorListener(mAnimator, /* appearing= */ !mIsStashed);
         boolean isTransientTaskbar = DisplayController.isTransientTaskbar(mActivity);
-        final float stashTranslation = isPhoneMode() || isTransientTaskbar
+        final float stashTranslation = mActivity.isPhoneMode() || isTransientTaskbar
                 ? 0
                 : (mUnstashedHeight - mStashedHeight);
 
@@ -589,6 +590,7 @@
             mAnimator.addListener(AnimatorListeners.forEndCallback(() -> {
                 mAnimator = null;
                 mIsStashed = isStashed;
+                onIsStashedChanged();
             }));
             return;
         }
@@ -603,7 +605,7 @@
             @Override
             public void onAnimationStart(Animator animation) {
                 mIsStashed = isStashed;
-                onIsStashedChanged(mIsStashed);
+                onIsStashedChanged();
 
                 cancelTimeoutIfExists();
             }
@@ -643,7 +645,7 @@
 
             firstHalfAnimatorSet.playTogether(
                     mIconAlphaForStash.animateToValue(0),
-                    mIconScaleForStash.animateToValue(isPhoneMode() ?
+                    mIconScaleForStash.animateToValue(mActivity.isPhoneMode() ?
                             0 : STASHED_TASKBAR_SCALE)
             );
             secondHalfAnimatorSet.playTogether(
@@ -828,9 +830,9 @@
                 .setDuration(TASKBAR_HINT_STASH_DURATION).start();
     }
 
-    private void onIsStashedChanged(boolean isStashed) {
+    private void onIsStashedChanged() {
         mControllers.runAfterInit(() -> {
-            mControllers.stashedHandleViewController.onIsStashedChanged(isStashed);
+            mControllers.stashedHandleViewController.onIsStashedChanged();
             mControllers.taskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged();
         });
     }
@@ -929,19 +931,26 @@
     }
 
     /**
-     * We stash when IME or IME switcher is showing AND NOT
-     *  * in small screen AND
-     *  * 3 button nav AND
-     *  * landscape (or seascape)
-     * We do not stash if taskbar is transient or hardware keyboard is active.
+     * We stash when IME or IME switcher is showing.
+     *
+     * <p>Do not stash if in small screen, with 3 button nav, and in landscape (or seascape).
+     * <p>Do not stash if taskbar is transient.
+     * <p>Do not stash if hardware keyboard is attached and taskbar is pinned.
      */
     private boolean shouldStashForIme() {
-        if (DisplayController.isTransientTaskbar(mActivity) || mActivity.isHardwareKeyboard()) {
+        if (DisplayController.isTransientTaskbar(mActivity)) {
             return false;
         }
-        return (mIsImeShowing || mIsImeSwitcherShowing) &&
-                !(isPhoneMode() && mActivity.isThreeButtonNav()
-                        && mActivity.getDeviceProfile().isLandscape);
+        // Do not stash if in small screen, with 3 button nav, and in landscape.
+        if (mActivity.isPhoneMode() && mActivity.isThreeButtonNav()
+                && mActivity.getDeviceProfile().isLandscape) {
+            return false;
+        }
+        // Do not stash if pinned taskbar and hardware keyboard is attached.
+        if (mActivity.isHardwareKeyboard() && DisplayController.isPinnedTaskbar(mActivity)) {
+            return false;
+        }
+        return mIsImeShowing || mIsImeSwitcherShowing;
     }
 
     /**
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
index a29a25c..ecedf8a 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
@@ -25,11 +25,13 @@
 import android.graphics.drawable.BitmapDrawable;
 import android.view.MotionEvent;
 import android.view.View;
+import android.window.RemoteTransition;
 
 import androidx.annotation.CallSuper;
 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;
@@ -218,7 +220,7 @@
                 Collections.singletonList(splitSelectSource.itemInfo.getComponentKey()),
                 false /* findExactPairMatch */,
                 foundTasks -> {
-                    @Nullable Task foundTask = foundTasks.get(0);
+                    @Nullable Task foundTask = foundTasks[0];
                     splitSelectSource.alreadyRunningTaskId = foundTask == null
                             ? INVALID_TASK_ID
                             : foundTask.key.id;
@@ -237,7 +239,7 @@
                 Collections.singletonList(info.getComponentKey()),
                 false /* findExactPairMatch */,
                 foundTasks -> {
-                    @Nullable Task foundTask = foundTasks.get(0);
+                    @Nullable Task foundTask = foundTasks[0];
                     if (foundTask != null) {
                         TaskView foundTaskView = recents.getTaskViewByTaskId(foundTask.key.id);
                         // TODO (b/266482558): This additional null check is needed because there
@@ -301,7 +303,8 @@
     /**
      * Launches the given task in split-screen.
      */
-    public void launchSplitTasks(@NonNull GroupTask groupTask) { }
+    public void launchSplitTasks(
+            @NonNull GroupTask groupTask, @Nullable RemoteTransition remoteTransition) { }
 
     /**
      * Returns the matching view (if any) in the taskbar.
@@ -330,6 +333,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/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
index 2ab0066..74517a8 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
@@ -123,7 +123,7 @@
         mIconLayoutBounds = mActivityContext.getTransientTaskbarBounds();
         Resources resources = getResources();
         boolean isTransientTaskbar = DisplayController.isTransientTaskbar(mActivityContext)
-                && !TaskbarManager.isPhoneMode(mActivityContext.getDeviceProfile());
+                && !mActivityContext.isPhoneMode();
         mIsRtl = Utilities.isRtl(resources);
         mTransientTaskbarMinWidth = resources.getDimension(R.dimen.transient_taskbar_min_width);
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
index 6eda796..b762781 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
@@ -29,8 +29,6 @@
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_ALLAPPS_BUTTON_TAP;
 import static com.android.launcher3.taskbar.TaskbarPinningController.PINNING_PERSISTENT;
 import static com.android.launcher3.taskbar.TaskbarPinningController.PINNING_TRANSIENT;
-import static com.android.launcher3.taskbar.TaskbarManager.isPhoneButtonNavMode;
-import static com.android.launcher3.taskbar.TaskbarManager.isPhoneMode;
 import static com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VALUE;
 import static com.android.launcher3.util.MultiTranslateDelegate.INDEX_TASKBAR_ALIGNMENT_ANIM;
 import static com.android.launcher3.util.MultiTranslateDelegate.INDEX_TASKBAR_PINNING_ANIM;
@@ -161,6 +159,8 @@
     private final int mTransientIconSize;
     private final int mPersistentIconSize;
 
+    private final float mTaskbarLeftRightMargin;
+
     public TaskbarViewController(TaskbarActivityContext activity, TaskbarView taskbarView) {
         mActivity = activity;
         mTransientTaskbarDp = mActivity.getTransientTaskbarDeviceProfile();
@@ -186,13 +186,16 @@
             mTaskbarThemedIconsBackgroundColor = ColorUtils.HSLToColor(colorHSL);
         }
         mIsRtl = Utilities.isRtl(mTaskbarView.getResources());
+        mTaskbarLeftRightMargin = mActivity.getResources().getDimensionPixelSize(
+                R.dimen.transient_taskbar_padding);
+
     }
 
     public void init(TaskbarControllers controllers) {
         mControllers = controllers;
         mTaskbarView.init(new TaskbarViewCallbacks());
-        mTaskbarView.getLayoutParams().height = isPhoneMode(mActivity.getDeviceProfile())
-                ? mActivity.getResources().getDimensionPixelSize(R.dimen.taskbar_size)
+        mTaskbarView.getLayoutParams().height = mActivity.isPhoneMode()
+                ? mActivity.getResources().getDimensionPixelSize(R.dimen.taskbar_phone_size)
                 : mActivity.getDeviceProfile().taskbarHeight;
 
         mTaskbarIconScaleForStash.updateValue(1f);
@@ -219,7 +222,7 @@
             // This gets modified in NavbarButtonsViewController, but the initial value it reads
             // may be incorrect since it's state gets destroyed on taskbar recreate, so reset here
             mTaskbarIconAlpha.get(ALPHA_INDEX_SMALL_SCREEN)
-                    .animateToValue(isPhoneButtonNavMode(mActivity) ? 0 : 1).start();
+                    .animateToValue(mActivity.isPhoneButtonNavMode() ? 0 : 1).start();
         }
         if (enableTaskbarPinning()) {
             mTaskbarView.addOnLayoutChangeListener(mTaskbarViewLayoutChangeListener);
@@ -393,6 +396,26 @@
         }
     }
 
+    /**
+     * Calculates visual taskbar view width.
+     */
+    public float getCurrentVisualTaskbarWidth() {
+        if (mTaskbarView.getIconViews().length == 0) {
+            return 0;
+        }
+
+        View[] iconViews = mTaskbarView.getIconViews();
+
+        int leftIndex = mActivity.getDeviceProfile().isQsbInline && !mIsRtl ? 1 : 0;
+        int rightIndex = mActivity.getDeviceProfile().isQsbInline && mIsRtl
+                ? iconViews.length - 2
+                : iconViews.length - 1;
+
+        float left = iconViews[leftIndex].getX();
+        float right = iconViews[rightIndex].getRight() + iconViews[rightIndex].getTranslationX();
+
+        return right - left + (2 * mTaskbarLeftRightMargin);
+    }
 
     /**
      * Sets the translation of the TaskbarView during the swipe up gesture.
@@ -602,7 +625,7 @@
      *                       1 => fully aligned
      */
     public void setLauncherIconAlignment(float alignmentRatio, DeviceProfile launcherDp) {
-        if (isPhoneMode(launcherDp)) {
+        if (mActivity.isPhoneMode()) {
             mIconAlignControllerLazy = null;
             return;
         }
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/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
index c482911..ec9f4e5 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
@@ -130,6 +130,8 @@
         super(context, attrs, defStyleAttr, defStyleRes);
         TaskbarActivityContext activityContext = ActivityContext.lookupContext(context);
 
+        setAlpha(0);
+        setVisibility(INVISIBLE);
         mIconOverlapAmount = getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_overlap);
         mIconSpacing = getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_spacing);
         mIconSize = getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_size);
diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/AbstractNavButtonLayoutter.kt b/quickstep/src/com/android/launcher3/taskbar/navbutton/AbstractNavButtonLayoutter.kt
index b516d6f..23e3310 100644
--- a/quickstep/src/com/android/launcher3/taskbar/navbutton/AbstractNavButtonLayoutter.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/AbstractNavButtonLayoutter.kt
@@ -65,18 +65,19 @@
 
     fun getParamsToCenterView(): FrameLayout.LayoutParams {
         val params = FrameLayout.LayoutParams(
-                ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
+                ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
         params.gravity = Gravity.CENTER
         return params;
     }
 
-    open fun repositionContextualContainer(contextualContainer: ViewGroup, barAxisMargin: Int,
+    open fun repositionContextualContainer(contextualContainer: ViewGroup, buttonSize: Int,
+                                           barAxisMarginStart: Int, barAxisMarginEnd: Int,
                                            gravity: Int) {
         val contextualContainerParams = FrameLayout.LayoutParams(
-                ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT)
+                buttonSize, ViewGroup.LayoutParams.MATCH_PARENT)
         contextualContainerParams.apply {
-            marginStart = barAxisMargin
-            marginEnd = barAxisMargin
+            marginStart = barAxisMarginStart
+            marginEnd = barAxisMarginEnd
             topMargin = 0
             bottomMargin = 0
         }
diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/KidsNavLayoutter.kt b/quickstep/src/com/android/launcher3/taskbar/navbutton/KidsNavLayoutter.kt
index 3f51958..f31af09 100644
--- a/quickstep/src/com/android/launcher3/taskbar/navbutton/KidsNavLayoutter.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/KidsNavLayoutter.kt
@@ -21,11 +21,12 @@
 import android.graphics.drawable.PaintDrawable
 import android.view.Gravity
 import android.view.ViewGroup
+import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
 import android.widget.FrameLayout
 import android.widget.ImageView
 import android.widget.LinearLayout
-import com.android.launcher3.DeviceProfile
 import com.android.launcher3.R
+import com.android.launcher3.taskbar.TaskbarActivityContext
 import com.android.launcher3.taskbar.navbutton.LayoutResourceHelper.*
 import com.android.systemui.shared.rotation.RotationButton
 
@@ -48,7 +49,7 @@
             a11yButton
     ) {
 
-    override fun layoutButtons(dp: DeviceProfile, isA11yButtonPersistent: Boolean) {
+    override fun layoutButtons(context: TaskbarActivityContext, isA11yButtonPersistent: Boolean) {
         val iconSize: Int = resources.getDimensionPixelSize(DIMEN_TASKBAR_ICON_SIZE_KIDS)
         val buttonWidth: Int = resources.getDimensionPixelSize(DIMEN_TASKBAR_NAV_BUTTONS_WIDTH_KIDS)
         val buttonHeight: Int =
@@ -103,8 +104,9 @@
 
         val contextualMargin = resources.getDimensionPixelSize(
                 R.dimen.taskbar_contextual_button_padding)
-        repositionContextualContainer(endContextualContainer, 0, Gravity.END)
-        repositionContextualContainer(startContextualContainer, contextualMargin, Gravity.START)
+        repositionContextualContainer(endContextualContainer, WRAP_CONTENT, 0, 0, Gravity.END)
+        repositionContextualContainer(startContextualContainer, WRAP_CONTENT, contextualMargin,
+                contextualMargin, Gravity.START)
 
         if (imeSwitcher != null) {
             startContextualContainer.addView(imeSwitcher)
diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/NavButtonLayoutFactory.kt b/quickstep/src/com/android/launcher3/taskbar/navbutton/NavButtonLayoutFactory.kt
index 6b05e9a..22f0131 100644
--- a/quickstep/src/com/android/launcher3/taskbar/navbutton/NavButtonLayoutFactory.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/NavButtonLayoutFactory.kt
@@ -24,6 +24,7 @@
 import android.widget.ImageView
 import android.widget.LinearLayout
 import com.android.launcher3.DeviceProfile
+import com.android.launcher3.taskbar.TaskbarActivityContext
 import com.android.launcher3.taskbar.navbutton.LayoutResourceHelper.*
 import com.android.launcher3.taskbar.navbutton.NavButtonLayoutFactory.Companion
 import com.android.launcher3.taskbar.navbutton.NavButtonLayoutFactory.NavButtonLayoutter
@@ -162,6 +163,6 @@
 
     /** Lays out and provides access to the home, recents, and back buttons for various mischief */
     interface NavButtonLayoutter {
-        fun layoutButtons(dp: DeviceProfile, isA11yButtonPersistent: Boolean)
+        fun layoutButtons(context: TaskbarActivityContext, isA11yButtonPersistent: Boolean)
     }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneGestureLayoutter.kt b/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneGestureLayoutter.kt
index 5a7bc49..3817f91 100644
--- a/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneGestureLayoutter.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneGestureLayoutter.kt
@@ -20,7 +20,7 @@
 import android.view.ViewGroup
 import android.widget.ImageView
 import android.widget.LinearLayout
-import com.android.launcher3.DeviceProfile
+import com.android.launcher3.taskbar.TaskbarActivityContext
 import com.android.systemui.shared.rotation.RotationButton
 
 /** Layoutter for showing gesture navigation on phone screen. No buttons here, no-op container */
@@ -43,7 +43,7 @@
                 a11yButton
         ) {
 
-    override fun layoutButtons(dp: DeviceProfile, isA11yButtonPersistent: Boolean) {
+    override fun layoutButtons(context: TaskbarActivityContext, isA11yButtonPersistent: Boolean) {
         endContextualContainer.removeAllViews()
         startContextualContainer.removeAllViews()
     }
diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneLandscapeNavLayoutter.kt b/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneLandscapeNavLayoutter.kt
index 382e298..b1b50d6 100644
--- a/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneLandscapeNavLayoutter.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneLandscapeNavLayoutter.kt
@@ -22,11 +22,8 @@
 import android.widget.FrameLayout
 import android.widget.ImageView
 import android.widget.LinearLayout
-import androidx.core.view.children
-import com.android.launcher3.DeviceProfile
 import com.android.launcher3.R
-import com.android.launcher3.taskbar.TaskbarManager
-import com.android.launcher3.util.DimensionUtils
+import com.android.launcher3.taskbar.TaskbarActivityContext
 import com.android.systemui.shared.rotation.RotationButton
 
 open class PhoneLandscapeNavLayoutter(
@@ -48,49 +45,68 @@
             a11yButton
     ) {
 
-    override fun layoutButtons(dp: DeviceProfile, isA11yButtonPersistent: Boolean) {
-        // TODO(b/230395757): Polish pending, this is just to make it usable
-        val endStartMargins = resources.getDimensionPixelSize(R.dimen.taskbar_nav_buttons_size)
-        val taskbarDimensions = DimensionUtils.getTaskbarPhoneDimensions(dp, resources,
-                TaskbarManager.isPhoneMode(dp))
-        navButtonContainer.removeAllViews()
-        navButtonContainer.orientation = LinearLayout.VERTICAL
+    override fun layoutButtons(context: TaskbarActivityContext, isA11yButtonPersistent: Boolean) {
+        val totalHeight = context.deviceProfile.heightPx
+        val homeButtonHeight = resources.getDimensionPixelSize(
+                R.dimen.taskbar_phone_home_button_size)
+        val roundedCornerContentMargin = resources.getDimensionPixelSize(
+                R.dimen.taskbar_phone_rounded_corner_content_margin)
+        val contentPadding = resources.getDimensionPixelSize(R.dimen.taskbar_phone_content_padding)
+        val contentWidth = totalHeight - roundedCornerContentMargin * 2 - contentPadding * 2
+
+        // left:back:space(reserved for home):overview:right = 0.25:0.5:1:0.5:0.25
+        val contextualButtonHeight = contentWidth / (0.25f + 0.5f + 1f + 0.5f + 0.25f) * 0.25f
+        val sideButtonHeight = contextualButtonHeight * 2
+        val navButtonContainerHeight = contentWidth - contextualButtonHeight * 2
 
         val navContainerParams = FrameLayout.LayoutParams(
-                taskbarDimensions.x, ViewGroup.LayoutParams.MATCH_PARENT)
+            ViewGroup.LayoutParams.MATCH_PARENT, navButtonContainerHeight.toInt())
         navContainerParams.apply {
-            topMargin = endStartMargins
-            bottomMargin = endStartMargins
+            topMargin =
+                    (contextualButtonHeight + contentPadding + roundedCornerContentMargin).toInt()
+            bottomMargin =
+                    (contextualButtonHeight + contentPadding + roundedCornerContentMargin).toInt()
             marginEnd = 0
             marginStart = 0
         }
 
-        navButtonContainer.layoutParams = navContainerParams
-        navButtonContainer.gravity = Gravity.CENTER
+        // Ensure order of buttons is correct
+        navButtonContainer.removeAllViews()
+        navButtonContainer.orientation = LinearLayout.VERTICAL
 
         addThreeButtons()
 
+        navButtonContainer.layoutParams = navContainerParams
+        navButtonContainer.gravity = Gravity.CENTER
+
         // Add the spaces in between the nav buttons
-        val spaceInBetween: Int =
-            resources.getDimensionPixelSize(R.dimen.taskbar_button_space_inbetween_phone)
-        navButtonContainer.children.forEachIndexed { i, navButton ->
+        val spaceInBetween = (navButtonContainerHeight - homeButtonHeight -
+                sideButtonHeight * 2) / 2.0f
+        for (i in 0 until navButtonContainer.childCount) {
+            val navButton = navButtonContainer.getChildAt(i)
             val buttonLayoutParams = navButton.layoutParams as LinearLayout.LayoutParams
-            buttonLayoutParams.weight = 1f
+            val margin = (spaceInBetween / 2).toInt()
             when (i) {
                 0 -> {
-                    buttonLayoutParams.bottomMargin = spaceInBetween / 2
+                    // First button
+                    buttonLayoutParams.bottomMargin = margin
+                    buttonLayoutParams.height = sideButtonHeight.toInt()
                 }
                 navButtonContainer.childCount - 1 -> {
-                    buttonLayoutParams.topMargin = spaceInBetween / 2
+                    // Last button
+                    buttonLayoutParams.topMargin = margin
+                    buttonLayoutParams.height = sideButtonHeight.toInt()
                 }
                 else -> {
-                    buttonLayoutParams.bottomMargin = spaceInBetween / 2
-                    buttonLayoutParams.topMargin = spaceInBetween / 2
+                    // other buttons
+                    buttonLayoutParams.topMargin = margin
+                    buttonLayoutParams.bottomMargin = margin
+                    buttonLayoutParams.height = homeButtonHeight
                 }
             }
         }
 
-        repositionContextualButtons()
+        repositionContextualButtons(contextualButtonHeight.toInt())
     }
 
     open fun addThreeButtons() {
@@ -100,13 +116,15 @@
         navButtonContainer.addView(backButton)
     }
 
-    open fun repositionContextualButtons() {
+    open fun repositionContextualButtons(buttonSize: Int) {
         endContextualContainer.removeAllViews()
         startContextualContainer.removeAllViews()
 
-        val contextualMargin = resources.getDimensionPixelSize(
-                R.dimen.taskbar_contextual_button_padding)
-        repositionContextualContainer(startContextualContainer, contextualMargin, Gravity.TOP)
+        val roundedCornerContentMargin = resources.getDimensionPixelSize(
+                R.dimen.taskbar_phone_rounded_corner_content_margin)
+        val contentPadding = resources.getDimensionPixelSize(R.dimen.taskbar_phone_content_padding)
+        repositionContextualContainer(startContextualContainer, buttonSize,
+                roundedCornerContentMargin + contentPadding, 0, Gravity.TOP)
 
         if (imeSwitcher != null) {
             startContextualContainer.addView(imeSwitcher)
@@ -121,15 +139,16 @@
         }
     }
 
-    override fun repositionContextualContainer(contextualContainer: ViewGroup, barAxisMargin: Int,
+    override fun repositionContextualContainer(contextualContainer: ViewGroup, buttonSize: Int,
+                                               barAxisMarginTop: Int, barAxisMarginBottom: Int,
                                                gravity: Int) {
         val contextualContainerParams = FrameLayout.LayoutParams(
-                ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
+                ViewGroup.LayoutParams.MATCH_PARENT, buttonSize)
         contextualContainerParams.apply {
             marginStart = 0
             marginEnd = 0
-            topMargin = barAxisMargin
-            bottomMargin = barAxisMargin
+            topMargin = barAxisMarginTop
+            bottomMargin = barAxisMarginBottom
         }
         contextualContainerParams.gravity = gravity or Gravity.CENTER_HORIZONTAL
         contextualContainer.layoutParams = contextualContainerParams
diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/PhonePortraitNavLayoutter.kt b/quickstep/src/com/android/launcher3/taskbar/navbutton/PhonePortraitNavLayoutter.kt
index e1ffa4d..05183b8 100644
--- a/quickstep/src/com/android/launcher3/taskbar/navbutton/PhonePortraitNavLayoutter.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/PhonePortraitNavLayoutter.kt
@@ -22,10 +22,8 @@
 import android.widget.FrameLayout
 import android.widget.ImageView
 import android.widget.LinearLayout
-import com.android.launcher3.DeviceProfile
 import com.android.launcher3.R
-import com.android.launcher3.taskbar.TaskbarManager
-import com.android.launcher3.util.DimensionUtils
+import com.android.launcher3.taskbar.TaskbarActivityContext
 import com.android.systemui.shared.rotation.RotationButton
 
 class PhonePortraitNavLayoutter(
@@ -47,27 +45,34 @@
             a11yButton
     ) {
 
-    override fun layoutButtons(dp: DeviceProfile, isA11yButtonPersistent: Boolean) {
-        // TODO(b/230395757): Polish pending, this is just to make it usable
-        val taskbarDimensions =
-            DimensionUtils.getTaskbarPhoneDimensions(dp, resources,
-                    TaskbarManager.isPhoneMode(dp))
-        val endStartMargins = resources.getDimensionPixelSize(R.dimen.taskbar_nav_buttons_size)
+    override fun layoutButtons(context: TaskbarActivityContext, isA11yButtonPersistent: Boolean) {
+        val totalWidth = context.deviceProfile.widthPx
+        val homeButtonWidth = resources.getDimensionPixelSize(R.dimen.taskbar_phone_home_button_size)
+        val roundedCornerContentMargin = resources.getDimensionPixelSize(
+                R.dimen.taskbar_phone_rounded_corner_content_margin)
+        val contentPadding = resources.getDimensionPixelSize(R.dimen.taskbar_phone_content_padding)
+        val contentWidth = totalWidth - roundedCornerContentMargin * 2 - contentPadding * 2
+
+        // left:back:space(reserved for home):overview:right = 0.25:0.5:1:0.5:0.25
+        val contextualButtonWidth = contentWidth / (0.25f + 0.5f + 1f + 0.5f + 0.25f) * 0.25f
+        val sideButtonWidth = contextualButtonWidth * 2
+        val navButtonContainerWidth = contentWidth - contextualButtonWidth * 2
+
+        val navContainerParams = FrameLayout.LayoutParams(navButtonContainerWidth.toInt(),
+                ViewGroup.LayoutParams.MATCH_PARENT)
+        navContainerParams.apply {
+            topMargin = 0
+            bottomMargin = 0
+            marginEnd =
+                    (contextualButtonWidth + contentPadding + roundedCornerContentMargin).toInt()
+            marginStart =
+                    (contextualButtonWidth + contentPadding + roundedCornerContentMargin).toInt()
+        }
 
         // Ensure order of buttons is correct
         navButtonContainer.removeAllViews()
         navButtonContainer.orientation = LinearLayout.HORIZONTAL
 
-        val navContainerParams = FrameLayout.LayoutParams(
-                taskbarDimensions.x, ViewGroup.LayoutParams.MATCH_PARENT)
-        navContainerParams.apply {
-            topMargin = 0
-            bottomMargin = 0
-            marginEnd = endStartMargins
-            marginStart = endStartMargins
-        }
-
-        // Swap recents and back button in case we were landscape prior to this
         navButtonContainer.addView(backButton)
         navButtonContainer.addView(homeButton)
         navButtonContainer.addView(recentsButton)
@@ -76,25 +81,28 @@
         navButtonContainer.gravity = Gravity.CENTER
 
         // Add the spaces in between the nav buttons
-        val spaceInBetween =
-            resources.getDimensionPixelSize(R.dimen.taskbar_button_space_inbetween_phone)
+        val spaceInBetween = (navButtonContainerWidth - homeButtonWidth -
+                sideButtonWidth * 2) / 2.0f
         for (i in 0 until navButtonContainer.childCount) {
             val navButton = navButtonContainer.getChildAt(i)
             val buttonLayoutParams = navButton.layoutParams as LinearLayout.LayoutParams
-            buttonLayoutParams.weight = 1f
+            val margin = (spaceInBetween / 2).toInt()
             when (i) {
                 0 -> {
                     // First button
-                    buttonLayoutParams.marginEnd = spaceInBetween / 2
+                    buttonLayoutParams.marginEnd = margin
+                    buttonLayoutParams.width = sideButtonWidth.toInt()
                 }
                 navButtonContainer.childCount - 1 -> {
                     // Last button
-                    buttonLayoutParams.marginStart = spaceInBetween / 2
+                    buttonLayoutParams.marginStart = margin
+                    buttonLayoutParams.width = sideButtonWidth.toInt()
                 }
                 else -> {
                     // other buttons
-                    buttonLayoutParams.marginStart = spaceInBetween / 2
-                    buttonLayoutParams.marginEnd = spaceInBetween / 2
+                    buttonLayoutParams.marginStart = margin
+                    buttonLayoutParams.marginEnd = margin
+                    buttonLayoutParams.width = homeButtonWidth
                 }
             }
         }
@@ -102,9 +110,8 @@
         endContextualContainer.removeAllViews()
         startContextualContainer.removeAllViews()
 
-        val contextualMargin = resources.getDimensionPixelSize(
-                R.dimen.taskbar_contextual_button_padding)
-        repositionContextualContainer(endContextualContainer, contextualMargin, Gravity.END)
+        repositionContextualContainer(endContextualContainer, contextualButtonWidth.toInt(), 0,
+                roundedCornerContentMargin + contentPadding, Gravity.END)
 
         if (imeSwitcher != null) {
             endContextualContainer.addView(imeSwitcher)
diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneSeascapeNavLayoutter.kt b/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneSeascapeNavLayoutter.kt
index 0368b1d..0f52552 100644
--- a/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneSeascapeNavLayoutter.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneSeascapeNavLayoutter.kt
@@ -50,13 +50,15 @@
         navButtonContainer.addView(recentsButton)
     }
 
-    override fun repositionContextualButtons() {
+    override fun repositionContextualButtons(buttonSize: Int) {
         endContextualContainer.removeAllViews()
         startContextualContainer.removeAllViews()
 
-        val contextualMargin = resources.getDimensionPixelSize(
-                R.dimen.taskbar_contextual_button_padding)
-        repositionContextualContainer(endContextualContainer, contextualMargin, Gravity.BOTTOM)
+        val roundedCornerContentMargin = resources.getDimensionPixelSize(
+                R.dimen.taskbar_phone_rounded_corner_content_margin)
+        val contentPadding = resources.getDimensionPixelSize(R.dimen.taskbar_phone_content_padding)
+        repositionContextualContainer(endContextualContainer, buttonSize, 0,
+                roundedCornerContentMargin + contentPadding,  Gravity.BOTTOM)
 
         if (imeSwitcher != null) {
             endContextualContainer.addView(imeSwitcher)
diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/SetupNavLayoutter.kt b/quickstep/src/com/android/launcher3/taskbar/navbutton/SetupNavLayoutter.kt
index abdd32c..5111bba 100644
--- a/quickstep/src/com/android/launcher3/taskbar/navbutton/SetupNavLayoutter.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/SetupNavLayoutter.kt
@@ -19,11 +19,12 @@
 import android.content.res.Resources
 import android.view.Gravity
 import android.view.ViewGroup
+import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
 import android.widget.FrameLayout
 import android.widget.ImageView
 import android.widget.LinearLayout
-import com.android.launcher3.DeviceProfile
 import com.android.launcher3.R
+import com.android.launcher3.taskbar.TaskbarActivityContext
 import com.android.systemui.shared.rotation.RotationButton
 
 class SetupNavLayoutter(
@@ -45,7 +46,7 @@
             a11yButton
     ) {
 
-    override fun layoutButtons(dp: DeviceProfile, isA11yButtonPersistent: Boolean) {
+    override fun layoutButtons(context: TaskbarActivityContext, isA11yButtonPersistent: Boolean) {
         // Since setup wizard only has back button enabled, it looks strange to be
         // end-aligned, so start-align instead.
         val navButtonsLayoutParams = navButtonContainer.layoutParams as FrameLayout.LayoutParams
@@ -61,8 +62,9 @@
 
         val contextualMargin = resources.getDimensionPixelSize(
                 R.dimen.taskbar_contextual_button_padding)
-        repositionContextualContainer(endContextualContainer, 0, Gravity.END)
-        repositionContextualContainer(startContextualContainer, contextualMargin, Gravity.START)
+        repositionContextualContainer(endContextualContainer, WRAP_CONTENT, 0, 0, Gravity.END)
+        repositionContextualContainer(startContextualContainer, WRAP_CONTENT, contextualMargin,
+                contextualMargin, Gravity.START)
 
         if (imeSwitcher != null) {
             startContextualContainer.addView(imeSwitcher)
diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/TaskbarNavLayoutter.kt b/quickstep/src/com/android/launcher3/taskbar/navbutton/TaskbarNavLayoutter.kt
index f5a4c64..45dbebb 100644
--- a/quickstep/src/com/android/launcher3/taskbar/navbutton/TaskbarNavLayoutter.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/TaskbarNavLayoutter.kt
@@ -19,11 +19,12 @@
 import android.content.res.Resources
 import android.view.Gravity
 import android.view.ViewGroup
+import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
 import android.widget.FrameLayout
 import android.widget.ImageView
 import android.widget.LinearLayout
-import com.android.launcher3.DeviceProfile
 import com.android.launcher3.R
+import com.android.launcher3.taskbar.TaskbarActivityContext
 import com.android.systemui.shared.rotation.RotationButton
 
 /**
@@ -48,9 +49,11 @@
             a11yButton
     ) {
 
-    override fun layoutButtons(dp: DeviceProfile, isA11yButtonPersistent: Boolean) {
+    override fun layoutButtons(context: TaskbarActivityContext, isA11yButtonPersistent: Boolean) {
         // Add spacing after the end of the last nav button
-        var navMarginEnd = resources.getDimension(dp.inv.inlineNavButtonsEndSpacing).toInt()
+        var navMarginEnd = resources
+                .getDimension(context.deviceProfile.inv.inlineNavButtonsEndSpacing)
+                .toInt()
         val contextualWidth = endContextualContainer.width
         // If contextual buttons are showing, we check if the end margin is enough for the
         // contextual button to be showing - if not, move the nav buttons over a smidge
@@ -91,11 +94,12 @@
         endContextualContainer.removeAllViews()
         startContextualContainer.removeAllViews()
 
-        if (!dp.isGestureMode) {
+        if (!context.deviceProfile.isGestureMode) {
             val contextualMargin = resources.getDimensionPixelSize(
                     R.dimen.taskbar_contextual_button_padding)
-            repositionContextualContainer(endContextualContainer, 0, Gravity.END)
-            repositionContextualContainer(startContextualContainer, contextualMargin, Gravity.START)
+            repositionContextualContainer(endContextualContainer, WRAP_CONTENT, 0, 0, Gravity.END)
+            repositionContextualContainer(startContextualContainer, WRAP_CONTENT, contextualMargin,
+                    contextualMargin, Gravity.START)
 
             if (imeSwitcher != null) {
                 startContextualContainer.addView(imeSwitcher)
diff --git a/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayController.java b/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayController.java
index c4eeea7..adbec65 100644
--- a/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayController.java
@@ -16,6 +16,7 @@
 package com.android.launcher3.taskbar.overlay;
 
 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
+import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_CONSUME_IME_INSETS;
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
 
 import static com.android.launcher3.AbstractFloatingView.TYPE_ALL;
@@ -187,6 +188,7 @@
         layoutParams.setFitInsetsTypes(0); // Handled by container view.
         layoutParams.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
         layoutParams.setSystemApplicationOverlay(true);
+        layoutParams.privateFlags = PRIVATE_FLAG_CONSUME_IME_INSETS;
         return layoutParams;
     }
 
diff --git a/quickstep/src/com/android/launcher3/uioverrides/ApiWrapper.java b/quickstep/src/com/android/launcher3/uioverrides/ApiWrapper.java
index 4a26559..3ebc8ed 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/ApiWrapper.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/ApiWrapper.java
@@ -17,13 +17,16 @@
 package com.android.launcher3.uioverrides;
 
 import android.app.ActivityOptions;
+import android.app.PendingIntent;
 import android.app.Person;
 import android.content.Context;
+import android.content.Intent;
 import android.content.pm.LauncherActivityInfo;
 import android.content.pm.LauncherApps;
 import android.content.pm.LauncherUserInfo;
 import android.content.pm.ShortcutInfo;
 import android.graphics.drawable.ColorDrawable;
+import android.net.Uri;
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.util.ArrayMap;
@@ -31,9 +34,12 @@
 
 import com.android.launcher3.Flags;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.proxy.ProxyActivityStarter;
+import com.android.launcher3.util.StartActivityParams;
 import com.android.launcher3.util.UserIconInfo;
 import com.android.quickstep.util.FadeOutRemoteTransition;
 
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 
@@ -74,6 +80,9 @@
                 if (android.os.Flags.allowPrivateProfile() && Flags.enablePrivateSpace()) {
                     LauncherApps launcherApps = context.getSystemService(LauncherApps.class);
                     LauncherUserInfo launcherUserInfo = launcherApps.getLauncherUserInfo(user);
+                    if (launcherUserInfo == null) {
+                        continue;
+                    }
                     // UserTypes not supported in Launcher are deemed to be the current
                     // Foreground User.
                     int userType = switch (launcherUserInfo.getUserType()) {
@@ -102,6 +111,46 @@
         return users;
     }
 
+    /**
+     * Returns the list of the system packages that are installed at user creation.
+     * An empty list denotes that all system packages are installed for that user at creation.
+     */
+    public static List<String> getPreInstalledSystemPackages(Context context, UserHandle user) {
+        LauncherApps launcherApps = context.getSystemService(LauncherApps.class);
+        if (android.os.Flags.allowPrivateProfile() && Flags.enablePrivateSpace()
+                && Flags.privateSpaceSysAppsSeparation()) {
+            return launcherApps.getPreInstalledSystemPackages(user);
+        } else {
+            return new ArrayList<>();
+        }
+    }
+
+    /**
+     * Returns an intent which can be used to start the App Market activity (Installer
+     * Activity).
+     */
+    public static Intent getAppMarketActivityIntent(Context context, String packageName,
+            UserHandle user) {
+        LauncherApps launcherApps = context.getSystemService(LauncherApps.class);
+        if (android.os.Flags.allowPrivateProfile()
+                && Flags.enablePrivateSpace()
+                && (Flags.privateSpaceAppInstallerButton()
+                        || Flags.enablePrivateSpaceInstallShortcut())) {
+            StartActivityParams params = new StartActivityParams((PendingIntent) null, 0);
+            params.intentSender = launcherApps.getAppMarketActivityIntent(packageName, user);
+            return ProxyActivityStarter.getLaunchIntent(context, params);
+        } else {
+            return new Intent(Intent.ACTION_VIEW)
+                    .setData(new Uri.Builder()
+                            .scheme("market")
+                            .authority("details")
+                            .appendQueryParameter("id", packageName)
+                            .build())
+                    .putExtra(Intent.EXTRA_REFERRER, new Uri.Builder().scheme("android-app")
+                            .authority(context.getPackageName()).build());
+        }
+    }
+
     private static class NoopDrawable extends ColorDrawable {
         @Override
         public int getIntrinsicHeight() {
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepAppWidgetHost.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepAppWidgetHost.java
index 6659fa0..45813ce 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepAppWidgetHost.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepAppWidgetHost.java
@@ -25,6 +25,7 @@
 import androidx.annotation.NonNull;
 
 import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.util.Executors;
 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.widget.LauncherWidgetHolder;
 
@@ -55,7 +56,10 @@
 
     @Override
     public void onAppWidgetRemoved(int appWidgetId) {
-        mAppWidgetRemovedCallback.accept(appWidgetId);
+        // Route the call via model thread, in case it comes while a loader-bind is in progress
+        Executors.MODEL_EXECUTOR.execute(
+                () -> Executors.MAIN_EXECUTOR.execute(
+                        () -> mAppWidgetRemovedCallback.accept(appWidgetId)));
     }
 
     @Override
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepInteractionHandler.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepInteractionHandler.java
index 8092582..d834935 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepInteractionHandler.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepInteractionHandler.java
@@ -16,6 +16,7 @@
 package com.android.launcher3.uioverrides;
 
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_APP_LAUNCH_TAP;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SPLIT_WIDGET_ATTEMPT;
 
 import android.app.ActivityOptions;
 import android.app.ActivityTaskManager;
@@ -59,6 +60,9 @@
                     remoteResponse.getLaunchOptions(view));
         }
         if (mLauncher.isSplitSelectionEnabled()) {
+            // Log metric
+            StatsLogManager.StatsLogger logger = mLauncher.getStatsLogManager().logger();
+            logger.log(LAUNCHER_SPLIT_WIDGET_ATTEMPT);
             Toast.makeText(hostView.getContext(), R.string.split_widgets_not_supported,
                     Toast.LENGTH_SHORT).show();
             return true;
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index b685d3c..f0ab08c 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -39,7 +39,9 @@
 import static com.android.launcher3.model.data.ItemInfo.NO_MATCHING_ID;
 import static com.android.launcher3.popup.QuickstepSystemShortcut.getSplitSelectShortcutByPosition;
 import static com.android.launcher3.popup.SystemShortcut.APP_INFO;
+import static com.android.launcher3.popup.SystemShortcut.DONT_SUGGEST_APP;
 import static com.android.launcher3.popup.SystemShortcut.INSTALL;
+import static com.android.launcher3.popup.SystemShortcut.PRIVATE_PROFILE_INSTALL;
 import static com.android.launcher3.popup.SystemShortcut.WIDGETS;
 import static com.android.launcher3.taskbar.LauncherTaskbarUIController.ALL_APPS_PAGE_PROGRESS_INDEX;
 import static com.android.launcher3.taskbar.LauncherTaskbarUIController.MINUS_ONE_PAGE_PROGRESS_INDEX;
@@ -84,6 +86,7 @@
 import android.window.BackEvent;
 import android.window.OnBackAnimationCallback;
 import android.window.OnBackInvokedDispatcher;
+import android.window.RemoteTransition;
 import android.window.SplashScreen;
 
 import androidx.annotation.BinderThread;
@@ -94,6 +97,7 @@
 import com.android.app.viewcapture.SettingsAwareViewCapture;
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Flags;
 import com.android.launcher3.HomeTransitionController;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherSettings.Favorites;
@@ -106,6 +110,7 @@
 import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.PendingAnimation;
+import com.android.launcher3.apppairs.AppPairIcon;
 import com.android.launcher3.appprediction.PredictionRowView;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.desktop.DesktopRecentsTransitionController;
@@ -116,7 +121,6 @@
 import com.android.launcher3.model.BgDataModel.FixedContainerItems;
 import com.android.launcher3.model.WellbeingModel;
 import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.popup.SystemShortcut;
 import com.android.launcher3.proxy.ProxyActivityStarter;
 import com.android.launcher3.statehandlers.DepthController;
@@ -266,8 +270,8 @@
         mAppTransitionManager.registerRemoteTransitions();
 
         if (FeatureFlags.enableHomeTransitionListener()) {
-            mHomeTransitionController = new HomeTransitionController(this);
-            mHomeTransitionController.registerHomeTransitionListener();
+            mHomeTransitionController = new HomeTransitionController();
+            mHomeTransitionController.registerHomeTransitionListener(this);
         }
 
         mTISBindHelper = new TISBindHelper(this, this::onTISConnected);
@@ -414,6 +418,12 @@
         shortcuts.addAll(getSplitShortcuts());
         shortcuts.add(WIDGETS);
         shortcuts.add(INSTALL);
+        if (Flags.enablePrivateSpaceInstallShortcut()) {
+            shortcuts.add(PRIVATE_PROFILE_INSTALL);
+        }
+        if (Flags.enableShortcutDontSuggestApp()) {
+            shortcuts.add(DONT_SUGGEST_APP);
+        }
         return shortcuts.stream();
     }
 
@@ -644,7 +654,7 @@
                 Collections.singletonList(splitSelectSource.itemInfo.getComponentKey()),
                 false /* findExactPairMatch */,
                 foundTasks -> {
-                    @Nullable Task foundTask = foundTasks.get(0);
+                    @Nullable Task foundTask = foundTasks[0];
                     boolean taskWasFound = foundTask != null;
                     splitSelectSource.alreadyRunningTaskId = taskWasFound
                             ? foundTask.key.id
@@ -707,6 +717,13 @@
     }
 
     @Override
+    public void onStateTransitionCompletedAfterSwipeToHome(LauncherState finalState) {
+        if (mTaskbarUIController != null) {
+            mTaskbarUIController.onStateTransitionCompletedAfterSwipeToHome(finalState);
+        }
+    }
+
+    @Override
     protected void onResume() {
         super.onResume();
 
@@ -1264,7 +1281,8 @@
     /**
      * Launches the given {@link GroupTask} in splitscreen.
      */
-    public void launchSplitTasks(@NonNull GroupTask groupTask) {
+    public void launchSplitTasks(
+            @NonNull GroupTask groupTask, @Nullable RemoteTransition remoteTransition) {
         // Top/left and bottom/right tasks respectively.
         Task task1 = groupTask.task1;
         // task2 should never be null when calling this method. Allow a crash to catch invalid calls
@@ -1278,14 +1296,15 @@
                 /* freezeTaskList= */ false,
                 groupTask.mSplitBounds == null
                         ? SNAP_TO_50_50
-                        : groupTask.mSplitBounds.snapPosition);
+                        : groupTask.mSplitBounds.snapPosition,
+                remoteTransition);
     }
 
     /**
      * Launches two apps as an app pair.
      */
-    public void launchAppPair(WorkspaceItemInfo app1, WorkspaceItemInfo app2) {
-        mSplitSelectStateController.getAppPairsController().launchAppPair(app1, app2);
+    public void launchAppPair(AppPairIcon appPairIcon) {
+        mSplitSelectStateController.getAppPairsController().launchAppPair(appPairIcon);
     }
 
     public boolean canStartHomeSafely() {
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepWidgetHolder.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepWidgetHolder.java
index f66bc60..0fb2b17 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepWidgetHolder.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepWidgetHolder.java
@@ -34,10 +34,10 @@
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.model.WidgetsModel;
 import com.android.launcher3.util.IntSet;
+import com.android.launcher3.util.SafeCloseable;
 import com.android.launcher3.widget.LauncherAppWidgetHostView;
 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.widget.LauncherWidgetHolder;
-import com.android.launcher3.widget.custom.CustomWidgetManager;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -67,13 +67,11 @@
 
     private static AppWidgetHost sWidgetHost = null;
 
-    private final SparseArray<AppWidgetHostView> mViews = new SparseArray<>();
-
+    private final UpdateHandler mUpdateHandler = this::onWidgetUpdate;
     private final @Nullable RemoteViews.InteractionHandler mInteractionHandler;
 
     private final @NonNull IntConsumer mAppWidgetRemovedCallback;
 
-    private final ArrayList<ProviderChangedListener> mProviderChangedListeners = new ArrayList<>();
     // Map to all pending updated keyed with appWidgetId;
     private final SparseArray<PendingUpdate> mPendingUpdateMap = new SparseArray<>();
 
@@ -112,16 +110,12 @@
 
     @Override
     protected void updateDeferredView() {
-        super.updateDeferredView();
         int count = mPendingUpdateMap.size();
         for (int i = 0; i < count; i++) {
             int widgetId = mPendingUpdateMap.keyAt(i);
             AppWidgetHostView view = mViews.get(widgetId);
-            if (view == null) {
-                continue;
-            }
             PendingUpdate pendingUpdate = mPendingUpdateMap.valueAt(i);
-            if (pendingUpdate == null) {
+            if (view == null || pendingUpdate == null) {
                 continue;
             }
             if (pendingUpdate.providerInfo != null) {
@@ -172,7 +166,6 @@
     @Override
     public void deleteAppWidgetId(int appWidgetId) {
         super.deleteAppWidgetId(appWidgetId);
-        mViews.remove(appWidgetId);
         sListeners.remove(appWidgetId);
     }
 
@@ -182,7 +175,10 @@
     @Override
     public void destroy() {
         try {
-            MAIN_EXECUTOR.submit(() -> sHolders.remove(this)).get();
+            MAIN_EXECUTOR.submit(() -> {
+                clearViews();
+                sHolders.remove(this);
+            }).get();
         } catch (Exception e) {
             Log.e(TAG, "Failed to remove self from holder list", e);
         }
@@ -195,26 +191,6 @@
     }
 
     /**
-     * Add a listener that is triggered when the providers of the widgets are changed
-     * @param listener The listener that notifies when the providers changed
-     */
-    @Override
-    public void addProviderChangeListener(
-            @NonNull LauncherWidgetHolder.ProviderChangedListener listener) {
-        MAIN_EXECUTOR.execute(() -> mProviderChangedListeners.add(listener));
-    }
-
-    /**
-     * Remove the specified listener from the host
-     * @param listener The listener that is to be removed from the host
-     */
-    @Override
-    public void removeProviderChangeListener(
-            LauncherWidgetHolder.ProviderChangedListener listener) {
-        MAIN_EXECUTOR.execute(() -> mProviderChangedListeners.remove(listener));
-    }
-
-    /**
      * Stop the host from updating the widget views
      */
     @Override
@@ -227,46 +203,41 @@
         setListeningFlag(false);
     }
 
-    /**
-     * Create a view for the specified app widget
-     * @param context The activity context for which the view is created
-     * @param appWidgetId The ID of the widget
-     * @param appWidget The {@link LauncherAppWidgetProviderInfo} of the widget
-     * @return A view for the widget
-     */
+    @Override
+    public SafeCloseable addOnUpdateListener(int appWidgetId,
+            LauncherAppWidgetProviderInfo appWidget, Runnable callback) {
+        UpdateHandler handler = new UpdateHandler() {
+            @Override
+            public <T> void onWidgetUpdate(int widgetId, UpdateKey<T> key, T data) {
+                if (KEY_VIEWS_UPDATE == key) {
+                    callback.run();
+                }
+            }
+        };
+        QuickstepWidgetHolderListener holderListener = getHolderListener(appWidgetId);
+        holderListener.addHolder(handler);
+        return () -> holderListener.mListeningHolders.remove(handler);
+    }
+
     @NonNull
     @Override
-    public LauncherAppWidgetHostView createView(@NonNull Context context, int appWidgetId,
-            @NonNull LauncherAppWidgetProviderInfo appWidget) {
-
-        if (appWidget.isCustomWidget()) {
-            LauncherAppWidgetHostView lahv = new LauncherAppWidgetHostView(context);
-            lahv.setAppWidget(appWidgetId, appWidget);
-            CustomWidgetManager.INSTANCE.get(context).onViewCreated(lahv);
-            return lahv;
-        }
-
-        LauncherAppWidgetHostView widgetView = getPendingView(appWidgetId);
-        if (widgetView != null) {
-            removePendingView(appWidgetId);
-        } else {
-            widgetView = new LauncherAppWidgetHostView(context);
-        }
-        widgetView.setIsWidgetCachingDisabled(true);
+    protected LauncherAppWidgetHostView createViewInternal(
+            int appWidgetId, @NonNull LauncherAppWidgetProviderInfo appWidget) {
+        LauncherAppWidgetHostView widgetView = new LauncherAppWidgetHostView(mContext);
         widgetView.setInteractionHandler(mInteractionHandler);
         widgetView.setAppWidget(appWidgetId, appWidget);
-        mViews.put(appWidgetId, widgetView);
+        widgetView.updateAppWidget(getHolderListener(appWidgetId).addHolder(mUpdateHandler));
+        return widgetView;
+    }
 
+    private static QuickstepWidgetHolderListener getHolderListener(int appWidgetId) {
         QuickstepWidgetHolderListener listener = sListeners.get(appWidgetId);
         if (listener == null) {
             listener = new QuickstepWidgetHolderListener(appWidgetId);
             sWidgetHost.setListener(appWidgetId, listener);
             sListeners.put(appWidgetId, listener);
         }
-        RemoteViews remoteViews = listener.addHolder(this);
-        widgetView.updateAppWidget(remoteViews);
-
-        return widgetView;
+        return listener;
     }
 
     /**
@@ -276,7 +247,7 @@
     public void clearViews() {
         mViews.clear();
         for (int i = sListeners.size() - 1; i >= 0; i--) {
-            sListeners.valueAt(i).mListeningHolders.remove(this);
+            sListeners.valueAt(i).mListeningHolders.remove(mUpdateHandler);
         }
     }
 
@@ -284,7 +255,7 @@
             implements AppWidgetHost.AppWidgetHostListener {
 
         // Static listeners should use a set that is backed by WeakHashMap to avoid memory leak
-        private final Set<QuickstepWidgetHolder> mListeningHolders = Collections.newSetFromMap(
+        private final Set<UpdateHandler> mListeningHolders = Collections.newSetFromMap(
                 new WeakHashMap<>());
 
         private final int mWidgetId;
@@ -297,7 +268,7 @@
 
         @UiThread
         @Nullable
-        public RemoteViews addHolder(@NonNull QuickstepWidgetHolder holder) {
+        public RemoteViews addHolder(@NonNull UpdateHandler holder) {
             mListeningHolders.add(holder);
             return mRemoteViews;
         }
@@ -368,11 +339,15 @@
         }
     }
 
+    private interface UpdateKey<T> extends BiConsumer<AppWidgetHostView, T> { }
+
+    private interface UpdateHandler {
+        <T> void onWidgetUpdate(int widgetId, UpdateKey<T> key, T data);
+    }
+
     private static class PendingUpdate {
         public final IntSet changedViews = new IntSet();
         public AppWidgetProviderInfo providerInfo;
         public RemoteViews remoteViews;
     }
-
-    private interface UpdateKey<T> extends BiConsumer<AppWidgetHostView, T> { }
 }
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/states/OverviewModalTaskState.java b/quickstep/src/com/android/launcher3/uioverrides/states/OverviewModalTaskState.java
index 1d55da3..856b519 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/states/OverviewModalTaskState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/OverviewModalTaskState.java
@@ -62,12 +62,6 @@
     @Override
     public void onBackPressed(Launcher launcher) {
         launcher.getStateManager().goToState(LauncherState.OVERVIEW);
-        RecentsView recentsView = launcher.<RecentsView>getOverviewPanel();
-        if (recentsView != null) {
-            recentsView.resetModalVisuals();
-        } else {
-            super.onBackPressed(launcher);
-        }
     }
 
     @Override
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java b/quickstep/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java
index 6651c73..0650f9d 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java
@@ -110,10 +110,13 @@
                         clampToProgress(LINEAR, 0, 0.33f));
             }
 
+            // We sync the scrim fade with the taskbar animation duration to avoid any flickers for
+            // taskbar icons disappearing before hotseat icons show up.
+            float scrimUpperBoundFromSplit = TASKBAR_TO_HOME_DURATION / (float) config.duration;
             config.setInterpolator(ANIM_OVERVIEW_ACTIONS_FADE, clampToProgress(LINEAR, 0, 0.25f));
             config.setInterpolator(ANIM_SCRIM_FADE,
                     fromState == OVERVIEW_SPLIT_SELECT
-                            ? clampToProgress(LINEAR, 0.33f, 1)
+                            ? clampToProgress(LINEAR, 0.33f, scrimUpperBoundFromSplit)
                             : LINEAR);
             config.setInterpolator(ANIM_WORKSPACE_SCALE, DECELERATE);
             config.setInterpolator(ANIM_WORKSPACE_FADE, ACCELERATE);
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/NoButtonQuickSwitchTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
index 968faf0..6d3b60a 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
@@ -16,6 +16,7 @@
 package com.android.launcher3.uioverrides.touchcontrollers;
 
 import static android.view.MotionEvent.ACTION_DOWN;
+
 import static com.android.app.animation.Interpolators.ACCELERATE_0_75;
 import static com.android.app.animation.Interpolators.DECELERATE_3;
 import static com.android.app.animation.Interpolators.LINEAR;
@@ -65,6 +66,7 @@
 import android.view.MotionEvent;
 import android.view.animation.Interpolator;
 
+import com.android.internal.jank.Cuj;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
@@ -190,8 +192,7 @@
     public void onDragStart(boolean start) {
         mMotionPauseDetector.clear();
         if (start) {
-            InteractionJankMonitorWrapper.begin(mRecentsView,
-                    InteractionJankMonitorWrapper.CUJ_QUICK_SWITCH);
+            InteractionJankMonitorWrapper.begin(mRecentsView, Cuj.CUJ_LAUNCHER_QUICK_SWITCH);
 
             mStartState = mLauncher.getStateManager().getState();
 
@@ -327,7 +328,7 @@
         if (mMotionPauseDetector.isPaused() && noFling) {
             // Going to Overview.
             cancelAnimations();
-            InteractionJankMonitorWrapper.cancel(InteractionJankMonitorWrapper.CUJ_QUICK_SWITCH);
+            InteractionJankMonitorWrapper.cancel(Cuj.CUJ_LAUNCHER_QUICK_SWITCH);
 
             StateAnimationConfig config = new StateAnimationConfig();
             config.duration = ATOMIC_DURATION_FROM_PAUSED_TO_OVERVIEW;
@@ -445,7 +446,7 @@
                     RecentsView.SCROLL_VIBRATION_PRIMITIVE_SCALE,
                     RecentsView.SCROLL_VIBRATION_FALLBACK);
         } else {
-            InteractionJankMonitorWrapper.cancel(InteractionJankMonitorWrapper.CUJ_QUICK_SWITCH);
+            InteractionJankMonitorWrapper.cancel(Cuj.CUJ_LAUNCHER_QUICK_SWITCH);
         }
 
         nonOverviewAnim.setDuration(Math.max(xDuration, yDuration));
@@ -469,7 +470,7 @@
                                         : LAUNCHER_UNKNOWN_SWIPEDOWN));
 
         if (targetState == QUICK_SWITCH_FROM_HOME) {
-            InteractionJankMonitorWrapper.end(InteractionJankMonitorWrapper.CUJ_QUICK_SWITCH);
+            InteractionJankMonitorWrapper.end(Cuj.CUJ_LAUNCHER_QUICK_SWITCH);
         }
 
         mLauncher.getStateManager().goToState(targetState, false, forEndCallback(this::clearState));
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
index 8cbf239..5db5d17 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;
@@ -25,6 +24,7 @@
 import android.view.MotionEvent;
 
 import com.android.app.animation.Interpolators;
+import com.android.internal.jank.Cuj;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
@@ -84,7 +84,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;
@@ -106,7 +106,7 @@
     protected StateAnimationConfig getConfigForStates(
             LauncherState fromState, LauncherState toState) {
         final StateAnimationConfig config = new StateAnimationConfig();
-        config.userControlled = true;
+        config.animProps |= StateAnimationConfig.USER_CONTROLLED;
         if (fromState == NORMAL && toState == ALL_APPS) {
             AllAppsSwipeController.applyNormalToAllAppsAnimConfig(mLauncher, config);
         } else if (fromState == ALL_APPS && toState == NORMAL) {
@@ -186,18 +186,15 @@
         switch (ev.getAction()) {
             case MotionEvent.ACTION_DOWN:
                 InteractionJankMonitorWrapper.begin(
-                        mLauncher.getRootView(), InteractionJankMonitorWrapper.CUJ_OPEN_ALL_APPS);
+                        mLauncher.getRootView(), Cuj.CUJ_LAUNCHER_OPEN_ALL_APPS);
                 InteractionJankMonitorWrapper.begin(
-                        mLauncher.getRootView(),
-                        InteractionJankMonitorWrapper.CUJ_CLOSE_ALL_APPS_SWIPE);
+                        mLauncher.getRootView(), Cuj.CUJ_LAUNCHER_CLOSE_ALL_APPS_SWIPE);
                 break;
 
             case MotionEvent.ACTION_CANCEL:
             case MotionEvent.ACTION_UP:
-                InteractionJankMonitorWrapper.cancel(
-                        InteractionJankMonitorWrapper.CUJ_OPEN_ALL_APPS);
-                InteractionJankMonitorWrapper.cancel(
-                        InteractionJankMonitorWrapper.CUJ_CLOSE_ALL_APPS_SWIPE);
+                InteractionJankMonitorWrapper.cancel(Cuj.CUJ_LAUNCHER_OPEN_ALL_APPS);
+                InteractionJankMonitorWrapper.cancel(Cuj.CUJ_LAUNCHER_CLOSE_ALL_APPS_SWIPE);
                 break;
         }
         return super.onControllerInterceptTouchEvent(ev);
@@ -208,11 +205,10 @@
     protected void onReinitToState(LauncherState newToState) {
         super.onReinitToState(newToState);
         if (newToState != ALL_APPS) {
-            InteractionJankMonitorWrapper.cancel(InteractionJankMonitorWrapper.CUJ_OPEN_ALL_APPS);
+            InteractionJankMonitorWrapper.cancel(Cuj.CUJ_LAUNCHER_OPEN_ALL_APPS);
         }
         if (newToState != NORMAL) {
-            InteractionJankMonitorWrapper.cancel(
-                    InteractionJankMonitorWrapper.CUJ_CLOSE_ALL_APPS_SWIPE);
+            InteractionJankMonitorWrapper.cancel(Cuj.CUJ_LAUNCHER_CLOSE_ALL_APPS_SWIPE);
         }
     }
 
@@ -220,18 +216,16 @@
     protected void onReachedFinalState(LauncherState toState) {
         super.onReachedFinalState(toState);
         if (toState == ALL_APPS) {
-            InteractionJankMonitorWrapper.end(InteractionJankMonitorWrapper.CUJ_OPEN_ALL_APPS);
+            InteractionJankMonitorWrapper.end(Cuj.CUJ_LAUNCHER_OPEN_ALL_APPS);
         } else if (toState == NORMAL) {
-            InteractionJankMonitorWrapper.end(
-                    InteractionJankMonitorWrapper.CUJ_CLOSE_ALL_APPS_SWIPE);
+            InteractionJankMonitorWrapper.end(Cuj.CUJ_LAUNCHER_CLOSE_ALL_APPS_SWIPE);
         }
     }
 
     @Override
     protected void clearState() {
         super.clearState();
-        InteractionJankMonitorWrapper.cancel(InteractionJankMonitorWrapper.CUJ_OPEN_ALL_APPS);
-        InteractionJankMonitorWrapper.cancel(
-                InteractionJankMonitorWrapper.CUJ_CLOSE_ALL_APPS_SWIPE);
+        InteractionJankMonitorWrapper.cancel(Cuj.CUJ_LAUNCHER_OPEN_ALL_APPS);
+        InteractionJankMonitorWrapper.cancel(Cuj.CUJ_LAUNCHER_CLOSE_ALL_APPS_SWIPE);
     }
 }
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/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index d7ff59e..cbf6ad6 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -96,6 +96,7 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.UiThread;
 
+import com.android.internal.jank.Cuj;
 import com.android.internal.util.LatencyTracker;
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.DeviceProfile;
@@ -1024,12 +1025,12 @@
                     }
                     mHandled = true;
 
+                    InteractionJankMonitorWrapper.begin(mRecentsView, Cuj.CUJ_LAUNCHER_QUICK_SWITCH,
+                            2000 /* ms timeout */);
                     InteractionJankMonitorWrapper.begin(mRecentsView,
-                            InteractionJankMonitorWrapper.CUJ_QUICK_SWITCH, 2000 /* ms timeout */);
+                            Cuj.CUJ_LAUNCHER_APP_CLOSE_TO_HOME);
                     InteractionJankMonitorWrapper.begin(mRecentsView,
-                            InteractionJankMonitorWrapper.CUJ_APP_CLOSE_TO_HOME);
-                    InteractionJankMonitorWrapper.begin(mRecentsView,
-                            InteractionJankMonitorWrapper.CUJ_APP_SWIPE_TO_RECENTS);
+                            Cuj.CUJ_LAUNCHER_APP_SWIPE_TO_RECENTS);
 
                     rv.post(() -> rv.getViewTreeObserver().removeOnDrawListener(this));
                 }
@@ -1145,16 +1146,13 @@
         View postResumeLastTask = mActivityInterface.onSettledOnEndTarget(endTarget);
 
         if (endTarget != NEW_TASK) {
-            InteractionJankMonitorWrapper.cancel(
-                    InteractionJankMonitorWrapper.CUJ_QUICK_SWITCH);
+            InteractionJankMonitorWrapper.cancel(Cuj.CUJ_LAUNCHER_QUICK_SWITCH);
         }
         if (endTarget != HOME) {
-            InteractionJankMonitorWrapper.cancel(
-                    InteractionJankMonitorWrapper.CUJ_APP_CLOSE_TO_HOME);
+            InteractionJankMonitorWrapper.cancel(Cuj.CUJ_LAUNCHER_APP_CLOSE_TO_HOME);
         }
         if (endTarget != RECENTS) {
-            InteractionJankMonitorWrapper.cancel(
-                    InteractionJankMonitorWrapper.CUJ_APP_SWIPE_TO_RECENTS);
+            InteractionJankMonitorWrapper.cancel(Cuj.CUJ_LAUNCHER_APP_SWIPE_TO_RECENTS);
         }
 
         switch (endTarget) {
@@ -1574,7 +1572,8 @@
                         mSwipePipToHomeAnimator.getTaskId(),
                         mSwipePipToHomeAnimator.getComponentName(),
                         mSwipePipToHomeAnimator.getDestinationBounds(),
-                        mSwipePipToHomeAnimator.getContentOverlay());
+                        mSwipePipToHomeAnimator.getContentOverlay(),
+                        mSwipePipToHomeAnimator.getAppBounds());
 
                 windowAnim = mSwipePipToHomeAnimators;
             } else {
diff --git a/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java b/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java
index 9e58160..877bdf8 100644
--- a/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java
+++ b/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java
@@ -28,6 +28,8 @@
 import android.animation.AnimatorListenerAdapter;
 import android.animation.AnimatorSet;
 import android.animation.ValueAnimator;
+import android.content.ComponentCallbacks;
+import android.content.res.Configuration;
 import android.graphics.Matrix;
 import android.graphics.PointF;
 import android.graphics.Rect;
@@ -52,11 +54,14 @@
 
 import com.android.internal.view.AppearanceRegion;
 import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.BubbleTextView;
+import com.android.launcher3.LauncherPrefs;
 import com.android.launcher3.QuickstepTransitionManager;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.taskbar.LauncherTaskbarUIController;
 import com.android.launcher3.uioverrides.QuickstepLauncher;
+import com.android.launcher3.widget.LauncherAppWidgetHostView;
 import com.android.quickstep.util.RectFSpringAnim;
 import com.android.systemui.shared.system.QuickStepContract;
 
@@ -96,43 +101,45 @@
     private final RectF mCurrentRect = new RectF();
     private final QuickstepLauncher mLauncher;
     private final int mWindowScaleMarginX;
-    /** Max window translation in the Y axis. */
-    private final int mWindowMaxDeltaY;
-    private final float mWindowScaleEndCornerRadius;
-    private final float mWindowScaleStartCornerRadius;
+    private float mWindowScaleEndCornerRadius;
+    private float mWindowScaleStartCornerRadius;
     private final Interpolator mCancelInterpolator;
     private final Interpolator mProgressInterpolator = new DecelerateInterpolator();
     private final PointF mInitialTouchPos = new PointF();
 
     private RemoteAnimationTarget mBackTarget;
-    private SurfaceControl.Transaction mTransaction = new SurfaceControl.Transaction();
+    private View mLauncherTargetView;
+    private final SurfaceControl.Transaction mTransaction = new SurfaceControl.Transaction();
     private boolean mSpringAnimationInProgress = false;
     private boolean mAnimatorSetInProgress = false;
     private float mBackProgress = 0;
     private boolean mBackInProgress = false;
     private OnBackInvokedCallbackStub mBackCallback;
     private IRemoteAnimationFinishedCallback mAnimationFinishedCallback;
-    private BackProgressAnimator mProgressAnimator = new BackProgressAnimator();
+    private final BackProgressAnimator mProgressAnimator = new BackProgressAnimator();
     private SurfaceControl mScrimLayer;
     private ValueAnimator mScrimAlphaAnimator;
     private float mScrimAlpha;
     private boolean mOverridingStatusBarFlags;
 
+    private final ComponentCallbacks mComponentCallbacks = new ComponentCallbacks() {
+        @Override
+        public void onConfigurationChanged(Configuration newConfig) {
+            loadCornerRadius();
+        }
+
+        @Override
+        public void onLowMemory() {}
+    };
+
     public LauncherBackAnimationController(
             QuickstepLauncher launcher,
             QuickstepTransitionManager quickstepTransitionManager) {
         mLauncher = launcher;
         mQuickstepTransitionManager = quickstepTransitionManager;
-        mWindowScaleEndCornerRadius = QuickStepContract.supportsRoundedCornersOnWindows(
-                mLauncher.getResources())
-                ? mLauncher.getResources().getDimensionPixelSize(
-                        R.dimen.swipe_back_window_corner_radius)
-                : 0;
-        mWindowScaleStartCornerRadius = QuickStepContract.getWindowCornerRadius(mLauncher);
+        loadCornerRadius();
         mWindowScaleMarginX = mLauncher.getResources().getDimensionPixelSize(
                 R.dimen.swipe_back_window_scale_x_margin);
-        mWindowMaxDeltaY = mLauncher.getResources().getDimensionPixelSize(
-                R.dimen.swipe_back_window_max_delta_y);
         mCancelInterpolator =
                 AnimationUtils.loadInterpolator(mLauncher, R.interpolator.standard_interpolator);
     }
@@ -284,7 +291,7 @@
         mBackInProgress = true;
         RemoteAnimationTarget appTarget = backEvent.getDepartingAnimationTarget();
 
-        if (appTarget == null) {
+        if (appTarget == null || appTarget.leash == null || !appTarget.leash.isValid()) {
             return;
         }
 
@@ -294,13 +301,27 @@
         mBackTarget = appTarget;
         mInitialTouchPos.set(backEvent.getTouchX(), backEvent.getTouchY());
 
-        // TODO(b/218916755): Offset start rectangle in multiwindow mode.
         mStartRect.set(appTarget.windowConfiguration.getMaxBounds());
+
+        // inset bottom in case of pinned taskbar being present
+        mStartRect.inset(0, 0, 0, appTarget.contentInsets.bottom);
+
+        mLauncherTargetView = mQuickstepTransitionManager.findLauncherView(
+                new RemoteAnimationTarget[]{ mBackTarget });
+        setLauncherTargetViewVisible(false);
         mCurrentRect.set(mStartRect);
         addScrimLayer();
         mTransaction.apply();
     }
 
+    private void setLauncherTargetViewVisible(boolean isVisible) {
+        if (mLauncherTargetView instanceof BubbleTextView) {
+            ((BubbleTextView) mLauncherTargetView).setIconVisible(isVisible);
+        } else if (mLauncherTargetView instanceof LauncherAppWidgetHostView) {
+            mLauncherTargetView.setAlpha(isVisible ? 1f : 0f);
+        }
+    }
+
     void addScrimLayer() {
         ViewRootImpl viewRootImpl = mLauncher.getDragLayer().getViewRootImpl();
         SurfaceControl parent = viewRootImpl != null
@@ -429,6 +450,8 @@
             mLauncher.getStateManager().moveToRestState();
         }
 
+        setLauncherTargetViewVisible(true);
+
         // Explicitly close opened floating views (which is typically called from
         // Launcher#onResumed, but in the predictive back flow launcher is not resumed until
         // the transition is fully finished.)
@@ -449,6 +472,7 @@
                     mBackInProgress /* fromPredictiveBack */);
         startTransitionAnimations(pair.first, pair.second);
         mLauncher.clearForceInvisibleFlag(INVISIBLE_ALL);
+        customizeStatusBarAppearance(true);
     }
 
     private void finishAnimation() {
@@ -462,6 +486,8 @@
         mInitialTouchPos.set(0, 0);
         mAnimatorSetInProgress = false;
         mSpringAnimationInProgress = false;
+        setLauncherTargetViewVisible(true);
+        mLauncherTargetView = null;
         // We don't call customizeStatusBarAppearance here to prevent the status bar update with
         // the legacy appearance. It should be refreshed after the transition done.
         mOverridingStatusBarFlags = false;
@@ -525,6 +551,30 @@
         anim.start();
     }
 
+    private void loadCornerRadius() {
+        mWindowScaleEndCornerRadius = QuickStepContract.supportsRoundedCornersOnWindows(
+                mLauncher.getResources())
+                ? mLauncher.getResources().getDimensionPixelSize(
+                R.dimen.swipe_back_window_corner_radius)
+                : 0;
+        mWindowScaleStartCornerRadius = QuickStepContract.getWindowCornerRadius(mLauncher);
+    }
+
+    /**
+     * Called when launcher is destroyed. Unregisters component callbacks to avoid memory leaks.
+     */
+    public void unregisterComponentCallbacks() {
+        mLauncher.unregisterComponentCallbacks(mComponentCallbacks);
+    }
+
+    /**
+     * Registers component callbacks with the launcher to receive configuration change events.
+     */
+    public void registerComponentCallbacks() {
+        mLauncher.registerComponentCallbacks(mComponentCallbacks);
+    }
+
+
     private void resetScrim() {
         removeScrimLayer();
         mScrimAlpha = 0;
diff --git a/quickstep/src/com/android/quickstep/LauncherRestoreEventLoggerImpl.kt b/quickstep/src/com/android/quickstep/LauncherRestoreEventLoggerImpl.kt
new file mode 100644
index 0000000..27bd03d
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/LauncherRestoreEventLoggerImpl.kt
@@ -0,0 +1,148 @@
+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.enableLauncherBrMetricsFixed
+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 restoreEventLogger: BackupRestoreEventLogger =
+        BackupManager(context).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 (enableLauncherBrMetricsFixed()) {
+            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 (enableLauncherBrMetricsFixed()) {
+            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 (enableLauncherBrMetricsFixed()) {
+            restoreEventLogger.logItemsRestored(favoritesIdToDataType(favoritesId), 1)
+        }
+    }
+
+    /**
+     * Helper to log successfully restoring multiple items from the Favorites table.
+     *
+     * @param favoritesId The id of the item type from [Favorites] that was restored.
+     * @param count number of items that restored.
+     */
+    override fun logFavoritesItemsRestored(favoritesId: Int, count: Int) {
+        if (enableLauncherBrMetricsFixed()) {
+            restoreEventLogger.logItemsRestored(favoritesIdToDataType(favoritesId), count)
+        }
+    }
+
+    /**
+     * 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 (enableLauncherBrMetricsFixed()) {
+            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 (enableLauncherBrMetricsFixed()) {
+            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 (enableLauncherBrMetricsFixed()) {
+            BackupManager(context).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/NavHandle.java b/quickstep/src/com/android/quickstep/NavHandle.java
new file mode 100644
index 0000000..da3311f
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/NavHandle.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep;
+
+import android.content.Context;
+
+import com.android.launcher3.R;
+
+/**
+ * Control and get information about the gesture nav bar at the bottom of the screen, which has
+ * historically been drawn by SysUI, but is also emulated by the stashed Taskbar on large screens.
+ */
+public interface NavHandle {
+
+    /**
+     * Animate the nav bar being long-pressed.
+     *
+     * @param isTouchDown {@code true} if the button is starting to be pressed ({@code false} if
+     *                                released or canceled)
+     * @param shrink {@code true} if the handle should shrink, {@code false} if it should grow
+     * @param durationMs how long the animation should take (for the {@code isTouchDown} case, this
+     *                   should be the same as the amount of time to trigger a long-press)
+     */
+    void animateNavBarLongPress(boolean isTouchDown, boolean shrink, long durationMs);
+
+    /** @return {@code true} if this nav handle is actually the stashed taskbar */
+    default boolean isNavHandleStashedTaskbar() {
+        return false;
+    }
+
+    /** @return {@code true} if this nav handle can currently accept long presses */
+    default boolean canNavHandleBeLongPressed() {
+        return true;
+    }
+
+    /** @return the width of this nav handle, in pixels */
+    default int getNavHandleWidth(Context context) {
+        return context.getResources().getDimensionPixelSize(R.dimen.navigation_home_handle_width);
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/OverviewCommandHelper.java b/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
index 31fe791..b2429ad 100644
--- a/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
+++ b/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
@@ -31,6 +31,7 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.UiThread;
 
+import com.android.internal.jank.Cuj;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.statemanager.StatefulActivity;
@@ -262,7 +263,7 @@
         if (activity != null) {
             InteractionJankMonitorWrapper.begin(
                     activity.getRootView(),
-                    InteractionJankMonitorWrapper.CUJ_QUICK_SWITCH);
+                    Cuj.CUJ_LAUNCHER_QUICK_SWITCH);
         }
 
         GestureState gestureState = mService.createGestureState(GestureState.DEFAULT_STATE,
diff --git a/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java b/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
index 0303791..65c8662 100644
--- a/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
+++ b/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
@@ -1,5 +1,6 @@
 package com.android.quickstep;
 
+import static com.android.launcher3.taskbar.TaskbarThresholdUtils.getFromNavThreshold;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 
 import android.app.Activity;
@@ -10,7 +11,6 @@
 
 import androidx.annotation.Nullable;
 
-import com.android.launcher3.R;
 import com.android.launcher3.taskbar.TaskbarActivityContext;
 import com.android.launcher3.testing.TestInformationHandler;
 import com.android.launcher3.testing.shared.TestProtocol;
@@ -18,6 +18,7 @@
 import com.android.launcher3.util.DisplayController;
 import com.android.quickstep.util.LayoutUtils;
 import com.android.quickstep.util.TISBindHelper;
+import com.android.quickstep.views.RecentsView;
 
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutionException;
@@ -77,6 +78,11 @@
                 return response;
             }
 
+            case TestProtocol.REQUEST_GET_OVERVIEW_CURRENT_PAGE_INDEX: {
+                return getLauncherUIProperty(Bundle::putInt,
+                        launcher -> launcher.<RecentsView>getOverviewPanel().getCurrentPage());
+            }
+
             case TestProtocol.REQUEST_HAS_TIS: {
                 response.putBoolean(TestProtocol.TEST_INFO_RESPONSE_FIELD, true);
                 return response;
@@ -93,7 +99,7 @@
             case TestProtocol.REQUEST_TASKBAR_FROM_NAV_THRESHOLD: {
                 final Resources resources = mContext.getResources();
                 response.putInt(TestProtocol.TEST_INFO_RESPONSE_FIELD,
-                        resources.getDimensionPixelSize(R.dimen.taskbar_from_nav_threshold));
+                        getFromNavThreshold(resources, mDeviceProfile));
                 return response;
             }
 
@@ -149,6 +155,19 @@
             case TestProtocol.REQUEST_REFRESH_OVERVIEW_TARGET:
                 runOnTISBinder(TouchInteractionService.TISBinder::refreshOverviewTarget);
                 return response;
+
+            case TestProtocol.REQUEST_RECREATE_TASKBAR:
+                // Allow null-pointer to catch illegal states.
+                runOnTISBinder(tisBinder -> tisBinder.getTaskbarManager().recreateTaskbar());
+                return response;
+
+            case TestProtocol.REQUEST_UNSTASH_BUBBLE_BAR_IF_STASHED:
+                runOnTISBinder(tisBinder -> {
+                    // Allow null-pointer to catch illegal states.
+                    tisBinder.getTaskbarManager().getCurrentActivityContext()
+                            .unstashBubbleBarIfStashed();
+                });
+                return response;
         }
 
         return super.call(method, arg, extras);
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationController.java b/quickstep/src/com/android/quickstep/RecentsAnimationController.java
index 341e18c..06a442b 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationController.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationController.java
@@ -25,14 +25,13 @@
 import android.os.RemoteException;
 import android.util.Log;
 import android.view.IRecentsAnimationController;
-import android.view.RemoteAnimationTarget;
 import android.view.SurfaceControl;
 import android.view.WindowManagerGlobal;
 import android.window.PictureInPictureSurfaceTransaction;
 
-import androidx.annotation.NonNull;
 import androidx.annotation.UiThread;
 
+import com.android.internal.jank.Cuj;
 import com.android.internal.os.IResultReceiver;
 import com.android.launcher3.util.Preconditions;
 import com.android.launcher3.util.RunnableList;
@@ -183,10 +182,9 @@
                     });
                 }
             });
-            InteractionJankMonitorWrapper.end(InteractionJankMonitorWrapper.CUJ_QUICK_SWITCH);
-            InteractionJankMonitorWrapper.end(InteractionJankMonitorWrapper.CUJ_APP_CLOSE_TO_HOME);
-            InteractionJankMonitorWrapper.end(
-                    InteractionJankMonitorWrapper.CUJ_APP_SWIPE_TO_RECENTS);
+            InteractionJankMonitorWrapper.end(Cuj.CUJ_LAUNCHER_QUICK_SWITCH);
+            InteractionJankMonitorWrapper.end(Cuj.CUJ_LAUNCHER_APP_CLOSE_TO_HOME);
+            InteractionJankMonitorWrapper.end(Cuj.CUJ_LAUNCHER_APP_SWIPE_TO_RECENTS);
         };
         if (forceFinish) {
             finishCb.run();
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java
index 27de20c..723af43 100644
--- a/quickstep/src/com/android/quickstep/SystemUiProxy.java
+++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java
@@ -17,7 +17,6 @@
 
 import static android.app.ActivityManager.RECENT_IGNORE_UNAVAILABLE;
 
-import static com.android.launcher3.testing.shared.TestProtocol.testLogD;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 import static com.android.launcher3.util.SplitConfigurationOptions.StagePosition;
@@ -102,11 +101,12 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.LinkedHashMap;
+import java.util.List;
 
 /**
  * Holds the reference to SystemUI.
  */
-public class SystemUiProxy implements ISystemUiProxy {
+public class SystemUiProxy implements ISystemUiProxy, NavHandle {
     private static final String TAG = SystemUiProxy.class.getSimpleName();
 
     public static final MainThreadInitializedObject<SystemUiProxy> INSTANCE =
@@ -147,6 +147,9 @@
     private IDesktopTaskListener mDesktopTaskListener;
     private final LinkedHashMap<RemoteTransition, TransitionFilter> mRemoteTransitions =
             new LinkedHashMap<>();
+
+    private final List<Runnable> mStateChangeCallbacks = new ArrayList<>();
+
     private IBinder mOriginalTransactionToken = null;
     private IOnBackInvokedCallback mBackToLauncherCallback;
     private IRemoteAnimationRunner mBackToLauncherRunner;
@@ -268,6 +271,7 @@
         setDesktopTaskListener(mDesktopTaskListener);
         setAssistantOverridesRequested(
                 AssistUtils.newInstance(mContext).getSysUiAssistOverrideInvocationTypes());
+        mStateChangeCallbacks.forEach(Runnable::run);
     }
 
     /**
@@ -278,6 +282,20 @@
         setProxy(null, null, null, null, null, null, null, null, null, null, null, null, null);
     }
 
+    /**
+     * Adds a callback to be notified whenever the active state changes
+     */
+    public void addOnStateChangeListener(Runnable callback) {
+        mStateChangeCallbacks.add(callback);
+    }
+
+    /**
+     * Removes a previously added state change callback
+     */
+    public void removeOnStateChangeListener(Runnable callback) {
+        mStateChangeCallbacks.remove(callback);
+    }
+
     // TODO(141886704): Find a way to remove this
     public void setLastSystemUiStateFlags(int stateFlags) {
         mLastSystemUiStateFlags = stateFlags;
@@ -612,10 +630,11 @@
      * should be responsible for cleaning up the overlay.
      */
     public void stopSwipePipToHome(int taskId, ComponentName componentName, Rect destinationBounds,
-            SurfaceControl overlay) {
+            SurfaceControl overlay, Rect appBounds) {
         if (mPip != null) {
             try {
-                mPip.stopSwipePipToHome(taskId, componentName, destinationBounds, overlay);
+                mPip.stopSwipePipToHome(taskId, componentName, destinationBounds, overlay,
+                        appBounds);
             } catch (RemoteException e) {
                 Log.w(TAG, "Failed call stopSwipePipToHome");
             }
@@ -1082,6 +1101,25 @@
     }
 
     /**
+     * Returns a surface which can be used to attach overlays to home task or null if
+     * the task doesn't exist or sysui is not connected
+     */
+    @Nullable
+    public SurfaceControl getHomeTaskOverlayContainer() {
+        // Use a local reference as this method can be called on a worker thread, which can lead
+        // to NullPointer exceptions if mShellTransitions is modified on the main thread.
+        IShellTransitions shellTransitions = mShellTransitions;
+        if (shellTransitions != null) {
+            try {
+                return mShellTransitions.getHomeTaskOverlayContainer();
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed call getOverlayContainerForTask", e);
+            }
+        }
+        return null;
+    }
+
+    /**
      * Use SystemUI's transaction-queue instead of Launcher's independent one. This is necessary
      * if Launcher and SystemUI need to coordinate transactions (eg. for shell transitions).
      */
diff --git a/quickstep/src/com/android/quickstep/TaskAnimationManager.java b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
index 8e03a91..4f885af 100644
--- a/quickstep/src/com/android/quickstep/TaskAnimationManager.java
+++ b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
@@ -38,6 +38,7 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.UiThread;
 
+import com.android.internal.util.ArrayUtils;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.util.DisplayController;
@@ -185,6 +186,16 @@
                 cleanUpRecentsAnimation(newCallbacks);
             }
 
+            private boolean isNonRecentsStartedTasksAppeared(
+                    RemoteAnimationTarget[] appearedTaskTargets) {
+                // For example, right after swiping from task X to task Y (e.g. from
+                // AbsSwipeUpHandler#startNewTask), and then task Y starts X immediately
+                // (e.g. in Y's onResume). The case will be: lastStartedTask=Y and appearedTask=X.
+                return mLastGestureState.getEndTarget() == GestureState.GestureEndTarget.NEW_TASK
+                        && ArrayUtils.find(appearedTaskTargets,
+                                mLastGestureState.mLastStartedTaskIdPredicate) == null;
+            }
+
             @Override
             public void onTasksAppeared(RemoteAnimationTarget[] appearedTaskTargets) {
                 RemoteAnimationTarget appearedTaskTarget = appearedTaskTargets[0];
@@ -206,13 +217,15 @@
                     }
                 }
 
-                RemoteAnimationTarget[] nonAppTargets = SystemUiProxy.INSTANCE.get(mCtx)
-                        .onStartingSplitLegacy(appearedTaskTargets);
+                RemoteAnimationTarget[] nonAppTargets = ENABLE_SHELL_TRANSITIONS
+                        ? null : SystemUiProxy.INSTANCE.get(mCtx).onStartingSplitLegacy(
+                                appearedTaskTargets);
                 if (nonAppTargets == null) {
                     nonAppTargets = new RemoteAnimationTarget[0];
                 }
                 if ((activityInterface.isInLiveTileMode()
-                            || mLastGestureState.getEndTarget() == RECENTS)
+                            || mLastGestureState.getEndTarget() == RECENTS
+                            || isNonRecentsStartedTasksAppeared(appearedTaskTargets))
                         && activityInterface.getCreatedActivity() != null) {
                     RecentsView recentsView =
                             activityInterface.getCreatedActivity().getOverviewPanel();
diff --git a/quickstep/src/com/android/quickstep/TaskViewUtils.java b/quickstep/src/com/android/quickstep/TaskViewUtils.java
index ddddc89..4e84f4a 100644
--- a/quickstep/src/com/android/quickstep/TaskViewUtils.java
+++ b/quickstep/src/com/android/quickstep/TaskViewUtils.java
@@ -18,8 +18,6 @@
 import static android.view.RemoteAnimationTarget.MODE_CLOSING;
 import static android.view.RemoteAnimationTarget.MODE_OPENING;
 import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER;
-import static android.view.WindowManager.TRANSIT_OPEN;
-import static android.view.WindowManager.TRANSIT_TO_FRONT;
 
 import static com.android.app.animation.Interpolators.LINEAR;
 import static com.android.app.animation.Interpolators.TOUCH_RESPONSE;
@@ -62,6 +60,7 @@
 import androidx.annotation.Nullable;
 
 import com.android.app.animation.Interpolators;
+import com.android.internal.jank.Cuj;
 import com.android.launcher3.BaseActivity;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.anim.AnimatedFloat;
@@ -397,8 +396,7 @@
             @Override
             public void onAnimationSuccess(Animator animator) {
                 if (isQuickSwitch) {
-                    InteractionJankMonitorWrapper.end(
-                            InteractionJankMonitorWrapper.CUJ_QUICK_SWITCH);
+                    InteractionJankMonitorWrapper.end(Cuj.CUJ_LAUNCHER_QUICK_SWITCH);
                 }
             }
 
@@ -421,106 +419,34 @@
      * Technically this case should be taken care of by
      * {@link #composeRecentsSplitLaunchAnimatorLegacy} below, but the way we launch tasks whether
      * it's a single task or multiple tasks results in different entry-points.
-     *
-     * If it is null, then it will simply fade in the starting apps and fade out launcher (for the
-     * case where launcher handles animating starting split tasks from app icon)
      */
     public static void composeRecentsSplitLaunchAnimator(GroupedTaskView launchingTaskView,
             @NonNull StateManager stateManager, @Nullable DepthController depthController,
-            int initialTaskId, int secondTaskId, @NonNull TransitionInfo transitionInfo,
-            SurfaceControl.Transaction t, @NonNull Runnable finishCallback) {
-        if (launchingTaskView != null) {
-            AnimatorSet animatorSet = new AnimatorSet();
-            animatorSet.addListener(new AnimatorListenerAdapter() {
-                @Override
-                public void onAnimationEnd(Animator animation) {
-                    finishCallback.run();
-                }
-            });
-
-            final RemoteAnimationTarget[] appTargets =
-                    RemoteAnimationTargetCompat.wrapApps(transitionInfo, t, null /* leashMap */);
-            final RemoteAnimationTarget[] wallpaperTargets =
-                    RemoteAnimationTargetCompat.wrapNonApps(
-                            transitionInfo, true /* wallpapers */, t, null /* leashMap */);
-            final RemoteAnimationTarget[] nonAppTargets =
-                    RemoteAnimationTargetCompat.wrapNonApps(
-                            transitionInfo, false /* wallpapers */, t, null /* leashMap */);
-            final RecentsView recentsView = launchingTaskView.getRecentsView();
-            composeRecentsLaunchAnimator(animatorSet, launchingTaskView,
-                    appTargets, wallpaperTargets, nonAppTargets,
-                    true, stateManager,
-                    recentsView, depthController);
-
-            t.apply();
-            animatorSet.start();
-            return;
-        }
-
-        TransitionInfo.Change splitRoot1 = null;
-        TransitionInfo.Change splitRoot2 = null;
-        final ArrayList<SurfaceControl> openingTargets = new ArrayList<>();
-        for (int i = 0; i < transitionInfo.getChanges().size(); ++i) {
-            final TransitionInfo.Change change = transitionInfo.getChanges().get(i);
-            if (change.getTaskInfo() == null) {
-                continue;
-            }
-            final int taskId = change.getTaskInfo().taskId;
-            final int mode = change.getMode();
-
-            // Find the target tasks' root tasks since those are the split stages that need to
-            // be animated (the tasks themselves are children and thus inherit animation).
-            if (taskId == initialTaskId || taskId == secondTaskId) {
-                if (!(mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT)) {
-                    throw new IllegalStateException(
-                            "Expected task to be showing, but it is " + mode);
-                }
-            }
-            if (taskId == initialTaskId) {
-                splitRoot1 = change.getParent() == null ? change :
-                        transitionInfo.getChange(change.getParent());
-                openingTargets.add(splitRoot1.getLeash());
-            }
-            if (taskId == secondTaskId) {
-                splitRoot2 = change.getParent() == null ? change :
-                        transitionInfo.getChange(change.getParent());
-                openingTargets.add(splitRoot2.getLeash());
-            }
-        }
-
-        SurfaceControl.Transaction animTransaction = new SurfaceControl.Transaction();
-        ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);
-        animator.setDuration(SPLIT_LAUNCH_DURATION);
-        animator.addUpdateListener(valueAnimator -> {
-            float progress = valueAnimator.getAnimatedFraction();
-            for (SurfaceControl leash: openingTargets) {
-                animTransaction.setAlpha(leash, progress);
-            }
-            animTransaction.apply();
-        });
-        animator.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationStart(Animator animation) {
-                for (SurfaceControl leash: openingTargets) {
-                    animTransaction.show(leash)
-                            .setAlpha(leash, 0.0f);
-                }
-                animTransaction.apply();
-            }
-
+            @NonNull TransitionInfo transitionInfo, SurfaceControl.Transaction t,
+            @NonNull Runnable finishCallback) {
+        AnimatorSet animatorSet = new AnimatorSet();
+        animatorSet.addListener(new AnimatorListenerAdapter() {
             @Override
             public void onAnimationEnd(Animator animation) {
                 finishCallback.run();
             }
         });
 
-        if (splitRoot1 != null && splitRoot1.getParent() != null) {
-            // Set the highest level split root alpha; we could technically use the parent of either
-            // splitRoot1 or splitRoot2
-            t.setAlpha(transitionInfo.getChange(splitRoot1.getParent()).getLeash(), 1f);
-        }
+        final RemoteAnimationTarget[] appTargets =
+                RemoteAnimationTargetCompat.wrapApps(transitionInfo, t, null /* leashMap */);
+        final RemoteAnimationTarget[] wallpaperTargets =
+                RemoteAnimationTargetCompat.wrapNonApps(
+                        transitionInfo, true /* wallpapers */, t, null /* leashMap */);
+        final RemoteAnimationTarget[] nonAppTargets =
+                RemoteAnimationTargetCompat.wrapNonApps(
+                        transitionInfo, false /* wallpapers */, t, null /* leashMap */);
+        final RecentsView recentsView = launchingTaskView.getRecentsView();
+        composeRecentsLaunchAnimator(animatorSet, launchingTaskView, appTargets, wallpaperTargets,
+                nonAppTargets, /* launcherClosing */ true, stateManager, recentsView,
+                depthController);
+
         t.apply();
-        animator.start();
+        animatorSet.start();
     }
 
     /**
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index bd4625b..647ff90 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -23,6 +23,7 @@
 import static android.view.MotionEvent.ACTION_POINTER_UP;
 import static android.view.MotionEvent.ACTION_UP;
 
+import static com.android.launcher3.Flags.enableCursorHoverStates;
 import static com.android.launcher3.Launcher.INTENT_ACTION_ALL_APPS_TOGGLE;
 import static com.android.launcher3.LauncherPrefs.backedUpItem;
 import static com.android.launcher3.MotionEventsUtils.isTrackpadMotionEvent;
@@ -86,7 +87,6 @@
 
 import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.ConstantItem;
-import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.EncryptionType;
 import com.android.launcher3.LauncherPrefs;
 import com.android.launcher3.R;
@@ -306,6 +306,10 @@
         @Override
         public void onNavigationBarSurface(SurfaceControl surface) {
             // TODO: implement
+            if (surface != null) {
+                surface.release();
+                surface = null;
+            }
         }
 
         @BinderThread
@@ -369,6 +373,12 @@
                     taskbarManager.onNavButtonsDarkIntensityChanged(darkIntensity));
         }
 
+        @Override
+        public void onNavigationBarLumaSamplingEnabled(int displayId, boolean enable) {
+            executeForTaskbarManager(taskbarManager ->
+                    taskbarManager.onNavigationBarLumaSamplingEnabled(displayId, enable));
+        }
+
         private void executeForTouchInteractionService(
                 @NonNull Consumer<TouchInteractionService> tisConsumer) {
             TouchInteractionService tis = mTis.get();
@@ -725,8 +735,8 @@
         final int action = event.getActionMasked();
         // Note this will create a new consumer every mouse click, as after ACTION_UP from the click
         // an ACTION_HOVER_ENTER will fire as well.
-        boolean isHoverActionWithoutConsumer =
-                event.isHoverEvent() && (mUncheckedConsumer.getType() & TYPE_CURSOR_HOVER) == 0;
+        boolean isHoverActionWithoutConsumer = enableCursorHoverStates()
+                && isHoverActionWithoutConsumer(event);
         CompoundString reasonString = action == ACTION_DOWN
                 ? new CompoundString("onMotionEvent: ") : CompoundString.NO_OP;
         if (action == ACTION_DOWN || isHoverActionWithoutConsumer) {
@@ -846,6 +856,15 @@
         traceToken.close();
     }
 
+    private boolean isHoverActionWithoutConsumer(MotionEvent event) {
+        // Only process these events when taskbar is present.
+        TaskbarActivityContext tac = mTaskbarManager.getCurrentActivityContext();
+        boolean isTaskbarPresent = tac != null && tac.getDeviceProfile().isTaskbarPresent
+                && !tac.isPhoneMode();
+        return event.isHoverEvent() && (mUncheckedConsumer.getType() & TYPE_CURSOR_HOVER) == 0
+                && isTaskbarPresent;
+    }
+
     // Talkback generates hover events on touch, which we do not want to consume.
     private boolean isCursorHoverEvent(MotionEvent event) {
         return event.isHoverEvent() && event.getSource() == InputDevice.SOURCE_MOUSE;
@@ -974,8 +993,8 @@
             TaskbarActivityContext tac = mTaskbarManager.getCurrentActivityContext();
             if (tac != null && !(base instanceof AssistantInputConsumer)) {
                 // Present always on large screen or on small screen w/ flag
-                DeviceProfile dp = tac.getDeviceProfile();
-                boolean useTaskbarConsumer = dp.isTaskbarPresent && !TaskbarManager.isPhoneMode(dp)
+                boolean useTaskbarConsumer = tac.getDeviceProfile().isTaskbarPresent
+                        && !tac.isPhoneMode()
                         && !tac.isInStashedLauncherState();
                 if (canStartSystemGesture && useTaskbarConsumer) {
                     reasonString.append(NEWLINE_PREFIX)
@@ -986,14 +1005,22 @@
                     base = new TaskbarUnstashInputConsumer(this, base, mInputMonitorCompat, tac,
                             mOverviewCommandHelper);
                 }
-            } else if (canStartSystemGesture && !previousGestureState.isRecentsAnimationRunning()) {
+            }
+
+            NavHandle navHandle = tac != null ? tac.getNavHandle()
+                    : SystemUiProxy.INSTANCE.get(this);
+            if (canStartSystemGesture && !previousGestureState.isRecentsAnimationRunning()
+                    && navHandle.canNavHandleBeLongPressed()) {
                 reasonString.append(NEWLINE_PREFIX)
                         .append(reasonPrefix)
                         .append(SUBSTRING_PREFIX)
-                        .append("Not running recents animation, ")
-                        .append("using NavHandleLongPressInputConsumer");
+                        .append("Not running recents animation, ");
+                if (tac != null && tac.getNavHandle().canNavHandleBeLongPressed()) {
+                    reasonString.append("stashed handle is long-pressable, ");
+                }
+                reasonString.append("using NavHandleLongPressInputConsumer");
                 base = new NavHandleLongPressInputConsumer(this, base, mInputMonitorCompat,
-                        mDeviceState);
+                        mDeviceState, navHandle);
             }
 
             if (mDeviceState.isBubblesExpanded()) {
diff --git a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
index 35fa539..1008da3 100644
--- a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
+++ b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
@@ -217,7 +217,6 @@
         } else {
             if (mActivity.isInState(RecentsState.MODAL_TASK)) {
                 mActivity.getStateManager().goToState(DEFAULT, animate);
-                resetModalVisuals();
             }
         }
     }
@@ -237,6 +236,8 @@
         setOverviewFullscreenEnabled(toState.isFullScreen());
         if (toState == MODAL_TASK) {
             setOverviewSelectEnabled(true);
+        } else {
+            resetModalVisuals();
         }
 
         // Set border after select mode changes to avoid showing border during state transition
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressHandler.java b/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressHandler.java
index 4d47f07..1d00e53 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressHandler.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressHandler.java
@@ -22,6 +22,7 @@
 
 import com.android.launcher3.R;
 import com.android.launcher3.util.ResourceBasedOverride;
+import com.android.quickstep.NavHandle;
 
 /**
  * Class for extending nav handle long press behavior
@@ -42,19 +43,26 @@
      * A Runnable is returned here to ensure the InputConsumer can call
      * {@link android.view.InputMonitor#pilferPointers()} before invoking the long press behavior
      * since pilfering can break the long press behavior.
+     *
+     * @param navHandle to handle this long press
      */
-    public @Nullable Runnable getLongPressRunnable() {
+    public @Nullable Runnable getLongPressRunnable(NavHandle navHandle) {
         return null;
     }
 
     /**
      * Called when nav handle gesture starts.
+     *
+     * @param navHandle to handle the animation for this touch
      */
-    public void onTouchStarted() {}
+    public void onTouchStarted(NavHandle navHandle) {}
 
     /**
      * Called when nav handle gesture is finished by the user lifting their finger or the system
      * cancelling the touch for some other reason.
+     *
+     * @param navHandle to handle the animation for this touch
+     * @param reason why the touch ended
      */
-    public void onTouchFinished(String reason) {}
+    public void onTouchFinished(NavHandle navHandle, String reason) {}
 }
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java
index 0a558e2..4c0b550 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java
@@ -17,7 +17,9 @@
 
 import static com.android.launcher3.LauncherPrefs.LONG_PRESS_NAV_HANDLE_TIMEOUT_MS;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_DEEP_PRESS_NAVBAR;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_DEEP_PRESS_STASHED_TASKBAR;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LONG_PRESS_NAVBAR;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LONG_PRESS_STASHED_TASKBAR;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 
 import android.content.Context;
@@ -25,11 +27,11 @@
 import android.view.ViewConfiguration;
 
 import com.android.launcher3.LauncherPrefs;
-import com.android.launcher3.R;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.util.DisplayController;
 import com.android.quickstep.InputConsumer;
+import com.android.quickstep.NavHandle;
 import com.android.quickstep.RecentsAnimationDeviceState;
 import com.android.quickstep.TopTaskTracker;
 import com.android.systemui.shared.system.InputMonitorCompat;
@@ -47,6 +49,7 @@
     private final float mTouchSlopSquared;
     private final int mLongPressTimeout;
     private final boolean mDeepPressEnabled;
+    private final NavHandle mNavHandle;
     private final StatsLogManager mStatsLogManager;
     private final TopTaskTracker mTopTaskTracker;
 
@@ -54,10 +57,9 @@
     private boolean mDeepPressLogged;  // Whether deep press has been logged for the current touch.
 
     public NavHandleLongPressInputConsumer(Context context, InputConsumer delegate,
-            InputMonitorCompat inputMonitor, RecentsAnimationDeviceState deviceState) {
+            InputMonitorCompat inputMonitor, RecentsAnimationDeviceState deviceState,
+            NavHandle navHandle) {
         super(delegate, inputMonitor);
-        mNavHandleWidth = context.getResources().getDimensionPixelSize(
-                R.dimen.navigation_home_handle_width);
         mScreenWidth = DisplayController.INSTANCE.get(context).getInfo().currentSize.x;
         mDeepPressEnabled = FeatureFlags.ENABLE_LPNH_DEEP_PRESS.get();
         if (FeatureFlags.CUSTOM_LPNH_THRESHOLDS.get()) {
@@ -66,6 +68,8 @@
             mLongPressTimeout = ViewConfiguration.getLongPressTimeout();
         }
         mTouchSlopSquared = deviceState.getSquaredTouchSlop();
+        mNavHandle = navHandle;
+        mNavHandleWidth = navHandle.getNavHandleWidth(context);
         mNavHandleLongPressHandler = NavHandleLongPressHandler.newInstance(context);
         mStatsLogManager = StatsLogManager.newInstance(context);
         mTopTaskTracker = TopTaskTracker.INSTANCE.get(context);
@@ -89,6 +93,11 @@
         }
     }
 
+    @Override
+    public void onHoverEvent(MotionEvent ev) {
+        mDelegate.onHoverEvent(ev);
+    }
+
     private void handleMotionEvent(MotionEvent ev) {
         switch (ev.getAction()) {
             case MotionEvent.ACTION_DOWN -> {
@@ -98,9 +107,8 @@
                 mCurrentDownEvent = MotionEvent.obtain(ev);
                 mDeepPressLogged = false;
                 if (isInNavBarHorizontalArea(ev.getRawX())) {
-                    mNavHandleLongPressHandler.onTouchStarted();
-                    MAIN_EXECUTOR.getHandler().postDelayed(mTriggerLongPress,
-                            mLongPressTimeout);
+                    mNavHandleLongPressHandler.onTouchStarted(mNavHandle);
+                    MAIN_EXECUTOR.getHandler().postDelayed(mTriggerLongPress, mLongPressTimeout);
                 }
             }
             case MotionEvent.ACTION_MOVE -> {
@@ -127,8 +135,9 @@
             // Log deep press even if feature is disabled.
             String runningPackage = mTopTaskTracker.getCachedTopTask(
                     /* filterOnlyVisibleRecents */ true).getPackageName();
-            mStatsLogManager.logger().withPackageName(runningPackage)
-                    .log(LAUNCHER_DEEP_PRESS_NAVBAR);
+            mStatsLogManager.logger().withPackageName(runningPackage).log(
+                    mNavHandle.isNavHandleStashedTaskbar() ? LAUNCHER_DEEP_PRESS_STASHED_TASKBAR
+                            : LAUNCHER_DEEP_PRESS_NAVBAR);
             mDeepPressLogged = true;
 
             // But only trigger if the feature is enabled.
@@ -142,9 +151,11 @@
     private void triggerLongPress() {
         String runningPackage = mTopTaskTracker.getCachedTopTask(
                 /* filterOnlyVisibleRecents */ true).getPackageName();
-        mStatsLogManager.logger().withPackageName(runningPackage).log(LAUNCHER_LONG_PRESS_NAVBAR);
+        mStatsLogManager.logger().withPackageName(runningPackage).log(
+                mNavHandle.isNavHandleStashedTaskbar() ? LAUNCHER_LONG_PRESS_STASHED_TASKBAR
+                        : LAUNCHER_LONG_PRESS_NAVBAR);
 
-        Runnable longPressRunnable = mNavHandleLongPressHandler.getLongPressRunnable();
+        Runnable longPressRunnable = mNavHandleLongPressHandler.getLongPressRunnable(mNavHandle);
         if (longPressRunnable == null) {
             return;
         }
@@ -161,7 +172,7 @@
 
     private void cancelLongPress(String reason) {
         MAIN_EXECUTOR.getHandler().removeCallbacks(mTriggerLongPress);
-        mNavHandleLongPressHandler.onTouchFinished(reason);
+        mNavHandleLongPressHandler.onTouchFinished(mNavHandle, reason);
     }
 
     private boolean isInNavBarHorizontalArea(float x) {
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/logging/StatsLogCompatManager.java b/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
index bce2e82..cf9fc74 100644
--- a/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
+++ b/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
@@ -42,6 +42,7 @@
 import androidx.annotation.WorkerThread;
 import androidx.slice.SliceItem;
 
+import com.android.internal.jank.Cuj;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.logger.LauncherAtom;
@@ -401,11 +402,10 @@
                 case LAUNCHER_ALLAPPS_VERTICAL_SWIPE_BEGIN:
                     InteractionJankMonitorWrapper.begin(
                             view,
-                            InteractionJankMonitorWrapper.CUJ_ALL_APPS_SCROLL);
+                            Cuj.CUJ_LAUNCHER_ALL_APPS_SCROLL);
                     break;
                 case LAUNCHER_ALLAPPS_VERTICAL_SWIPE_END:
-                    InteractionJankMonitorWrapper.end(
-                            InteractionJankMonitorWrapper.CUJ_ALL_APPS_SCROLL);
+                    InteractionJankMonitorWrapper.end(Cuj.CUJ_LAUNCHER_ALL_APPS_SCROLL);
                     break;
                 default:
                     break;
diff --git a/quickstep/src/com/android/quickstep/util/AnimUtils.java b/quickstep/src/com/android/quickstep/util/AnimUtils.java
index b7b7825..7fbbb6e 100644
--- a/quickstep/src/com/android/quickstep/util/AnimUtils.java
+++ b/quickstep/src/com/android/quickstep/util/AnimUtils.java
@@ -39,4 +39,13 @@
                 ? SplitAnimationTimings.TABLET_SPLIT_TO_CONFIRM
                 : SplitAnimationTimings.PHONE_SPLIT_TO_CONFIRM;
     }
+
+    /**
+     * Fetches device-specific timings for the app pair launch animation.
+     */
+    public static SplitAnimationTimings getDeviceAppPairLaunchTimings(boolean isTablet) {
+        return isTablet
+                ? SplitAnimationTimings.TABLET_APP_PAIR_LAUNCH
+                : SplitAnimationTimings.PHONE_APP_PAIR_LAUNCH;
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/util/AppPairLaunchTimings.kt b/quickstep/src/com/android/quickstep/util/AppPairLaunchTimings.kt
new file mode 100644
index 0000000..086c8af
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/AppPairLaunchTimings.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.util
+
+import com.android.app.animation.Interpolators
+
+/** Timings for the app pair launch animation. */
+abstract class AppPairLaunchTimings : SplitAnimationTimings {
+    protected abstract val STAGED_RECT_SLIDE_DURATION: Int
+
+    // Common timings that apply to app pair launches on any type of device
+    override fun getStagedRectSlideStart() = 0
+    override fun getStagedRectSlideEnd() = stagedRectSlideStart + STAGED_RECT_SLIDE_DURATION
+    override fun getPlaceholderFadeInStart() = 0
+    override fun getPlaceholderFadeInEnd() = 0
+    override fun getPlaceholderIconFadeInStart() = 0
+    override fun getPlaceholderIconFadeInEnd() = 0
+
+    private val iconFadeStart: Int
+        get() = getStagedRectSlideEnd()
+    private val iconFadeEnd: Int
+        get() = iconFadeStart + 83
+    private val appRevealStart: Int
+        get() = getStagedRectSlideEnd() + 67
+    private val appRevealEnd: Int
+        get() = appRevealStart + 217
+    private val cellSplitStart: Int
+        get() = (getStagedRectSlideEnd() * 0.83f).toInt()
+    private val cellSplitEnd: Int
+        get() = cellSplitStart + 500
+
+    override fun getStagedRectXInterpolator() = Interpolators.EMPHASIZED_COMPLEMENT
+    override fun getStagedRectYInterpolator() = Interpolators.EMPHASIZED
+    override fun getStagedRectScaleXInterpolator() = Interpolators.EMPHASIZED
+    override fun getStagedRectScaleYInterpolator() = Interpolators.EMPHASIZED
+    override fun getCellSplitInterpolator() = Interpolators.EMPHASIZED
+    override fun getIconFadeInterpolator() = Interpolators.LINEAR
+
+    override fun getCellSplitStartOffset(): Float {
+        return cellSplitStart.toFloat() / getDuration()
+    }
+    override fun getCellSplitEndOffset(): Float {
+        return cellSplitEnd.toFloat() / getDuration()
+    }
+    override fun getIconFadeStartOffset(): Float {
+        return iconFadeStart.toFloat() / getDuration()
+    }
+    override fun getIconFadeEndOffset(): Float {
+        return iconFadeEnd.toFloat() / getDuration()
+    }
+    override fun getAppRevealStartOffset(): Float {
+        return appRevealStart.toFloat() / getDuration()
+    }
+    override fun getAppRevealEndOffset(): Float {
+        return appRevealEnd.toFloat() / getDuration()
+    }
+    abstract override fun getDuration(): Int
+}
diff --git a/quickstep/src/com/android/quickstep/util/AppPairsController.java b/quickstep/src/com/android/quickstep/util/AppPairsController.java
index b6a8797..0f3c029 100644
--- a/quickstep/src/com/android/quickstep/util/AppPairsController.java
+++ b/quickstep/src/com/android/quickstep/util/AppPairsController.java
@@ -36,6 +36,7 @@
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.R;
 import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
+import com.android.launcher3.apppairs.AppPairIcon;
 import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.model.data.FolderInfo;
@@ -120,14 +121,16 @@
      * Launches an app pair by searching the RecentsModel for running instances of each app, and
      * staging either those running instances or launching the apps as new Intents.
      */
-    public void launchAppPair(WorkspaceItemInfo app1, WorkspaceItemInfo app2) {
+    public void launchAppPair(AppPairIcon appPairIcon) {
+        WorkspaceItemInfo app1 = appPairIcon.getInfo().contents.get(0);
+        WorkspaceItemInfo app2 = appPairIcon.getInfo().contents.get(1);
         ComponentKey app1Key = new ComponentKey(app1.getTargetComponent(), app1.user);
         ComponentKey app2Key = new ComponentKey(app2.getTargetComponent(), app2.user);
         mSplitSelectStateController.findLastActiveTasksAndRunCallback(
                 Arrays.asList(app1Key, app2Key),
                 false /* findExactPairMatch */,
                 foundTasks -> {
-                    @Nullable Task foundTask1 = foundTasks.get(0);
+                    @Nullable Task foundTask1 = foundTasks[0];
                     Intent task1Intent;
                     int task1Id;
                     if (foundTask1 != null) {
@@ -144,7 +147,7 @@
                             LAUNCHER_APP_PAIR_LAUNCH,
                             task1Id);
 
-                    @Nullable Task foundTask2 = foundTasks.get(1);
+                    @Nullable Task foundTask2 = foundTasks[1];
                     if (foundTask2 != null) {
                         mSplitSelectStateController.setSecondTask(foundTask2);
                     } else {
@@ -152,6 +155,8 @@
                                 app2.intent, app2.user);
                     }
 
+                    mSplitSelectStateController.setLaunchingIconView(appPairIcon);
+
                     mSplitSelectStateController.launchSplitTasks(
                             AppPairsController.convertRankToSnapPosition(app1.rank));
                 }
diff --git a/quickstep/src/com/android/quickstep/util/PhoneAppPairLaunchTimings.kt b/quickstep/src/com/android/quickstep/util/PhoneAppPairLaunchTimings.kt
new file mode 100644
index 0000000..beab90f
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/PhoneAppPairLaunchTimings.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.util
+
+/** Timings for the app pair launch animation on phones. */
+class PhoneAppPairLaunchTimings : AppPairLaunchTimings(), SplitAnimationTimings {
+    override val STAGED_RECT_SLIDE_DURATION = 500
+    override fun getDuration() = SplitAnimationTimings.PHONE_APP_PAIR_LAUNCH_DURATION
+}
diff --git a/quickstep/src/com/android/quickstep/util/SlideInRemoteTransition.kt b/quickstep/src/com/android/quickstep/util/SlideInRemoteTransition.kt
new file mode 100644
index 0000000..6544ba7
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/SlideInRemoteTransition.kt
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.util
+
+import android.animation.TimeInterpolator
+import android.animation.ValueAnimator
+import android.app.WindowConfiguration.ACTIVITY_TYPE_HOME
+import android.graphics.Rect
+import android.os.IBinder
+import android.os.RemoteException
+import android.view.SurfaceControl
+import android.view.SurfaceControl.Transaction
+import android.window.IRemoteTransition
+import android.window.IRemoteTransitionFinishedCallback
+import android.window.TransitionInfo
+import com.android.launcher3.anim.AnimatorListeners.forEndCallback
+import com.android.launcher3.util.Executors
+import com.android.wm.shell.util.TransitionUtil
+
+/** Remote animation which slides the opening targets in and the closing targets out */
+class SlideInRemoteTransition(
+    private val isRtl: Boolean,
+    private val pageSpacing: Int,
+    private val cornerRadius: Float,
+    private val interpolator: TimeInterpolator,
+) : IRemoteTransition.Stub() {
+    private val animationDurationMs = 500L
+
+    override fun mergeAnimation(
+        iBinder: IBinder,
+        transitionInfo: TransitionInfo,
+        transaction: Transaction,
+        mergeTarget: IBinder,
+        finishCB: IRemoteTransitionFinishedCallback
+    ) {
+
+        try {
+            finishCB.onTransitionFinished(null, Transaction())
+        } catch (e: RemoteException) {
+            // Ignore
+        }
+    }
+
+    override fun startAnimation(
+        transition: IBinder,
+        info: TransitionInfo,
+        startT: Transaction,
+        finishCB: IRemoteTransitionFinishedCallback
+    ) {
+        val anim = ValueAnimator.ofFloat(0f, 1f)
+        anim.interpolator = interpolator
+        anim.duration = animationDurationMs
+
+        val closingStartBounds: HashMap<SurfaceControl, Rect> = HashMap()
+        val openingEndBounds: HashMap<SurfaceControl, Rect> = HashMap()
+        for (chg in info.changes) {
+            val leash = chg.leash
+            startT.show(leash)
+
+            val taskInfo = chg.taskInfo
+            if (taskInfo?.activityType == ACTIVITY_TYPE_HOME || taskInfo?.parentTaskId != -1) {
+                continue
+            }
+            if (TransitionUtil.isClosingType(chg.mode)) {
+                closingStartBounds[leash] = chg.startAbsBounds
+                startT.setCrop(leash, chg.startAbsBounds).setCornerRadius(leash, cornerRadius)
+            }
+            if (TransitionUtil.isOpeningType(chg.mode)) {
+                openingEndBounds[leash] = chg.endAbsBounds
+                startT.setCrop(leash, chg.endAbsBounds).setCornerRadius(leash, cornerRadius)
+            }
+        }
+        startT.apply()
+
+        anim.addUpdateListener {
+            val t = Transaction()
+            closingStartBounds.keys.forEach {
+                // Translate the surface from its original position on-screen to off-screen on the
+                // right (or left in RTL)
+                val startBounds = closingStartBounds[it]
+                val targetX = (if (isRtl) -1 else 1) * (startBounds!!.right + pageSpacing)
+                t.setPosition(it, anim.animatedValue as Float * targetX, 0f)
+            }
+            openingEndBounds.keys.forEach {
+                // Set the alpha in the update listener to prevent one visible frame at the
+                // beginning
+                t.setAlpha(it, 1f)
+                // Translate the surface from off-screen on the left (or left in RTL) to its final
+                // position on-screen
+                val endBounds = openingEndBounds[it]
+                val targetX = (if (isRtl) -1 else 1) * (endBounds!!.right + pageSpacing)
+                t.setPosition(it, (1f - anim.animatedValue as Float) * -targetX, 0f)
+            }
+            t.apply()
+        }
+        anim.addListener(
+            forEndCallback(
+                Runnable {
+                    val t = Transaction()
+                    try {
+                        finishCB.onTransitionFinished(null, t)
+                    } catch (e: RemoteException) {
+                        // Ignore
+                    }
+                }
+            )
+        )
+
+        Executors.MAIN_EXECUTOR.execute { anim.start() }
+    }
+
+    override fun onTransitionConsumed(transition: IBinder?, aborted: Boolean) {}
+}
diff --git a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
index dfbd32c..ad9f5ea 100644
--- a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
+++ b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
@@ -21,20 +21,41 @@
 import android.animation.AnimatorListenerAdapter
 import android.animation.AnimatorSet
 import android.animation.ObjectAnimator
+import android.animation.ValueAnimator
+import android.app.ActivityManager.RunningTaskInfo
+import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW
 import android.graphics.Bitmap
 import android.graphics.Rect
 import android.graphics.RectF
 import android.graphics.drawable.Drawable
+import android.view.RemoteAnimationTarget
+import android.view.SurfaceControl
+import android.view.SurfaceControl.Transaction
 import android.view.View
+import android.view.WindowManager.TRANSIT_OPEN
+import android.view.WindowManager.TRANSIT_TO_FRONT
+import android.window.TransitionInfo
+import android.window.TransitionInfo.Change
+import android.window.WindowContainerToken
+import androidx.annotation.VisibleForTesting
 import com.android.app.animation.Interpolators
 import com.android.launcher3.DeviceProfile
+import com.android.launcher3.Launcher
+import com.android.launcher3.QuickstepTransitionManager
 import com.android.launcher3.Utilities
 import com.android.launcher3.anim.PendingAnimation
+import com.android.launcher3.apppairs.AppPairIcon
 import com.android.launcher3.config.FeatureFlags
+import com.android.launcher3.statehandlers.DepthController
+import com.android.launcher3.statemanager.StateManager
 import com.android.launcher3.statemanager.StatefulActivity
+import com.android.launcher3.taskbar.TaskbarActivityContext
 import com.android.launcher3.util.SplitConfigurationOptions.SplitSelectSource
 import com.android.launcher3.views.BaseDragLayer
+import com.android.quickstep.TaskViewUtils
+import com.android.quickstep.views.FloatingAppPairView
 import com.android.quickstep.views.FloatingTaskView
+import com.android.quickstep.views.GroupedTaskView
 import com.android.quickstep.views.RecentsView
 import com.android.quickstep.views.SplitInstructionsView
 import com.android.quickstep.views.TaskThumbnailView
@@ -308,6 +329,422 @@
         pendingAnimation.buildAnim().start()
     }
 
+    /**
+     * Called when launching a specific pair of apps, e.g. when tapping a pair of apps in Overview,
+     * or launching an app pair from its Home icon. Selects the appropriate launch animation and
+     * plays it.
+     */
+    fun playSplitLaunchAnimation(
+        launchingTaskView: GroupedTaskView?,
+        launchingIconView: AppPairIcon?,
+        initialTaskId: Int,
+        secondTaskId: Int,
+        apps: Array<RemoteAnimationTarget>?,
+        wallpapers: Array<RemoteAnimationTarget>?,
+        nonApps: Array<RemoteAnimationTarget>?,
+        stateManager: StateManager<*>,
+        depthController: DepthController?,
+        info: TransitionInfo?,
+        t: Transaction?,
+        finishCallback: Runnable
+    ) {
+        if (info == null && t == null) {
+            // (Legacy animation) Tapping a split tile in Overview
+            // TODO (b/315490678): Ensure that this works with app pairs flow
+            check(apps != null && wallpapers != null && nonApps != null) {
+                "trying to call composeRecentsSplitLaunchAnimatorLegacy, but encountered an " +
+                    "unexpected null"
+            }
+
+            composeRecentsSplitLaunchAnimatorLegacy(
+                launchingTaskView,
+                initialTaskId,
+                secondTaskId,
+                apps,
+                wallpapers,
+                nonApps,
+                stateManager,
+                depthController,
+                finishCallback
+            )
+
+            return
+        }
+
+        if (launchingTaskView != null) {
+            // Tapping a split tile in Overview
+            check(info != null && t != null) {
+                "trying to launch a GroupedTaskView, but encountered an unexpected null"
+            }
+
+            composeRecentsSplitLaunchAnimator(
+                launchingTaskView,
+                stateManager,
+                depthController,
+                info,
+                t,
+                finishCallback
+            )
+        } else if (launchingIconView != null) {
+            // Tapping an app pair icon
+            check(info != null && t != null) {
+                "trying to launch an app pair icon, but encountered an unexpected null"
+            }
+
+            // If launching an app pair from Taskbar inside of an app context, use fade-in animation
+            // TODO (b/316485863): Replace with desired app pair launch animation
+            if (launchingIconView.context is TaskbarActivityContext) {
+                composeFadeInSplitLaunchAnimator(
+                    initialTaskId, secondTaskId, info, t, finishCallback)
+                return
+            }
+
+            composeIconSplitLaunchAnimator(launchingIconView, info, t, finishCallback)
+        } else {
+            // Fallback case: simple fade-in animation
+            check(info != null && t != null) {
+                "trying to call composeFadeInSplitLaunchAnimator, but encountered an " +
+                    "unexpected null"
+            }
+
+            composeFadeInSplitLaunchAnimator(initialTaskId, secondTaskId, info, t, finishCallback)
+        }
+    }
+
+    /**
+     * When the user taps a split tile in Overview, this will play the tasks' launch animation from
+     * the position of the tapped tile.
+     */
+    @VisibleForTesting
+    fun composeRecentsSplitLaunchAnimator(
+        launchingTaskView: GroupedTaskView,
+        stateManager: StateManager<*>,
+        depthController: DepthController?,
+        info: TransitionInfo,
+        t: Transaction,
+        finishCallback: Runnable
+    ) {
+        TaskViewUtils.composeRecentsSplitLaunchAnimator(
+            launchingTaskView,
+            stateManager,
+            depthController,
+            info,
+            t,
+            finishCallback
+        )
+    }
+
+    /**
+     * LEGACY VERSION: When the user taps a split tile in Overview, this will play the tasks' launch
+     * animation from the position of the tapped tile.
+     */
+    @VisibleForTesting
+    fun composeRecentsSplitLaunchAnimatorLegacy(
+        launchingTaskView: GroupedTaskView?,
+        initialTaskId: Int,
+        secondTaskId: Int,
+        apps: Array<RemoteAnimationTarget>,
+        wallpapers: Array<RemoteAnimationTarget>,
+        nonApps: Array<RemoteAnimationTarget>,
+        stateManager: StateManager<*>,
+        depthController: DepthController?,
+        finishCallback: Runnable
+    ) {
+        TaskViewUtils.composeRecentsSplitLaunchAnimatorLegacy(
+            launchingTaskView,
+            initialTaskId,
+            secondTaskId,
+            apps,
+            wallpapers,
+            nonApps,
+            stateManager,
+            depthController,
+            finishCallback
+        )
+    }
+
+    /**
+     * When the user taps an app pair icon to launch split, this will play the tasks' launch
+     * animation from the position of the icon.
+     *
+     * To find the root shell leash that we want to fade in, we do the following:
+     * The Changes we receive in transitionInfo are structured like this
+     *
+     *     Root (grandparent)
+     *     |
+     *     |--> Split Root 1 (left/top side parent) (WINDOWING_MODE_MULTI_WINDOW)
+     *     |   |
+     *     |    --> App 1 (left/top side child) (WINDOWING_MODE_MULTI_WINDOW)
+     *     |--> Divider
+     *     |--> Split Root 2 (right/bottom side parent) (WINDOWING_MODE_MULTI_WINDOW)
+     *         |
+     *          --> App 2 (right/bottom side child) (WINDOWING_MODE_MULTI_WINDOW)
+     *
+     * We want to animate the Root (grandparent) so that it affects both apps and the divider.
+     * To do this, we find one of the nodes with WINDOWING_MODE_MULTI_WINDOW (one of the
+     * left-side ones, for simplicity) and traverse the tree until we find the grandparent.
+     */
+    @VisibleForTesting
+    fun composeIconSplitLaunchAnimator(
+        launchingIconView: AppPairIcon,
+        transitionInfo: TransitionInfo,
+        t: Transaction,
+        finishCallback: Runnable
+    ) {
+        val launcher = Launcher.getLauncher(launchingIconView.context)
+        val dp = launcher.deviceProfile
+
+        // Create an AnimatorSet that will run both shell and launcher transitions together
+        val launchAnimation = AnimatorSet()
+        val progressUpdater = ValueAnimator.ofFloat(0f, 1f)
+        val timings = AnimUtils.getDeviceAppPairLaunchTimings(dp.isTablet)
+        progressUpdater.setDuration(timings.getDuration().toLong())
+        progressUpdater.interpolator = Interpolators.LINEAR
+
+        var rootCandidate: Change? = null
+
+        for (change in transitionInfo.changes) {
+            val taskInfo: RunningTaskInfo = change.taskInfo ?: continue
+
+            // TODO (b/316490565): Replace this logic when SplitBounds is available to
+            //  startAnimation() and we can know the precise taskIds of launching tasks.
+            // Find a change that has WINDOWING_MODE_MULTI_WINDOW.
+            if (taskInfo.windowingMode == WINDOWING_MODE_MULTI_WINDOW &&
+                (change.mode == TRANSIT_OPEN || change.mode == TRANSIT_TO_FRONT)) {
+                // Check if it is a left/top app.
+                val isLeftTopApp =
+                    (dp.isLeftRightSplit && change.endAbsBounds.left == 0) ||
+                        (!dp.isLeftRightSplit && change.endAbsBounds.top == 0)
+                if (isLeftTopApp) {
+                    // Found one!
+                    rootCandidate = change
+                    break
+                }
+            }
+        }
+
+        // If we could not find a proper root candidate, something went wrong.
+        check(rootCandidate != null) { "Could not find a split root candidate" }
+
+        // Find the place where our left/top app window meets the divider (used for the
+        // launcher side animation)
+        val dividerPos =
+            if (dp.isLeftRightSplit) rootCandidate.endAbsBounds.right
+            else rootCandidate.endAbsBounds.bottom
+
+        // Recurse up the tree until parent is null, then we've found our root.
+        var parentToken: WindowContainerToken? = rootCandidate.parent
+        while (parentToken != null) {
+            rootCandidate = transitionInfo.getChange(parentToken) ?: break
+            parentToken = rootCandidate.parent
+        }
+
+        // Make sure nothing weird happened, like getChange() returning null.
+        check(rootCandidate != null) { "Failed to find a root leash" }
+
+        // Shell animation: the apps are revealed toward end of the launch animation
+        progressUpdater.addUpdateListener { valueAnimator: ValueAnimator ->
+            val progress =
+                Interpolators.clampToProgress(
+                    Interpolators.LINEAR,
+                    valueAnimator.animatedFraction,
+                    timings.appRevealStartOffset,
+                    timings.appRevealEndOffset
+                )
+
+            // Set the alpha of the shell layer (2 apps + divider)
+            t.setAlpha(rootCandidate.leash, progress)
+            t.apply()
+        }
+
+        // Create a new floating view in Launcher, positioned above the launching icon
+        val drawableArea = launchingIconView.iconDrawableArea
+        val appIcon1 = launchingIconView.info.contents[0].newIcon(launchingIconView.context)
+        val appIcon2 = launchingIconView.info.contents[1].newIcon(launchingIconView.context)
+        appIcon1.setBounds(0, 0, dp.iconSizePx, dp.iconSizePx)
+        appIcon2.setBounds(0, 0, dp.iconSizePx, dp.iconSizePx)
+        val floatingView =
+            FloatingAppPairView.getFloatingAppPairView(
+                launcher,
+                drawableArea,
+                appIcon1,
+                appIcon2,
+                dividerPos
+            )
+
+        // Launcher animation: animate the floating view, expanding to fill the display surface
+        progressUpdater.addUpdateListener(
+            object : MultiValueUpdateListener() {
+                var mDx =
+                    FloatProp(
+                        floatingView.startingPosition.left,
+                        dp.widthPx / 2f - floatingView.startingPosition.width() / 2f,
+                        0f /* delay */,
+                        timings.getDuration().toFloat(),
+                        Interpolators.clampToProgress(
+                            timings.getStagedRectXInterpolator(),
+                            timings.stagedRectSlideStartOffset,
+                            timings.stagedRectSlideEndOffset
+                        )
+                    )
+                var mDy =
+                    FloatProp(
+                        floatingView.startingPosition.top,
+                        dp.heightPx / 2f - floatingView.startingPosition.height() / 2f,
+                        0f /* delay */,
+                        timings.getDuration().toFloat(),
+                        Interpolators.clampToProgress(
+                            Interpolators.EMPHASIZED,
+                            timings.stagedRectSlideStartOffset,
+                            timings.stagedRectSlideEndOffset
+                        )
+                    )
+                var mScaleX =
+                    FloatProp(
+                        1f /* start */,
+                        dp.widthPx / floatingView.startingPosition.width(),
+                        0f /* delay */,
+                        timings.getDuration().toFloat(),
+                        Interpolators.clampToProgress(
+                            Interpolators.EMPHASIZED,
+                            timings.stagedRectSlideStartOffset,
+                            timings.stagedRectSlideEndOffset
+                        )
+                    )
+                var mScaleY =
+                    FloatProp(
+                        1f /* start */,
+                        dp.heightPx / floatingView.startingPosition.height(),
+                        0f /* delay */,
+                        timings.getDuration().toFloat(),
+                        Interpolators.clampToProgress(
+                            Interpolators.EMPHASIZED,
+                            timings.stagedRectSlideStartOffset,
+                            timings.stagedRectSlideEndOffset
+                        )
+                    )
+
+                override fun onUpdate(percent: Float, initOnly: Boolean) {
+                    floatingView.progress = percent
+                    floatingView.x = mDx.value
+                    floatingView.y = mDy.value
+                    floatingView.scaleX = mScaleX.value
+                    floatingView.scaleY = mScaleY.value
+                    floatingView.invalidate()
+                }
+            }
+        )
+
+        // When animation ends, remove the floating view and run finishCallback
+        progressUpdater.addListener(
+            object : AnimatorListenerAdapter() {
+                override fun onAnimationEnd(animation: Animator) {
+                    safeRemoveViewFromDragLayer(launcher, floatingView)
+                    finishCallback.run()
+                }
+            }
+        )
+
+        launchAnimation.play(progressUpdater)
+        launchAnimation.start()
+    }
+
+    /**
+     * If we are launching split screen without any special animation from a starting View, we
+     * simply fade in the starting apps and fade out launcher.
+     */
+    @VisibleForTesting
+    fun composeFadeInSplitLaunchAnimator(
+        initialTaskId: Int,
+        secondTaskId: Int,
+        transitionInfo: TransitionInfo,
+        t: Transaction,
+        finishCallback: Runnable
+    ) {
+        var splitRoot1: Change? = null
+        var splitRoot2: Change? = null
+        val openingTargets = ArrayList<SurfaceControl>()
+        for (change in transitionInfo.changes) {
+            val taskInfo: RunningTaskInfo = change.taskInfo ?: continue
+            val taskId = taskInfo.taskId
+            val mode = change.mode
+
+            // Find the target tasks' root tasks since those are the split stages that need to
+            // be animated (the tasks themselves are children and thus inherit animation).
+            if (taskId == initialTaskId || taskId == secondTaskId) {
+                check(mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT) {
+                    "Expected task to be showing, but it is $mode"
+                }
+            }
+
+            if (taskId == initialTaskId) {
+                splitRoot1 = change
+                val parentToken1 = change.parent
+                if (parentToken1 != null) {
+                    splitRoot1 = transitionInfo.getChange(parentToken1) ?: change
+                }
+
+                if (splitRoot1?.leash != null) {
+                    openingTargets.add(splitRoot1.leash)
+                }
+            }
+
+            if (taskId == secondTaskId) {
+                splitRoot2 = change
+                val parentToken2 = change.parent
+                if (parentToken2 != null) {
+                    splitRoot2 = transitionInfo.getChange(parentToken2) ?: change
+                }
+
+                if (splitRoot2?.leash != null) {
+                    openingTargets.add(splitRoot2.leash)
+                }
+            }
+        }
+
+        val animTransaction = Transaction()
+        val animator = ValueAnimator.ofFloat(0f, 1f)
+        animator.setDuration(QuickstepTransitionManager.SPLIT_LAUNCH_DURATION.toLong())
+        animator.addUpdateListener { valueAnimator: ValueAnimator ->
+            val progress = valueAnimator.animatedFraction
+            for (leash in openingTargets) {
+                animTransaction.setAlpha(leash, progress)
+            }
+            animTransaction.apply()
+        }
+
+        animator.addListener(
+            object : AnimatorListenerAdapter() {
+                override fun onAnimationStart(animation: Animator) {
+                    for (leash in openingTargets) {
+                        animTransaction.show(leash).setAlpha(leash, 0.0f)
+                    }
+                    animTransaction.apply()
+                }
+
+                override fun onAnimationEnd(animation: Animator) {
+                    finishCallback.run()
+                }
+            }
+        )
+
+        if (splitRoot1 != null) {
+            // Set the highest level split root alpha; we could technically use the parent of
+            // either splitRoot1 or splitRoot2
+            val parentToken = splitRoot1.parent
+            var rootLayer: Change? = null
+            if (parentToken != null) {
+                rootLayer = transitionInfo.getChange(parentToken)
+            }
+            if (rootLayer != null && rootLayer.leash != null) {
+                t.setAlpha(rootLayer.leash, 1f)
+            }
+        }
+
+        t.apply()
+        animator.start()
+    }
+
     private fun safeRemoveViewFromDragLayer(launcher: StatefulActivity<*>, view: View?) {
         if (view != null) {
             launcher.dragLayer.removeView(view)
diff --git a/quickstep/src/com/android/quickstep/util/SplitAnimationTimings.java b/quickstep/src/com/android/quickstep/util/SplitAnimationTimings.java
index 93f2255..b618546 100644
--- a/quickstep/src/com/android/quickstep/util/SplitAnimationTimings.java
+++ b/quickstep/src/com/android/quickstep/util/SplitAnimationTimings.java
@@ -21,31 +21,44 @@
 import android.view.animation.Interpolator;
 
 /**
- * An interface that supports the centralization of timing information for splitscreen animations.
+ * Organizes timing information for split screen animations.
  */
 public interface SplitAnimationTimings {
+    /** Total duration (ms) for initiating split screen (staging the first app) on tablets. */
     int TABLET_ENTER_DURATION = 866;
+    /** Total duration (ms) for confirming split screen (selecting the second app) on tablets. */
     int TABLET_CONFIRM_DURATION = 500;
-
+    /** Total duration (ms) for initiating split screen (staging the first app) on phones. */
     int PHONE_ENTER_DURATION = 517;
+    /** Total duration (ms) for confirming split screen (selecting the second app) on phones. */
     int PHONE_CONFIRM_DURATION = 333;
-
+    /** Total duration (ms) for aborting split screen (before selecting the second app). */
     int ABORT_DURATION = 500;
+    /** Total duration (ms) for launching an app pair from its icon on tablets. */
+    int TABLET_APP_PAIR_LAUNCH_DURATION = 998;
+    /** Total duration (ms) for launching an app pair from its icon on phones. */
+    int PHONE_APP_PAIR_LAUNCH_DURATION = 915;
 
+    // Initialize timing classes so they can be accessed statically
     SplitAnimationTimings TABLET_OVERVIEW_TO_SPLIT = new TabletOverviewToSplitTimings();
     SplitAnimationTimings TABLET_HOME_TO_SPLIT = new TabletHomeToSplitTimings();
     SplitAnimationTimings TABLET_SPLIT_TO_CONFIRM = new TabletSplitToConfirmTimings();
-
     SplitAnimationTimings PHONE_OVERVIEW_TO_SPLIT = new PhoneOverviewToSplitTimings();
     SplitAnimationTimings PHONE_SPLIT_TO_CONFIRM = new PhoneSplitToConfirmTimings();
+    SplitAnimationTimings TABLET_APP_PAIR_LAUNCH = new TabletAppPairLaunchTimings();
+    SplitAnimationTimings PHONE_APP_PAIR_LAUNCH = new PhoneAppPairLaunchTimings();
 
-    // Shared methods
+    // Shared methods: all split animations have these parameters
     int getDuration();
+    /** Start fading in the floating view tile at this time (in ms). */
     int getPlaceholderFadeInStart();
     int getPlaceholderFadeInEnd();
+    /** Start fading in the app icon at this time (in ms). */
     int getPlaceholderIconFadeInStart();
     int getPlaceholderIconFadeInEnd();
+    /** Start translating the floating view tile at this time (in ms). */
     int getStagedRectSlideStart();
+    /** The floating tile has reached its final position at this time (in ms). */
     int getStagedRectSlideEnd();
     Interpolator getStagedRectXInterpolator();
     Interpolator getStagedRectYInterpolator();
@@ -70,6 +83,11 @@
         return (float) getStagedRectSlideEnd() / getDuration();
     }
 
+    // DEFAULT VALUES: We define default values here so that SplitAnimationTimings can be used
+    // flexibly in animation-running functions, e.g. a single function that handles 2 types of split
+    // animations. The values are not intended to be used, and can safely be removed if refactoring
+    // these classes.
+
     // Defaults for OverviewToSplit
     default float getGridSlideStartOffset() { return 0; }
     default float getGridSlideStaggerOffset() { return 0; }
@@ -94,5 +112,13 @@
     // Defaults for SplitToConfirm
     default float getInstructionsFadeStartOffset() { return 0; }
     default float getInstructionsFadeEndOffset() { return 0; }
+
+    // Defaults for AppPair
+    default float getCellSplitStartOffset() { return 0; }
+    default float getCellSplitEndOffset() { return 0; }
+    default float getAppRevealStartOffset() { return 0; }
+    default float getAppRevealEndOffset() { return 0; }
+    default Interpolator getCellSplitInterpolator() { return LINEAR; }
+    default Interpolator getIconFadeInterpolator() { return LINEAR; }
 }
 
diff --git a/quickstep/src/com/android/quickstep/util/SplitScreenUtils.kt b/quickstep/src/com/android/quickstep/util/SplitScreenUtils.kt
index 596bb47..38bbe60 100644
--- a/quickstep/src/com/android/quickstep/util/SplitScreenUtils.kt
+++ b/quickstep/src/com/android/quickstep/util/SplitScreenUtils.kt
@@ -56,4 +56,4 @@
             }
         }
     }
-}
\ No newline at end of file
+}
diff --git a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
index 24d6d27..3c90e0c 100644
--- a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
+++ b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
@@ -71,6 +71,7 @@
 import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
 import com.android.launcher3.anim.PendingAnimation;
+import com.android.launcher3.apppairs.AppPairIcon;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.icons.IconProvider;
 import com.android.launcher3.logging.StatsLogManager;
@@ -92,7 +93,6 @@
 import com.android.quickstep.SplitSelectionListener;
 import com.android.quickstep.SystemUiProxy;
 import com.android.quickstep.TaskAnimationManager;
-import com.android.quickstep.TaskViewUtils;
 import com.android.quickstep.views.FloatingTaskView;
 import com.android.quickstep.views.GroupedTaskView;
 import com.android.quickstep.views.RecentsView;
@@ -105,7 +105,7 @@
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
-import java.util.Collections;
+import java.util.Arrays;
 import java.util.List;
 import java.util.function.Consumer;
 
@@ -141,6 +141,8 @@
     /** If not null, this is the TaskView we want to launch from */
     @Nullable
     private GroupedTaskView mLaunchingTaskView;
+    /** If not null, this is the icon we want to launch from */
+    private AppPairIcon mLaunchingIconView;
 
     /** True when the first selected split app is being launched in fullscreen. */
     private boolean mLaunchingFirstAppFullscreen;
@@ -223,14 +225,14 @@
      *                           tasks (i.e. searching for a running pair of tasks.)
      */
     public void findLastActiveTasksAndRunCallback(@Nullable List<ComponentKey> componentKeys,
-            boolean findExactPairMatch, Consumer<List<Task>> callback) {
+            boolean findExactPairMatch, Consumer<Task[]> callback) {
         mRecentTasksModel.getTasks(taskGroups -> {
             if (componentKeys == null || componentKeys.isEmpty()) {
-                callback.accept(Collections.emptyList());
+                callback.accept(new Task[]{});
                 return;
             }
 
-            List<Task> lastActiveTasks = new ArrayList<>();
+            Task[] lastActiveTasks = new Task[componentKeys.size()];
 
             if (findExactPairMatch) {
                 // Loop through tasks in reverse, since they are ordered with most-recent tasks last
@@ -238,32 +240,35 @@
                     GroupTask groupTask = taskGroups.get(i);
                     if (isInstanceOfAppPair(
                             groupTask, componentKeys.get(0), componentKeys.get(1))) {
-                        lastActiveTasks.add(groupTask.task1);
+                        lastActiveTasks[0] = groupTask.task1;
                         break;
                     }
                 }
             } else {
                 // For each key we are looking for, add to lastActiveTasks with the corresponding
                 // Task (or do nothing if not found).
-                for (ComponentKey key : componentKeys) {
+                for (int i = 0; i < componentKeys.size(); i++) {
+                    ComponentKey key = componentKeys.get(i);
                     Task lastActiveTask = null;
                     // Loop through tasks in reverse, since they are ordered with recent tasks last
-                    for (int i = taskGroups.size() - 1; i >= 0; i--) {
-                        GroupTask groupTask = taskGroups.get(i);
+                    for (int j = taskGroups.size() - 1; j >= 0; j--) {
+                        GroupTask groupTask = taskGroups.get(j);
                         Task task1 = groupTask.task1;
                         // Don't add duplicate Tasks
-                        if (isInstanceOfComponent(task1, key) && !lastActiveTasks.contains(task1)) {
+                        if (isInstanceOfComponent(task1, key)
+                                && !Arrays.asList(lastActiveTasks).contains(task1)) {
                             lastActiveTask = task1;
                             break;
                         }
                         Task task2 = groupTask.task2;
-                        if (isInstanceOfComponent(task2, key) && !lastActiveTasks.contains(task2)) {
+                        if (isInstanceOfComponent(task2, key)
+                                && !Arrays.asList(lastActiveTasks).contains(task2)) {
                             lastActiveTask = task2;
                             break;
                         }
                     }
 
-                    lastActiveTasks.add(lastActiveTask);
+                    lastActiveTasks[i] = lastActiveTask;
                 }
             }
 
@@ -493,6 +498,29 @@
     }
 
     /**
+     * Used to launch split screen from a split pair that already exists, optionally with a custom
+     * remote transition.
+     * <p>
+     * See {@link SplitSelectStateController#launchExistingSplitPair(
+     * GroupedTaskView, int, int, int, Consumer, boolean, int, RemoteTransition)}
+     */
+    public void launchExistingSplitPair(@Nullable GroupedTaskView groupedTaskView,
+            int firstTaskId, int secondTaskId, @StagePosition int stagePosition,
+            Consumer<Boolean> callback, boolean freezeTaskList,
+            @PersistentSnapPosition int snapPosition) {
+        launchExistingSplitPair(
+                groupedTaskView,
+                firstTaskId,
+                secondTaskId,
+                stagePosition,
+                callback,
+                freezeTaskList,
+                snapPosition,
+                /* remoteTransition= */ null);
+    }
+
+
+    /**
      * Used to launch split screen from a split pair that already exists (usually accessible through
      * Overview). This is different than {@link #launchTasks(Consumer, boolean, int, InstanceId)}
      * in that this only launches split screen that are existing tasks. This doesn't determine which
@@ -504,7 +532,7 @@
     public void launchExistingSplitPair(@Nullable GroupedTaskView groupedTaskView,
             int firstTaskId, int secondTaskId, @StagePosition int stagePosition,
             Consumer<Boolean> callback, boolean freezeTaskList,
-            @PersistentSnapPosition int snapPosition) {
+            @PersistentSnapPosition int snapPosition, @Nullable RemoteTransition remoteTransition) {
         mLaunchingTaskView = groupedTaskView;
         final ActivityOptions options1 = ActivityOptions.makeBasic();
         if (freezeTaskList) {
@@ -513,10 +541,12 @@
         Bundle optionsBundle = options1.toBundle();
 
         if (TaskAnimationManager.ENABLE_SHELL_TRANSITIONS) {
-            final RemoteTransition remoteTransition = getShellRemoteTransition(firstTaskId,
-                    secondTaskId, callback, "LaunchExistingPair");
+            final RemoteTransition transition = remoteTransition == null
+                    ? getShellRemoteTransition(
+                            firstTaskId, secondTaskId, callback, "LaunchExistingPair")
+                    : remoteTransition;
             mSystemUiProxy.startTasks(firstTaskId, optionsBundle, secondTaskId, null /* options2 */,
-                    stagePosition, snapPosition, remoteTransition, null /*shellInstanceId*/);
+                    stagePosition, snapPosition, transition, null /*shellInstanceId*/);
         } else {
             final RemoteAnimationAdapter adapter = getLegacyRemoteAdapter(firstTaskId,
                     secondTaskId, callback);
@@ -662,11 +692,21 @@
 
             MAIN_EXECUTOR.execute(() -> {
                 // Only animate from taskView if it's already visible
-                boolean shouldLaunchFromTaskView = mLaunchingTaskView != null &&
-                        mLaunchingTaskView.getRecentsView().isTaskViewVisible(mLaunchingTaskView);
-                TaskViewUtils.composeRecentsSplitLaunchAnimator(shouldLaunchFromTaskView
-                                ? mLaunchingTaskView : null, mStateManager,
-                        mDepthController, mInitialTaskId, mSecondTaskId, info, t, () -> {
+                boolean shouldLaunchFromTaskView = mLaunchingTaskView != null
+                        && mLaunchingTaskView.getRecentsView() != null
+                        && mLaunchingTaskView.getRecentsView().isTaskViewVisible(
+                        mLaunchingTaskView);
+                mSplitAnimationController.playSplitLaunchAnimation(
+                        shouldLaunchFromTaskView ? mLaunchingTaskView : null,
+                        mLaunchingIconView,
+                        mInitialTaskId,
+                        mSecondTaskId,
+                        null /* apps */,
+                        null /* wallpapers */,
+                        null /* nonApps */,
+                        mStateManager,
+                        mDepthController,
+                        info, t, () -> {
                             finishAdapter.run();
                             cleanup(true /*success*/);
                         });
@@ -722,9 +762,10 @@
                 RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps,
                 Runnable finishedCallback) {
             postAsyncCallback(mHandler,
-                    () -> TaskViewUtils.composeRecentsSplitLaunchAnimatorLegacy(
-                            mLaunchingTaskView, mInitialTaskId, mSecondTaskId, apps, wallpapers,
-                            nonApps, mStateManager, mDepthController, () -> {
+                    () -> mSplitAnimationController.playSplitLaunchAnimation(mLaunchingTaskView,
+                            mLaunchingIconView, mInitialTaskId, mSecondTaskId, apps, wallpapers,
+                            nonApps, mStateManager, mDepthController, null /* info */, null /* t */,
+                            () -> {
                                 finishedCallback.run();
                                 if (mSuccessCallback != null) {
                                     mSuccessCallback.accept(true);
@@ -757,6 +798,7 @@
         dispatchOnSplitSelectionExit();
         mRecentsAnimationRunning = false;
         mLaunchingTaskView = null;
+        mLaunchingIconView = null;
         mAnimateCurrentTaskDismissal = false;
         mDismissingFromSplitPair = false;
         mFirstFloatingTaskView = null;
@@ -817,6 +859,10 @@
         return mAppPairsController;
     }
 
+    public void setLaunchingIconView(AppPairIcon launchingIconView) {
+        mLaunchingIconView = launchingIconView;
+    }
+
     public BackPressHandler getSplitBackHandler() {
         return mSplitBackHandler;
     }
diff --git a/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java b/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java
index e705285..28efc97 100644
--- a/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java
+++ b/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java
@@ -34,6 +34,7 @@
 import android.os.UserHandle;
 import android.view.View;
 
+import com.android.internal.jank.Cuj;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherAppState;
@@ -174,8 +175,7 @@
             public void onAnimationEnd(Animator animation) {
                 if (!mIsCancelled) {
                     mController.launchSplitTasks(aBoolean -> cleanUp());
-                    InteractionJankMonitorWrapper.end(
-                            InteractionJankMonitorWrapper.CUJ_SPLIT_SCREEN_ENTER);
+                    InteractionJankMonitorWrapper.end(Cuj.CUJ_SPLIT_SCREEN_ENTER);
                 }
             }
 
diff --git a/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java b/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java
index cb32c6c..b663970 100644
--- a/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java
+++ b/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java
@@ -53,6 +53,7 @@
 
     private final QuickstepLauncher mLauncher;
     private final SplitSelectStateController mController;
+    private final RecentsAnimationDeviceState mDeviceState;
     private final OverviewComponentObserver mOverviewComponentObserver;
 
     private final int mSplitPlaceholderSize;
@@ -62,10 +63,9 @@
             SplitSelectStateController controller) {
         mLauncher = launcher;
         mController = controller;
-        RecentsAnimationDeviceState deviceState = new RecentsAnimationDeviceState(
-                launcher.getApplicationContext());
+        mDeviceState = new RecentsAnimationDeviceState(launcher.getApplicationContext());
         mOverviewComponentObserver = new OverviewComponentObserver(launcher.getApplicationContext(),
-                deviceState);
+                mDeviceState);
 
         mSplitPlaceholderSize = mLauncher.getResources().getDimensionPixelSize(
                 R.dimen.split_placeholder_size);
@@ -97,6 +97,7 @@
 
     public void onDestroy() {
         mOverviewComponentObserver.onDestroy();
+        mDeviceState.destroy();
     }
 
     private class SplitWithKeyboardShortcutRecentsAnimationListener implements
diff --git a/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java b/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java
index fe6ce46..6c89be1 100644
--- a/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java
+++ b/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java
@@ -16,8 +16,6 @@
 
 package com.android.quickstep.util;
 
-import static com.android.systemui.shared.system.InteractionJankMonitorWrapper.CUJ_APP_CLOSE_TO_PIP;
-
 import android.animation.Animator;
 import android.animation.RectEvaluator;
 import android.content.ComponentName;
@@ -35,6 +33,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
+import com.android.internal.jank.Cuj;
 import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.launcher3.icons.IconProvider;
 import com.android.quickstep.TaskAnimationManager;
@@ -174,19 +173,19 @@
         addAnimatorListener(new AnimationSuccessListener() {
             @Override
             public void onAnimationStart(Animator animation) {
-                InteractionJankMonitorWrapper.begin(view, CUJ_APP_CLOSE_TO_PIP);
+                InteractionJankMonitorWrapper.begin(view, Cuj.CUJ_LAUNCHER_APP_CLOSE_TO_PIP);
                 super.onAnimationStart(animation);
             }
 
             @Override
             public void onAnimationCancel(Animator animation) {
                 super.onAnimationCancel(animation);
-                InteractionJankMonitorWrapper.cancel(CUJ_APP_CLOSE_TO_PIP);
+                InteractionJankMonitorWrapper.cancel(Cuj.CUJ_LAUNCHER_APP_CLOSE_TO_PIP);
             }
 
             @Override
             public void onAnimationSuccess(Animator animator) {
-                InteractionJankMonitorWrapper.end(CUJ_APP_CLOSE_TO_PIP);
+                InteractionJankMonitorWrapper.end(Cuj.CUJ_LAUNCHER_APP_CLOSE_TO_PIP);
             }
 
             @Override
@@ -265,6 +264,10 @@
         return mDestinationBounds;
     }
 
+    public Rect getAppBounds() {
+        return mAppBounds;
+    }
+
     @Nullable
     public SurfaceControl getContentOverlay() {
         return mPipContentOverlay == null ? null : mPipContentOverlay.getLeash();
diff --git a/quickstep/src/com/android/quickstep/util/TabletAppPairLaunchTimings.kt b/quickstep/src/com/android/quickstep/util/TabletAppPairLaunchTimings.kt
new file mode 100644
index 0000000..fb2d63f
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/TabletAppPairLaunchTimings.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.util
+
+/** Timings for the app pair launch animation on tablets. */
+class TabletAppPairLaunchTimings : AppPairLaunchTimings(), SplitAnimationTimings {
+    override val STAGED_RECT_SLIDE_DURATION = 600
+    override fun getDuration() = SplitAnimationTimings.TABLET_APP_PAIR_LAUNCH_DURATION
+}
diff --git a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
index 5d8e53e..065a9c5 100644
--- a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
+++ b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
@@ -74,6 +74,7 @@
     private final boolean mIsRecentsRtl;
 
     private final Rect mTaskRect = new Rect();
+    private final Rect mFullTaskSize = new Rect();
     private final PointF mPivot = new PointF();
     private DeviceProfile mDp;
     @StagePosition
@@ -131,6 +132,47 @@
         mDp = dp;
         mLayoutValid = false;
         mOrientationState.setDeviceProfile(dp);
+        calculateTaskSize();
+    }
+
+    private void calculateTaskSize() {
+        if (mDp == null) {
+            return;
+        }
+
+        if (mIsGridTask) {
+            mSizeStrategy.calculateGridTaskSize(mContext, mDp, mFullTaskSize,
+                    mOrientationState.getOrientationHandler());
+        } else {
+            mSizeStrategy.calculateTaskSize(mContext, mDp, mFullTaskSize,
+                    mOrientationState.getOrientationHandler());
+        }
+
+        if (mSplitBounds != null) {
+            // The task rect changes according to the staged split task sizes, but recents
+            // fullscreen scale and pivot remains the same since the task fits into the existing
+            // sized task space bounds
+            mTaskRect.set(mFullTaskSize);
+            mOrientationState.getOrientationHandler()
+                    .setSplitTaskSwipeRect(mDp, mTaskRect, mSplitBounds, mStagePosition);
+            mTaskRect.offset(mTaskRectTranslationX, mTaskRectTranslationY);
+        } else if (mIsDesktopTask) {
+            // For desktop, tasks can take up only part of the screen size.
+            // Full task size represents the whole screen size, but scaled down to fit in recents.
+            // Task rect will represent the scaled down thumbnail position and is placed inside
+            // full task size as it is on the home screen.
+            PointF fullscreenTaskDimension = new PointF();
+            BaseActivityInterface.getTaskDimension(mContext, mDp, fullscreenTaskDimension);
+            // Calculate the scale down factor used in recents
+            float scale = mFullTaskSize.width() / fullscreenTaskDimension.x;
+            mTaskRect.set(mThumbnailPosition);
+            mTaskRect.scale(scale);
+            // Ensure the task rect is inside the full task rect
+            mTaskRect.offset(mFullTaskSize.left, mFullTaskSize.top);
+        } else {
+            mTaskRect.set(mFullTaskSize);
+            mTaskRect.offset(mTaskRectTranslationX, mTaskRectTranslationY);
+        }
     }
 
     /**
@@ -148,44 +190,11 @@
         if (mDp == null) {
             return 1;
         }
-
-        if (mIsGridTask) {
-            mSizeStrategy.calculateGridTaskSize(mContext, mDp, mTaskRect,
-                    mOrientationState.getOrientationHandler());
-        } else {
-            mSizeStrategy.calculateTaskSize(mContext, mDp, mTaskRect,
-                    mOrientationState.getOrientationHandler());
-        }
-
-        Rect fullTaskSize;
-        if (mSplitBounds != null) {
-            // The task rect changes according to the staged split task sizes, but recents
-            // fullscreen scale and pivot remains the same since the task fits into the existing
-            // sized task space bounds
-            fullTaskSize = new Rect(mTaskRect);
-            mOrientationState.getOrientationHandler()
-                    .setSplitTaskSwipeRect(mDp, mTaskRect, mSplitBounds, mStagePosition);
-            mTaskRect.offset(mTaskRectTranslationX, mTaskRectTranslationY);
-        } else if (mIsDesktopTask) {
-            // For desktop, tasks can take up only part of the screen size.
-            // Full task size represents the whole screen size, but scaled down to fit in recents.
-            // Task rect will represent the scaled down thumbnail position and is placed inside
-            // full task size as it is on the home screen.
-            fullTaskSize = new Rect(mTaskRect);
-            PointF fullscreenTaskDimension = new PointF();
-            BaseActivityInterface.getTaskDimension(mContext, mDp, fullscreenTaskDimension);
-            // Calculate the scale down factor used in recents
-            float scale = fullTaskSize.width() / fullscreenTaskDimension.x;
-            mTaskRect.set(mThumbnailPosition);
-            mTaskRect.scale(scale);
-            // Ensure the task rect is inside the full task rect
-            mTaskRect.offset(fullTaskSize.left, fullTaskSize.top);
-        } else {
-            fullTaskSize = new Rect(mTaskRect);
-            mTaskRect.offset(mTaskRectTranslationX, mTaskRectTranslationY);
-        }
-        fullTaskSize.offset(mTaskRectTranslationX + mPivotOffsetX, mTaskRectTranslationY);
-        return mOrientationState.getFullScreenScaleAndPivot(fullTaskSize, mDp, mPivot);
+        // Copy mFullTaskSize instead of updating it directly so it could be reused next time
+        // without recalculating
+        Rect scaleRect = new Rect(mFullTaskSize);
+        scaleRect.offset(mTaskRectTranslationX + mPivotOffsetX, mTaskRectTranslationY);
+        return mOrientationState.getFullScreenScaleAndPivot(scaleRect, mDp, mPivot);
     }
 
     /**
@@ -209,13 +218,13 @@
         mSplitBounds = splitInfo;
         if (mSplitBounds == null) {
             mStagePosition = STAGE_POSITION_UNDEFINED;
-            return;
+        } else {
+            mStagePosition = mThumbnailPosition.equals(splitInfo.leftTopBounds)
+                    ? STAGE_POSITION_TOP_OR_LEFT : STAGE_POSITION_BOTTOM_OR_RIGHT;
+            mPositionHelper.setSplitBounds(convertLauncherSplitBoundsToShell(mSplitBounds),
+                    mStagePosition);
         }
-        mStagePosition = mThumbnailPosition.equals(splitInfo.leftTopBounds) ?
-                STAGE_POSITION_TOP_OR_LEFT :
-                STAGE_POSITION_BOTTOM_OR_RIGHT;
-        mPositionHelper.setSplitBounds(convertLauncherSplitBoundsToShell(mSplitBounds),
-                mStagePosition);
+        calculateTaskSize();
     }
 
     /**
@@ -261,6 +270,8 @@
     public void setTaskRectTranslation(int taskRectTranslationX, int taskRectTranslationY) {
         mTaskRectTranslationX = taskRectTranslationX;
         mTaskRectTranslationY = taskRectTranslationY;
+        // Re-calculate task size after changing translation
+        calculateTaskSize();
     }
 
     /**
@@ -269,7 +280,7 @@
     public void addAppToOverviewAnim(PendingAnimation pa, TimeInterpolator interpolator) {
         pa.addFloat(fullScreenProgress, AnimatedFloat.VALUE, 1, 0, interpolator);
         if (enableGridOnlyOverview() && mDp.isTablet) {
-            int translationXToMiddle = mDp.widthPx / 2 - mTaskRect.centerX();
+            int translationXToMiddle = mDp.widthPx / 2 - mFullTaskSize.centerX();
             taskPrimaryTranslation.value = translationXToMiddle;
             mPivotOffsetX = translationXToMiddle;
         }
@@ -324,8 +335,8 @@
     public void applyWindowToHomeRotation(Matrix matrix) {
         matrix.postTranslate(mDp.windowX, mDp.windowY);
         postDisplayRotation(deltaRotation(
-                mOrientationState.getRecentsActivityRotation(),
-                mOrientationState.getDisplayRotation()),
+                        mOrientationState.getRecentsActivityRotation(),
+                        mOrientationState.getDisplayRotation()),
                 mDp.widthPx, mDp.heightPx, matrix);
     }
 
@@ -427,9 +438,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/util/unfold/LauncherJankMonitorTransitionProgressListener.kt b/quickstep/src/com/android/quickstep/util/unfold/LauncherJankMonitorTransitionProgressListener.kt
index 4f89c7e..b4ca35e 100644
--- a/quickstep/src/com/android/quickstep/util/unfold/LauncherJankMonitorTransitionProgressListener.kt
+++ b/quickstep/src/com/android/quickstep/util/unfold/LauncherJankMonitorTransitionProgressListener.kt
@@ -16,6 +16,7 @@
 package com.android.quickstep.util.unfold
 
 import android.view.View
+import com.android.internal.jank.Cuj
 import com.android.systemui.shared.system.InteractionJankMonitorWrapper
 import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
 import java.util.function.Supplier
@@ -28,11 +29,11 @@
     override fun onTransitionStarted() {
         InteractionJankMonitorWrapper.begin(
             attachedViewProvider.get(),
-            InteractionJankMonitorWrapper.CUJ_LAUNCHER_UNFOLD_ANIM
+            Cuj.CUJ_LAUNCHER_UNFOLD_ANIM
         )
     }
 
     override fun onTransitionFinished() {
-        InteractionJankMonitorWrapper.end(InteractionJankMonitorWrapper.CUJ_LAUNCHER_UNFOLD_ANIM)
+        InteractionJankMonitorWrapper.end(Cuj.CUJ_LAUNCHER_UNFOLD_ANIM)
     }
 }
diff --git a/quickstep/src/com/android/quickstep/views/DesktopTaskView.java b/quickstep/src/com/android/quickstep/views/DesktopTaskView.java
index a10d2ed..6b00473 100644
--- a/quickstep/src/com/android/quickstep/views/DesktopTaskView.java
+++ b/quickstep/src/com/android/quickstep/views/DesktopTaskView.java
@@ -17,6 +17,7 @@
 package com.android.quickstep.views;
 
 import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
+
 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_UNDEFINED;
 
 import android.content.Context;
@@ -53,6 +54,8 @@
 import com.android.systemui.shared.system.QuickStepContract;
 import com.android.wm.shell.Flags;
 
+import kotlin.Unit;
+
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -60,7 +63,6 @@
 import java.util.List;
 import java.util.function.Consumer;
 
-import kotlin.Unit;
 
 /**
  * TaskView that contains all tasks that are part of the desktop.
@@ -91,6 +93,8 @@
 
     private View mBackgroundView;
 
+    private int mChildCountAtInflation;
+
     /** Check whether desktop windowing is enabled */
     public static boolean isDesktopModeSupported() {
         // Check for aconfig flag first
@@ -151,6 +155,8 @@
         Drawable iconBackground = getResources().getDrawable(R.drawable.bg_circle,
                 getContext().getTheme());
         mIconView.setDrawable(new LayerDrawable(new Drawable[]{iconBackground, icon}));
+
+        mChildCountAtInflation = getChildCount();
     }
 
     @Override
@@ -196,7 +202,9 @@
             for (int i = 0; i < diff; i++) {
                 TaskThumbnailView snapshotView = new TaskThumbnailView(getContext());
                 mSnapshotViews.add(snapshotView);
-                addView(snapshotView, new LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
+                // Add snapshots from to position after the initial child views.
+                addView(snapshotView, mChildCountAtInflation,
+                        new LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
             }
         }
 
diff --git a/quickstep/src/com/android/quickstep/views/FloatingAppPairBackground.kt b/quickstep/src/com/android/quickstep/views/FloatingAppPairBackground.kt
new file mode 100644
index 0000000..3a5873b
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/FloatingAppPairBackground.kt
@@ -0,0 +1,358 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.views
+
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.ColorFilter
+import android.graphics.Paint
+import android.graphics.PixelFormat
+import android.graphics.RectF
+import android.graphics.drawable.Drawable
+import android.os.Build
+import android.view.animation.Interpolator
+import com.android.app.animation.Interpolators
+import com.android.launcher3.Launcher
+import com.android.launcher3.R
+import com.android.launcher3.Utilities
+import com.android.quickstep.util.AnimUtils
+import com.android.systemui.shared.system.QuickStepContract
+
+/**
+ * A Drawable that is drawn onto [FloatingAppPairView] every frame during the app pair launch
+ * animation. Consists of a rectangular background that splits into two, and two app icons that
+ * increase in size during the animation.
+ */
+class FloatingAppPairBackground(
+    context: Context,
+    private val floatingView: FloatingAppPairView, // the view that we will draw this background on
+    private val appIcon1: Drawable,
+    private val appIcon2: Drawable,
+    dividerPos: Int
+) : Drawable() {
+    companion object {
+        // Design specs -- app icons start small and expand during the animation
+        private val STARTING_ICON_SIZE_PX = Utilities.dpToPx(22f)
+        private val ENDING_ICON_SIZE_PX = Utilities.dpToPx(66f)
+
+        // Null values to use with drawDoubleRoundRect(), since there doesn't seem to be any other
+        // API for drawing rectangles with 4 different corner radii.
+        private val EMPTY_RECT = RectF()
+        private val ARRAY_OF_ZEROES = FloatArray(8)
+    }
+
+    private val launcher: Launcher
+    private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG)
+
+    // Animation interpolators
+    private val expandXInterpolator: Interpolator
+    private val expandYInterpolator: Interpolator
+    private val cellSplitInterpolator: Interpolator
+    private val iconFadeInterpolator: Interpolator
+
+    // Device-specific measurements
+    private val deviceCornerRadius: Float
+    private val deviceHalfDividerSize: Float
+    private val desiredSplitRatio: Float
+
+    init {
+        launcher = Launcher.getLauncher(context)
+        val dp = launcher.deviceProfile
+        // Set up background paint color
+        val ta = context.theme.obtainStyledAttributes(R.styleable.FolderIconPreview)
+        backgroundPaint.style = Paint.Style.FILL
+        backgroundPaint.color = ta.getColor(R.styleable.FolderIconPreview_folderPreviewColor, 0)
+        ta.recycle()
+        // Set up timings and interpolators
+        val timings = AnimUtils.getDeviceAppPairLaunchTimings(launcher.deviceProfile.isTablet)
+        expandXInterpolator =
+            Interpolators.clampToProgress(
+                timings.getStagedRectScaleXInterpolator(),
+                timings.stagedRectSlideStartOffset,
+                timings.stagedRectSlideEndOffset
+            )
+        expandYInterpolator =
+            Interpolators.clampToProgress(
+                timings.getStagedRectScaleYInterpolator(),
+                timings.stagedRectSlideStartOffset,
+                timings.stagedRectSlideEndOffset
+            )
+        cellSplitInterpolator =
+            Interpolators.clampToProgress(
+                timings.cellSplitInterpolator,
+                timings.cellSplitStartOffset,
+                timings.cellSplitEndOffset
+            )
+        iconFadeInterpolator =
+            Interpolators.clampToProgress(
+                timings.iconFadeInterpolator,
+                timings.iconFadeStartOffset,
+                timings.iconFadeEndOffset
+            )
+
+        // Find device-specific measurements
+        deviceCornerRadius = QuickStepContract.getWindowCornerRadius(launcher)
+        deviceHalfDividerSize =
+            launcher.resources.getDimensionPixelSize(R.dimen.multi_window_task_divider_size) / 2f
+        val dividerCenterPos = dividerPos + deviceHalfDividerSize
+        desiredSplitRatio =
+            if (dp.isLeftRightSplit) dividerCenterPos / dp.widthPx
+            else dividerCenterPos / dp.heightPx
+    }
+
+    override fun draw(canvas: Canvas) {
+        if (launcher.deviceProfile.isLandscape) {
+            drawLeftRightSplit(canvas)
+        } else {
+            drawTopBottomSplit(canvas)
+        }
+    }
+
+    /** When device is in landscape, we draw the rectangles with a left-right split. */
+    private fun drawLeftRightSplit(canvas: Canvas) {
+        val progress = floatingView.progress
+
+        // Since the entire floating app pair surface is scaling up during this animation, we
+        // scale down most of these drawn elements so that they appear the proper size on-screen.
+        val scaleFactorX = floatingView.scaleX
+        val scaleFactorY = floatingView.scaleY
+
+        // Get the bounds where we will draw the background image
+        val width = bounds.width().toFloat()
+        val height = bounds.height().toFloat()
+
+        // Get device-specific measurements
+        val cornerRadiusX = deviceCornerRadius / scaleFactorX
+        val cornerRadiusY = deviceCornerRadius / scaleFactorY
+        val halfDividerSize = deviceHalfDividerSize / scaleFactorX
+
+        // Calculate changing measurements for background
+        // We add one pixel to some measurements to create a smooth edge with no gaps
+        val onePixel = 1f / scaleFactorX
+        val changingDividerSize =
+            (cellSplitInterpolator.getInterpolation(progress) * halfDividerSize) - onePixel
+        val changingInnerRadiusX = cellSplitInterpolator.getInterpolation(progress) * cornerRadiusX
+        val changingInnerRadiusY = cellSplitInterpolator.getInterpolation(progress) * cornerRadiusY
+        val dividerCenterPos = width * desiredSplitRatio
+
+        // The left half of the background image
+        val leftSide = RectF(
+            0f,
+            0f,
+            dividerCenterPos - changingDividerSize,
+            height
+        )
+        // The right half of the background image
+        val rightSide = RectF(
+            dividerCenterPos + changingDividerSize,
+            0f,
+            width,
+            height
+        )
+
+        // Draw background
+        drawCustomRoundedRect(
+            canvas,
+            leftSide,
+            floatArrayOf(
+                cornerRadiusX, cornerRadiusY,
+                changingInnerRadiusX, changingInnerRadiusY,
+                changingInnerRadiusX, changingInnerRadiusY,
+                cornerRadiusX, cornerRadiusY
+            )
+        )
+        drawCustomRoundedRect(
+            canvas,
+            rightSide,
+            floatArrayOf(
+                changingInnerRadiusX, changingInnerRadiusY,
+                cornerRadiusX, cornerRadiusY,
+                cornerRadiusX, cornerRadiusY,
+                changingInnerRadiusX, changingInnerRadiusY
+            )
+        )
+
+        // Calculate changing measurements for icons.
+        val changingIconSizeX =
+            (STARTING_ICON_SIZE_PX +
+                ((ENDING_ICON_SIZE_PX - STARTING_ICON_SIZE_PX) *
+                    expandXInterpolator.getInterpolation(progress))) / scaleFactorX
+        val changingIconSizeY =
+            (STARTING_ICON_SIZE_PX +
+                ((ENDING_ICON_SIZE_PX - STARTING_ICON_SIZE_PX) *
+                    expandYInterpolator.getInterpolation(progress))) / scaleFactorY
+
+        val changingIcon1Left = ((width / 2f - halfDividerSize) / 2f) - (changingIconSizeX / 2f)
+        val changingIcon2Left =
+            (width - ((width / 2f - halfDividerSize) / 2f)) - (changingIconSizeX / 2f)
+        val changingIconTop = (height / 2f) - (changingIconSizeY / 2f)
+        val changingIconScaleX = changingIconSizeX / appIcon1.bounds.width()
+        val changingIconScaleY = changingIconSizeY / appIcon1.bounds.height()
+        val changingIconAlpha =
+            (255 - (255 * iconFadeInterpolator.getInterpolation(progress))).toInt()
+
+        // Draw first icon
+        canvas.save()
+        canvas.translate(changingIcon1Left, changingIconTop)
+        canvas.scale(changingIconScaleX, changingIconScaleY)
+        appIcon1.alpha = changingIconAlpha
+        appIcon1.draw(canvas)
+        canvas.restore()
+
+        // Draw second icon
+        canvas.save()
+        canvas.translate(changingIcon2Left, changingIconTop)
+        canvas.scale(changingIconScaleX, changingIconScaleY)
+        appIcon2.alpha = changingIconAlpha
+        appIcon2.draw(canvas)
+        canvas.restore()
+    }
+
+    /** When device is in portrait, we draw the rectangles with a top-bottom split. */
+    private fun drawTopBottomSplit(canvas: Canvas) {
+        val progress = floatingView.progress
+
+        // Since the entire floating app pair surface is scaling up during this animation, we
+        // scale down most of these drawn elements so that they appear the proper size on-screen.
+        val scaleFactorX = floatingView.scaleX
+        val scaleFactorY = floatingView.scaleY
+
+        // Get the bounds where we will draw the background image
+        val width = bounds.width().toFloat()
+        val height = bounds.height().toFloat()
+
+        // Get device-specific measurements
+        val cornerRadiusX = deviceCornerRadius / scaleFactorX
+        val cornerRadiusY = deviceCornerRadius / scaleFactorY
+        val halfDividerSize = deviceHalfDividerSize / scaleFactorY
+
+        // Calculate changing measurements for background
+        // We add one pixel to some measurements to create a smooth edge with no gaps
+        val onePixel = 1f / scaleFactorY
+        val changingDividerSize =
+            (cellSplitInterpolator.getInterpolation(progress) * halfDividerSize) - onePixel
+        val changingInnerRadiusX = cellSplitInterpolator.getInterpolation(progress) * cornerRadiusX
+        val changingInnerRadiusY = cellSplitInterpolator.getInterpolation(progress) * cornerRadiusY
+        val dividerCenterPos = height * desiredSplitRatio
+
+        // The top half of the background image
+        val topSide = RectF(
+            0f,
+            0f,
+            width,
+            dividerCenterPos - changingDividerSize
+        )
+        // The bottom half of the background image
+        val bottomSide = RectF(
+            0f,
+            dividerCenterPos + changingDividerSize,
+            width,
+            height
+        )
+
+        // Draw background
+        drawCustomRoundedRect(
+            canvas,
+            topSide,
+            floatArrayOf(
+                cornerRadiusX, cornerRadiusY,
+                cornerRadiusX, cornerRadiusY,
+                changingInnerRadiusX, changingInnerRadiusY,
+                changingInnerRadiusX, changingInnerRadiusY
+            )
+        )
+        drawCustomRoundedRect(
+            canvas,
+            bottomSide,
+            floatArrayOf(
+                changingInnerRadiusX, changingInnerRadiusY,
+                changingInnerRadiusX, changingInnerRadiusY,
+                cornerRadiusX, cornerRadiusY,
+                cornerRadiusX, cornerRadiusY
+            )
+        )
+
+        // Calculate changing measurements for icons.
+        val changingIconSizeX =
+            (STARTING_ICON_SIZE_PX +
+                ((ENDING_ICON_SIZE_PX - STARTING_ICON_SIZE_PX) *
+                    expandXInterpolator.getInterpolation(progress))) / scaleFactorX
+        val changingIconSizeY =
+            (STARTING_ICON_SIZE_PX +
+                ((ENDING_ICON_SIZE_PX - STARTING_ICON_SIZE_PX) *
+                    expandYInterpolator.getInterpolation(progress))) / scaleFactorY
+
+        val changingIconLeft = (width / 2f) - (changingIconSizeX / 2f)
+        val changingIcon1Top = (((height / 2f) - halfDividerSize) / 2f) - (changingIconSizeY / 2f)
+        val changingIcon2Top =
+            (height - (((height / 2f) - halfDividerSize) / 2f)) - (changingIconSizeY / 2f)
+        val changingIconScaleX = changingIconSizeX / appIcon1.bounds.width()
+        val changingIconScaleY = changingIconSizeY / appIcon1.bounds.height()
+        val changingIconAlpha =
+            (255 - 255 * iconFadeInterpolator.getInterpolation(progress)).toInt()
+
+        // Draw first icon
+        canvas.save()
+        canvas.translate(changingIconLeft, changingIcon1Top)
+        canvas.scale(changingIconScaleX, changingIconScaleY)
+        appIcon1.alpha = changingIconAlpha
+        appIcon1.draw(canvas)
+        canvas.restore()
+
+        // Draw second icon
+        canvas.save()
+        canvas.translate(changingIconLeft, changingIcon2Top)
+        canvas.scale(changingIconScaleX, changingIconScaleY)
+        appIcon2.alpha = changingIconAlpha
+        appIcon2.draw(canvas)
+        canvas.restore()
+    }
+
+    /**
+     * Draws a rectangle with custom rounded corners.
+     *
+     * @param c The Canvas to draw on.
+     * @param rect The bounds of the rectangle.
+     * @param radii An array of 8 radii for the corners: top left x, top left y, top right x, top
+     *   right y, bottom right x, and so on.
+     */
+    private fun drawCustomRoundedRect(c: Canvas, rect: RectF, radii: FloatArray) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+            // Canvas.drawDoubleRoundRect is supported from Q onward
+            c.drawDoubleRoundRect(rect, radii, EMPTY_RECT, ARRAY_OF_ZEROES, backgroundPaint)
+        } else {
+            // Fallback rectangle with uniform rounded corners
+            val scaleFactorX = floatingView.scaleX
+            val scaleFactorY = floatingView.scaleY
+            val cornerRadiusX = QuickStepContract.getWindowCornerRadius(launcher) / scaleFactorX
+            val cornerRadiusY = QuickStepContract.getWindowCornerRadius(launcher) / scaleFactorY
+            c.drawRoundRect(rect, cornerRadiusX, cornerRadiusY, backgroundPaint)
+        }
+    }
+
+    override fun getOpacity(): Int {
+        return PixelFormat.OPAQUE
+    }
+
+    override fun setAlpha(i: Int) {
+        // Required by Drawable but not used.
+    }
+
+    override fun setColorFilter(colorFilter: ColorFilter?) {
+        // Required by Drawable but not used.
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/views/FloatingAppPairView.kt b/quickstep/src/com/android/quickstep/views/FloatingAppPairView.kt
new file mode 100644
index 0000000..e90aa13
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/FloatingAppPairView.kt
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.views
+
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Rect
+import android.graphics.RectF
+import android.graphics.drawable.Drawable
+import android.util.AttributeSet
+import android.view.View
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import com.android.launcher3.R
+import com.android.launcher3.Utilities
+import com.android.launcher3.statemanager.StatefulActivity
+import com.android.launcher3.views.BaseDragLayer
+
+/**
+ * A temporary View that is created for the app pair launch animation and destroyed at the end.
+ * Matches the size & position of the app pair icon graphic, and expands to full screen.
+ */
+class FloatingAppPairView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
+    FrameLayout(context, attrs) {
+    companion object {
+        fun getFloatingAppPairView(
+            launcher: StatefulActivity<*>,
+            originalView: View,
+            appIcon1: Drawable,
+            appIcon2: Drawable,
+            dividerPos: Int
+        ): FloatingAppPairView {
+            val dragLayer: ViewGroup = launcher.getDragLayer()
+            val floatingView =
+                launcher
+                    .getLayoutInflater()
+                    .inflate(R.layout.floating_app_pair_view, dragLayer, false)
+                    as FloatingAppPairView
+            floatingView.init(launcher, originalView, appIcon1, appIcon2, dividerPos)
+            dragLayer.addView(floatingView, dragLayer.childCount - 1)
+            return floatingView
+        }
+    }
+
+    val startingPosition = RectF()
+    private lateinit var background: FloatingAppPairBackground
+    var progress = 0f
+
+    /** Initializes the view, copying the bounds and location of the original icon view. */
+    fun init(
+        launcher: StatefulActivity<*>,
+        originalView: View,
+        appIcon1: Drawable,
+        appIcon2: Drawable,
+        dividerPos: Int
+    ) {
+        val viewBounds = Rect(0, 0, originalView.width, originalView.height)
+        Utilities.getBoundsForViewInDragLayer(
+            launcher.getDragLayer(),
+            originalView,
+            viewBounds,
+            false /* ignoreTransform */,
+            null /* recycle */,
+            startingPosition
+        )
+        val lp =
+            BaseDragLayer.LayoutParams(
+                Math.round(startingPosition.width()),
+                Math.round(startingPosition.height())
+            )
+        lp.ignoreInsets = true
+
+        // Position the floating view exactly on top of the original
+        lp.topMargin = Math.round(startingPosition.top)
+        lp.leftMargin = Math.round(startingPosition.left)
+
+        layout(lp.leftMargin, lp.topMargin, lp.leftMargin + lp.width, lp.topMargin + lp.height)
+        layoutParams = lp
+
+        // Prepare to draw app pair icon background
+        background = FloatingAppPairBackground(context, this, appIcon1, appIcon2, dividerPos)
+        background.setBounds(0, 0, lp.width, lp.height)
+    }
+
+    override fun dispatchDraw(canvas: Canvas) {
+        super.dispatchDraw(canvas)
+        background.draw(canvas)
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/views/GroupedTaskView.java b/quickstep/src/com/android/quickstep/views/GroupedTaskView.java
index dd201af..2ae64ff 100644
--- a/quickstep/src/com/android/quickstep/views/GroupedTaskView.java
+++ b/quickstep/src/com/android/quickstep/views/GroupedTaskView.java
@@ -17,6 +17,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
+import com.android.internal.jank.Cuj;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
@@ -246,12 +247,11 @@
         RunnableList endCallback = new RunnableList();
         RecentsView recentsView = getRecentsView();
         // Callbacks run from remote animation when recents animation not currently running
-        InteractionJankMonitorWrapper.begin(this,
-                InteractionJankMonitorWrapper.CUJ_SPLIT_SCREEN_ENTER, "Enter form GroupedTaskView");
+        InteractionJankMonitorWrapper.begin(this, Cuj.CUJ_SPLIT_SCREEN_ENTER,
+                "Enter form GroupedTaskView");
         launchTaskInternal(success -> {
             endCallback.executeAllAndDestroy();
-            InteractionJankMonitorWrapper.end(
-                    InteractionJankMonitorWrapper.CUJ_SPLIT_SCREEN_ENTER);
+            InteractionJankMonitorWrapper.end(Cuj.CUJ_SPLIT_SCREEN_ENTER);
         }, false /* freezeTaskList */, true /*launchingExistingTaskview*/);
 
 
diff --git a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
index 5b5d0de..9bb9775a 100644
--- a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
@@ -16,6 +16,7 @@
 package com.android.quickstep.views;
 
 import static android.app.ActivityTaskManager.INVALID_TASK_ID;
+
 import static com.android.launcher3.LauncherState.ALL_APPS;
 import static com.android.launcher3.LauncherState.CLEAR_ALL_BUTTON;
 import static com.android.launcher3.LauncherState.EDIT_MODE;
@@ -144,6 +145,8 @@
         setOverviewFullscreenEnabled(toState.getOverviewFullscreenProgress() == 1);
         if (toState == OVERVIEW_MODAL_TASK) {
             setOverviewSelectEnabled(true);
+        } else {
+            resetModalVisuals();
         }
 
         // Set border after select mode changes to avoid showing border during state transition
@@ -214,7 +217,6 @@
         } else {
             if (mActivity.isInState(LauncherState.OVERVIEW_MODAL_TASK)) {
                 mActivity.getStateManager().goToState(LauncherState.OVERVIEW, animate);
-                resetModalVisuals();
             }
         }
     }
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 7e1034b..997624f 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -133,6 +133,7 @@
 import androidx.annotation.UiThread;
 import androidx.core.graphics.ColorUtils;
 
+import com.android.internal.jank.Cuj;
 import com.android.launcher3.BaseActivity;
 import com.android.launcher3.BaseActivity.MultiWindowModeChangedListener;
 import com.android.launcher3.DeviceProfile;
@@ -1444,8 +1445,7 @@
             mActionsView.updateDisabledFlags(OverviewActionsView.DISABLED_SCROLLING, true);
         }
         if (mOverviewStateEnabled) { // only when in overview
-            InteractionJankMonitorWrapper.begin(/* view= */ this,
-                    InteractionJankMonitorWrapper.CUJ_RECENTS_SCROLLING);
+            InteractionJankMonitorWrapper.begin(/* view= */ this, Cuj.CUJ_RECENTS_SCROLLING);
         }
     }
 
@@ -1460,7 +1460,7 @@
         if (getNextPage() > 0) {
             setSwipeDownShouldLaunchApp(true);
         }
-        InteractionJankMonitorWrapper.end(InteractionJankMonitorWrapper.CUJ_RECENTS_SCROLLING);
+        InteractionJankMonitorWrapper.end(Cuj.CUJ_RECENTS_SCROLLING);
     }
 
     @Override
@@ -2124,8 +2124,7 @@
         for (int i = 0; i < taskCount; i++) {
             TaskView taskView = requireTaskViewAt(i);
             taskView.updateTaskSize();
-            taskView.getPrimaryNonGridTranslationProperty().set(taskView, accumulatedTranslationX);
-            taskView.getSecondaryNonGridTranslationProperty().set(taskView, 0f);
+            taskView.setNonGridTranslationX(accumulatedTranslationX);
             taskView.setNonGridPivotTranslationX(translateXToMiddle);
             // Compensate space caused by TaskView scaling.
             float widthDiff =
@@ -2642,23 +2641,25 @@
         if (endState.displayOverviewTasksAsGrid(mActivity.getDeviceProfile())) {
             TaskView runningTaskView = getRunningTaskView();
             float runningTaskPrimaryGridTranslation = 0;
+            float runningTaskSecondaryGridTranslation = 0;
             if (runningTaskView != null) {
                 // Apply the grid translation to running task unless it's being snapped to
                 // and removes the current translation applied to the running task.
-                runningTaskPrimaryGridTranslation = mOrientationHandler.getPrimaryValue(
-                        runningTaskView.getGridTranslationX(),
-                        runningTaskView.getGridTranslationY())
-                        - runningTaskView.getPrimaryNonGridTranslationProperty().get(
-                        runningTaskView);
+                runningTaskPrimaryGridTranslation = runningTaskView.getGridTranslationX()
+                        - runningTaskView.getNonGridTranslationX();
+                runningTaskSecondaryGridTranslation = runningTaskView.getGridTranslationY();
             }
             for (TaskViewSimulator tvs : taskViewSimulators) {
                 if (animatorSet == null) {
                     setGridProgress(1);
                     tvs.taskPrimaryTranslation.value = runningTaskPrimaryGridTranslation;
+                    tvs.taskSecondaryTranslation.value = runningTaskSecondaryGridTranslation;
                 } else {
                     animatorSet.play(ObjectAnimator.ofFloat(this, RECENTS_GRID_PROGRESS, 1));
                     animatorSet.play(tvs.taskPrimaryTranslation.animateToValue(
                             runningTaskPrimaryGridTranslation));
+                    animatorSet.play(tvs.taskSecondaryTranslation.animateToValue(
+                            runningTaskSecondaryGridTranslation));
                 }
             }
         }
@@ -3123,6 +3124,14 @@
                     + snappedTaskNonGridScrollAdjustment);
         }
 
+        final TaskView runningTask = getRunningTaskView();
+        if (showAsGrid() && enableGridOnlyOverview() && runningTask != null) {
+            runActionOnRemoteHandles(
+                    remoteTargetHandle -> remoteTargetHandle.getTaskViewSimulator()
+                            .taskSecondaryTranslation.value = runningTask.getGridTranslationY()
+            );
+        }
+
         mClearAllButton.setGridTranslationPrimary(
                 clearAllTotalTranslationX - snappedTaskGridTranslationX);
         mClearAllButton.setGridScrollOffset(
@@ -3237,8 +3246,9 @@
         anim.add(ObjectAnimator.ofFloat(taskView, secondaryViewTranslate,
                 verticalFactor * secondaryTaskDimension * 2).setDuration(duration), LINEAR, sp);
 
-        if (mEnableDrawingLiveTile && taskView.isRunningTask()) {
+        if (taskView.isRunningTask()) {
             anim.addOnFrameCallback(() -> {
+                if (!mEnableDrawingLiveTile) return;
                 runActionOnRemoteHandles(
                         remoteTargetHandle -> remoteTargetHandle.getTaskViewSimulator()
                                 .taskSecondaryTranslation.value = mOrientationHandler
@@ -3304,8 +3314,8 @@
                         timings.getInstructionsUnfoldEndOffset()));
         mSplitSelectStateController.setSplitInstructionsView(splitInstructionsView);
 
-        InteractionJankMonitorWrapper.begin(this,
-                InteractionJankMonitorWrapper.CUJ_SPLIT_SCREEN_ENTER, "First tile selected");
+        InteractionJankMonitorWrapper.begin(this, Cuj.CUJ_SPLIT_SCREEN_ENTER,
+                "First tile selected");
         anim.addListener(new AnimatorListenerAdapter() {
             @Override
             public void onAnimationStart(Animator animation) {
@@ -3321,8 +3331,7 @@
         });
         anim.addEndListener(success -> {
             if (success) {
-                InteractionJankMonitorWrapper.end(
-                        InteractionJankMonitorWrapper.CUJ_SPLIT_SCREEN_ENTER);
+                InteractionJankMonitorWrapper.end(Cuj.CUJ_SPLIT_SCREEN_ENTER);
             } else {
                 // If transition to split select was interrupted, clean up to prevent glitches
                 if (FeatureFlags.enableSplitContextually()) {
@@ -3330,8 +3339,7 @@
                 } else {
                     resetFromSplitSelectionState();
                 }
-                InteractionJankMonitorWrapper.cancel(
-                        InteractionJankMonitorWrapper.CUJ_SPLIT_SCREEN_ENTER);
+                InteractionJankMonitorWrapper.cancel(Cuj.CUJ_SPLIT_SCREEN_ENTER);
             }
 
             updateCurrentTaskActionsVisibility();
@@ -4374,11 +4382,14 @@
 
     private void updatePivots() {
         if (mOverviewSelectEnabled) {
-            getModalTaskSize(mTempRect);
-            Rect selectedTaskPosition = getSelectedTaskBounds();
-
-            Utilities.getPivotsForScalingRectToRect(mTempRect, selectedTaskPosition,
-                    mTempPointF);
+            if (enableGridOnlyOverview()) {
+                getModalTaskSize(mTempRect);
+                Rect selectedTaskPosition = getSelectedTaskBounds();
+                Utilities.getPivotsForScalingRectToRect(mTempRect, selectedTaskPosition,
+                        mTempPointF);
+            } else {
+                mTempPointF.set(mLastComputedTaskSize.centerX(), mLastComputedTaskSize.bottom);
+            }
         } else {
             mTempRect.set(mLastComputedTaskSize);
             // Only update pivot when it is tablet and not in grid yet, so the pivot is correct
@@ -4827,8 +4838,7 @@
                         } else {
                             resetFromSplitSelectionState();
                         }
-                        InteractionJankMonitorWrapper.end(
-                                InteractionJankMonitorWrapper.CUJ_SPLIT_SCREEN_ENTER);
+                        InteractionJankMonitorWrapper.end(Cuj.CUJ_SPLIT_SCREEN_ENTER);
                     });
         });
 
@@ -4838,8 +4848,8 @@
                     mSplitSelectStateController.getSecondTaskId());
         }
 
-        InteractionJankMonitorWrapper.begin(this,
-                InteractionJankMonitorWrapper.CUJ_SPLIT_SCREEN_ENTER, "Second tile selected");
+        InteractionJankMonitorWrapper.begin(this, Cuj.CUJ_SPLIT_SCREEN_ENTER,
+                "Second tile selected");
 
         // Fade out all other views underneath placeholders
         ObjectAnimator tvFade = ObjectAnimator.ofFloat(this, RecentsView.CONTENT_ALPHA,1, 0);
@@ -5702,11 +5712,20 @@
     }
 
     private void updateEnabledOverlays() {
+        TaskView focusedTaskView = getFocusedTaskView();
         int taskCount = getTaskViewCount();
         for (int i = 0; i < taskCount; i++) {
             TaskView taskView = requireTaskViewAt(i);
+            if (taskView == focusedTaskView) {
+                continue;
+            }
             taskView.setOverlayEnabled(mOverlayEnabled && isTaskViewFullyVisible(taskView));
         }
+        // Focus task overlay should be enabled and refreshed at last
+        if (focusedTaskView != null) {
+            focusedTaskView.setOverlayEnabled(
+                    mOverlayEnabled && isTaskViewFullyVisible(focusedTaskView));
+        }
     }
 
     public void setOverlayEnabled(boolean overlayEnabled) {
diff --git a/quickstep/src/com/android/quickstep/views/TaskMenuView.java b/quickstep/src/com/android/quickstep/views/TaskMenuView.java
index cf89d2e..77033b2 100644
--- a/quickstep/src/com/android/quickstep/views/TaskMenuView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskMenuView.java
@@ -115,9 +115,6 @@
             animateClose();
         } else {
             closeComplete();
-            if (enableOverviewIconMenu()) {
-                ((IconAppChipView) mTaskContainer.getIconView()).reset();
-            }
         }
     }
 
@@ -378,9 +375,18 @@
 
     private void closeComplete() {
         mIsOpen = false;
+        resetOverviewIconMenu();
         mActivity.getDragLayer().removeView(this);
     }
 
+    private void resetOverviewIconMenu() {
+        if (enableOverviewIconMenu()) {
+            ((IconAppChipView) mTaskContainer.getIconView()).reset();
+            setTranslationY(mMenuTranslationYBeforeOpen);
+            mTaskContainer.getIconView().asView().setTranslationY(mIconViewTranslationYBeforeOpen);
+        }
+    }
+
     private RoundedRectRevealOutlineProvider createOpenCloseOutlineProvider() {
         float radius = TaskCornerRadius.get(mContext);
         Rect fromRect = new Rect(
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java
index af4f402..66a880b 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskView.java
@@ -119,6 +119,8 @@
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.QuickStepContract;
 
+import kotlin.Unit;
+
 import java.lang.annotation.Retention;
 import java.util.Arrays;
 import java.util.Collections;
@@ -127,8 +129,6 @@
 import java.util.function.Consumer;
 import java.util.stream.Stream;
 
-import kotlin.Unit;
-
 /**
  * A task in the Recents view.
  */
@@ -304,32 +304,6 @@
                 }
             };
 
-    private static final FloatProperty<TaskView> NON_GRID_TRANSLATION_X =
-            new FloatProperty<TaskView>("nonGridTranslationX") {
-                @Override
-                public void setValue(TaskView taskView, float v) {
-                    taskView.setNonGridTranslationX(v);
-                }
-
-                @Override
-                public Float get(TaskView taskView) {
-                    return taskView.mNonGridTranslationX;
-                }
-            };
-
-    private static final FloatProperty<TaskView> NON_GRID_TRANSLATION_Y =
-            new FloatProperty<TaskView>("nonGridTranslationY") {
-                @Override
-                public void setValue(TaskView taskView, float v) {
-                    taskView.setNonGridTranslationY(v);
-                }
-
-                @Override
-                public Float get(TaskView taskView) {
-                    return taskView.mNonGridTranslationY;
-                }
-            };
-
     public static final FloatProperty<TaskView> GRID_END_TRANSLATION_X =
             new FloatProperty<TaskView>("gridEndTranslationX") {
                 @Override
@@ -386,7 +360,6 @@
     // Applied as a complement to gridTranslation, for adjusting the carousel overview and quick
     // switch.
     private float mNonGridTranslationX;
-    private float mNonGridTranslationY;
     private float mNonGridPivotTranslationX;
     // Used when in SplitScreenSelectState
     private float mSplitSelectTranslationY;
@@ -1062,9 +1035,6 @@
             anim.addListener(new AnimatorListenerAdapter() {
                 @Override
                 public void onAnimationStart(Animator animation) {
-                    if (!recentsView.showAsGrid()) {
-                        return;
-                    }
                     recentsView.runActionOnRemoteHandles(
                             (Consumer<RemoteTargetHandle>) remoteTargetHandle ->
                                     remoteTargetHandle
@@ -1323,7 +1293,7 @@
     }
 
     protected void resetPersistentViewTransforms() {
-        mNonGridTranslationX = mNonGridTranslationY = mGridTranslationX =
+        mNonGridTranslationX = mGridTranslationX =
                 mGridTranslationY = mBoxTranslationY = mNonGridPivotTranslationX = 0f;
         resetViewTransforms();
     }
@@ -1494,14 +1464,16 @@
         applyTranslationY();
     }
 
-    private void setNonGridTranslationX(float nonGridTranslationX) {
-        mNonGridTranslationX = nonGridTranslationX;
-        applyTranslationX();
+    public float getNonGridTranslationX() {
+        return mNonGridTranslationX;
     }
 
-    private void setNonGridTranslationY(float nonGridTranslationY) {
-        mNonGridTranslationY = nonGridTranslationY;
-        applyTranslationY();
+    /**
+     * Updates X coordinate of non-grid translation.
+     */
+    public void setNonGridTranslationX(float nonGridTranslationX) {
+        mNonGridTranslationX = nonGridTranslationX;
+        applyTranslationX();
     }
 
     public void setGridTranslationX(float gridTranslationX) {
@@ -1540,7 +1512,7 @@
         if (gridEnabled) {
             scrollAdjustment += mGridTranslationX;
         } else {
-            scrollAdjustment += getPrimaryNonGridTranslationProperty().get(this);
+            scrollAdjustment += getNonGridTranslationX();
         }
         return scrollAdjustment;
     }
@@ -1586,9 +1558,7 @@
      * change according to a temporary state (e.g. task offset).
      */
     public float getPersistentTranslationY() {
-        return mBoxTranslationY
-                + getNonGridTrans(mNonGridTranslationY)
-                + getGridTrans(mGridTranslationY);
+        return mBoxTranslationY + getGridTrans(mGridTranslationY);
     }
 
     public FloatProperty<TaskView> getPrimarySplitTranslationProperty() {
@@ -1626,16 +1596,6 @@
                 TASK_RESISTANCE_TRANSLATION_X, TASK_RESISTANCE_TRANSLATION_Y);
     }
 
-    public FloatProperty<TaskView> getPrimaryNonGridTranslationProperty() {
-        return getPagedOrientationHandler().getPrimaryValue(
-                NON_GRID_TRANSLATION_X, NON_GRID_TRANSLATION_Y);
-    }
-
-    public FloatProperty<TaskView> getSecondaryNonGridTranslationProperty() {
-        return getPagedOrientationHandler().getSecondaryValue(
-                NON_GRID_TRANSLATION_X, NON_GRID_TRANSLATION_Y);
-    }
-
     @Override
     public boolean hasOverlappingRendering() {
         // TODO: Clip-out the icon region from the thumbnail, since they are overlapping.
diff --git a/quickstep/tests/src/com/android/launcher3/model/AppEventProducerTest.java b/quickstep/tests/src/com/android/launcher3/model/AppEventProducerTest.java
new file mode 100644
index 0000000..d4dd580
--- /dev/null
+++ b/quickstep/tests/src/com/android/launcher3/model/AppEventProducerTest.java
@@ -0,0 +1,132 @@
+/*
+ * 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.model;
+
+import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
+
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_ALL_APPS;
+import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
+
+import static junit.framework.Assert.assertEquals;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+
+import android.app.prediction.AppTarget;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.os.Process;
+import android.os.UserHandle;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.launcher3.logger.LauncherAtom;
+import com.android.launcher3.model.data.AppInfo;
+import com.android.launcher3.pm.UserCache;
+import com.android.launcher3.util.MainThreadInitializedObject.SandboxContext;
+import com.android.launcher3.util.UserIconInfo;
+import com.android.systemui.shared.system.SysUiStatsLog;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.Arrays;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class AppEventProducerTest {
+
+    private static final UserHandle MAIN_HANDLE = Process.myUserHandle();
+    private static final UserHandle PRIVATE_HANDLE = new UserHandle(11);
+
+    private static final UserIconInfo MAIN_ICON_INFO =
+            new UserIconInfo(MAIN_HANDLE, UserIconInfo.TYPE_MAIN);
+    private static final UserIconInfo PRIVATE_ICON_INFO =
+            new UserIconInfo(PRIVATE_HANDLE, UserIconInfo.TYPE_PRIVATE);
+
+    private SandboxContext mContext;
+    private AppEventProducer mAppEventProducer;
+    @Mock
+    private UserCache mUserCache;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mContext = new SandboxContext(getApplicationContext());
+        mContext.putObject(UserCache.INSTANCE, mUserCache);
+        mAppEventProducer = new AppEventProducer(mContext, null);
+    }
+
+    @After
+    public void tearDown() {
+        mContext.onDestroy();
+    }
+
+    @Test
+    public void buildAppTarget_containsCorrectUser() {
+        when(mUserCache.getUserProfiles())
+                .thenReturn(Arrays.asList(MAIN_HANDLE, PRIVATE_HANDLE));
+        when(mUserCache.getUserInfo(any(UserHandle.class)))
+                .thenReturn(MAIN_ICON_INFO, PRIVATE_ICON_INFO);
+        ComponentName gmailComponentName = new ComponentName(mContext,
+                "com.android.launcher3.tests.Activity" + "Gmail");
+        AppInfo gmailAppInfo = new
+                AppInfo(gmailComponentName, "Gmail", MAIN_HANDLE, new Intent());
+        gmailAppInfo.container = CONTAINER_ALL_APPS;
+        gmailAppInfo.itemType = ITEM_TYPE_APPLICATION;
+
+        AppTarget gmailTarget = mAppEventProducer
+                .toAppTarget(buildItemInfoProtoForAppInfo(gmailAppInfo));
+
+        assert gmailTarget != null;
+        assertEquals(gmailTarget.getUser(), MAIN_HANDLE);
+
+        when(mUserCache.getUserInfo(any(UserHandle.class)))
+                .thenReturn(MAIN_ICON_INFO, PRIVATE_ICON_INFO);
+        AppInfo gmailAppInfoPrivate = new
+                AppInfo(gmailComponentName, "Gmail", PRIVATE_HANDLE, new Intent());
+        gmailAppInfoPrivate.container = CONTAINER_ALL_APPS;
+        gmailAppInfoPrivate.itemType = ITEM_TYPE_APPLICATION;
+
+        AppTarget gmailPrivateTarget = mAppEventProducer
+                .toAppTarget(buildItemInfoProtoForAppInfo(gmailAppInfoPrivate));
+
+        assert gmailPrivateTarget != null;
+        assertEquals(gmailPrivateTarget.getUser(), PRIVATE_HANDLE);
+    }
+
+    private LauncherAtom.ItemInfo buildItemInfoProtoForAppInfo(AppInfo appInfo) {
+        LauncherAtom.ItemInfo.Builder itemBuilder = LauncherAtom.ItemInfo.newBuilder();
+        if (appInfo.user.equals(PRIVATE_HANDLE)) {
+            itemBuilder.setUserType(SysUiStatsLog.LAUNCHER_UICHANGED__USER_TYPE__TYPE_PRIVATE);
+        } else {
+            itemBuilder.setUserType(SysUiStatsLog.LAUNCHER_UICHANGED__USER_TYPE__TYPE_MAIN);
+        }
+        itemBuilder.setApplication(LauncherAtom.Application.newBuilder()
+                .setComponentName(appInfo.componentName.flattenToShortString())
+                .setPackageName(appInfo.componentName.getPackageName()));
+        itemBuilder.setContainerInfo(LauncherAtom.ContainerInfo.newBuilder()
+                .setAllAppsContainer(LauncherAtom.AllAppsContainer.getDefaultInstance())
+                .build());
+        return itemBuilder.build();
+    }
+}
diff --git a/quickstep/tests/src/com/android/quickstep/AbstractQuickStepTest.java b/quickstep/tests/src/com/android/quickstep/AbstractQuickStepTest.java
index 3a5fb04..9ad360f 100644
--- a/quickstep/tests/src/com/android/quickstep/AbstractQuickStepTest.java
+++ b/quickstep/tests/src/com/android/quickstep/AbstractQuickStepTest.java
@@ -16,8 +16,6 @@
 
 package com.android.quickstep;
 
-import static com.android.launcher3.ui.TaplTestsLauncher3.getAppPackageName;
-
 import static org.junit.Assert.assertTrue;
 
 import android.os.SystemProperties;
diff --git a/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java b/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
index eced5a9..39d6f03 100644
--- a/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
+++ b/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
@@ -28,10 +28,12 @@
 import static com.android.launcher3.ui.AbstractLauncherUiTest.resolveSystemApp;
 import static com.android.launcher3.ui.AbstractLauncherUiTest.startAppFast;
 import static com.android.launcher3.ui.AbstractLauncherUiTest.startTestActivity;
-import static com.android.launcher3.ui.TaplTestsLauncher3.getAppPackageName;
+import static com.android.launcher3.ui.TaplTestsLauncher3Test.getAppPackageName;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.rule.ShellCommandRule.disableHeadsUpNotification;
 import static com.android.launcher3.util.rule.ShellCommandRule.getLauncherCommand;
+import static com.android.launcher3.util.rule.TestStabilityRule.LOCAL;
+import static com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_POSTSUBMIT;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
@@ -67,7 +69,6 @@
 
 import org.junit.After;
 import org.junit.Before;
-import org.junit.Ignore;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.RuleChain;
@@ -76,6 +77,7 @@
 import org.junit.runners.model.Statement;
 
 import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
 import java.util.function.Consumer;
 import java.util.function.Function;
@@ -131,8 +133,7 @@
                     UiDevice.getInstance(getInstrumentation()).executeShellCommand(
                             getLauncherCommand(getLauncherInMyProcess()));
                     // b/143488140
-                    mDevice.pressHome();
-                    mDevice.waitForIdle();
+                    pressHomeAndWaitForOverviewClose();
                 }
             }
         };
@@ -144,7 +145,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);
 
@@ -186,12 +187,14 @@
         mLauncher.getLaunchedAppState().switchToOverview();
     }
 
-    // b/143488140
+    // Staging; will be promoted to presubmit if stable
+    @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT)
+
     //@NavigationModeSwitch
-    @Ignore
     @Test
     public void goToOverviewFromApp() {
         startAppFast(resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR));
+        waitForRecentsActivityStop();
 
         mLauncher.getLaunchedAppState().switchToOverview();
     }
@@ -218,10 +221,34 @@
     }
 
     private BaseOverview pressHomeAndGoToOverview() {
-        mDevice.pressHome();
+        pressHomeAndWaitForOverviewClose();
         return mLauncher.getLaunchedAppState().switchToOverview();
     }
 
+    private void pressHomeAndWaitForOverviewClose() {
+        mDevice.pressHome();
+        waitForRecentsActivityStop();
+    }
+
+    private void waitForRecentsActivityStop() {
+        try {
+            final boolean recentsActivityIsNull = MAIN_EXECUTOR.submit(
+                    () -> RecentsActivity.ACTIVITY_TRACKER.getCreatedActivity() == null).get();
+            if (recentsActivityIsNull) {
+                // Null activity counts as a "stopped" one.
+                return;
+            }
+        } catch (ExecutionException e) {
+            throw new RuntimeException(e);
+        } catch (InterruptedException e) {
+            throw new RuntimeException(e);
+        }
+
+        Wait.atMost("Recents activity didn't stop",
+                () -> getFromRecents(recents -> !recents.isStarted()),
+                DEFAULT_UI_TIMEOUT, mLauncher);
+    }
+
     // b/143488140
     //@NavigationModeSwitch
     @Test
@@ -229,6 +256,7 @@
         startAppFast(getAppPackageName());
         startAppFast(resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR));
         startTestActivity(2);
+        waitForRecentsActivityStop();
         Wait.atMost("Expected three apps in the task list",
                 () -> mLauncher.getRecentTasks().size() >= 3, DEFAULT_ACTIVITY_TIMEOUT, mLauncher);
 
diff --git a/quickstep/tests/src/com/android/quickstep/HotseatWidthCalculationTest.kt b/quickstep/tests/src/com/android/quickstep/HotseatWidthCalculationTest.kt
index a347156..b1ba4c6 100644
--- a/quickstep/tests/src/com/android/quickstep/HotseatWidthCalculationTest.kt
+++ b/quickstep/tests/src/com/android/quickstep/HotseatWidthCalculationTest.kt
@@ -47,7 +47,7 @@
         assertThat(dp.getHotseatLayoutPadding(context).right).isEqualTo(580)
 
         assertThat(dp.isQsbInline).isFalse()
-        assertThat(dp.hotseatQsbWidth).isEqualTo(1445)
+        assertThat(dp.hotseatQsbWidth).isEqualTo(1435)
     }
 
     /**
@@ -69,7 +69,7 @@
         assertThat(dp.getHotseatLayoutPadding(context).right).isEqualTo(550)
 
         assertThat(dp.isQsbInline).isFalse()
-        assertThat(dp.hotseatQsbWidth).isEqualTo(1080)
+        assertThat(dp.hotseatQsbWidth).isEqualTo(1070)
     }
 
     /**
@@ -90,7 +90,7 @@
         assertThat(dp.getHotseatLayoutPadding(context).right).isEqualTo(759)
 
         assertThat(dp.isQsbInline).isFalse()
-        assertThat(dp.hotseatQsbWidth).isEqualTo(1468)
+        assertThat(dp.hotseatQsbWidth).isEqualTo(1455)
     }
 
     /**
@@ -115,7 +115,7 @@
         assertThat(dp.getHotseatLayoutPadding(context).right).isEqualTo(1040)
 
         assertThat(dp.isQsbInline).isFalse()
-        assertThat(dp.hotseatQsbWidth).isEqualTo(1233)
+        assertThat(dp.hotseatQsbWidth).isEqualTo(1223)
     }
 
     /** This is a case when after setting the hotseat, the QSB width needs to be changed to fit */
diff --git a/quickstep/tests/src/com/android/quickstep/TaplDigitalWellBeingToastTest.java b/quickstep/tests/src/com/android/quickstep/TaplDigitalWellBeingToastTest.java
index 728fe67..4aa7cb0 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplDigitalWellBeingToastTest.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplDigitalWellBeingToastTest.java
@@ -17,8 +17,6 @@
 
 import static androidx.test.InstrumentationRegistry.getInstrumentation;
 
-import static com.android.launcher3.LauncherState.OVERVIEW;
-
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
@@ -84,8 +82,7 @@
     }
 
     private DigitalWellBeingToast getToast() {
-        executeOnLauncher(launcher -> launcher.getStateManager().goToState(OVERVIEW));
-        waitForState("Launcher internal state didn't switch to Overview", () -> OVERVIEW);
+        mLauncher.getWorkspace().switchToOverview();
         final TaskView task = getOnceNotNull("No latest task", launcher -> getLatestTask(launcher));
 
         return getFromLauncher(launcher -> {
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsKeyboardQuickSwitch.java b/quickstep/tests/src/com/android/quickstep/TaplTestsKeyboardQuickSwitch.java
index 829e54b..7191f70 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsKeyboardQuickSwitch.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsKeyboardQuickSwitch.java
@@ -33,7 +33,16 @@
 public class TaplTestsKeyboardQuickSwitch extends AbstractQuickStepTest {
 
     private enum TestSurface {
-        HOME, LAUNCHED_APP, HOME_ALL_APPS, WIDGETS,
+        HOME(true),
+        LAUNCHED_APP(false),
+        HOME_ALL_APPS(true),
+        WIDGETS(true);
+
+        private final boolean mInitialFocusAtZero;
+
+        TestSurface(boolean initialFocusAtZero) {
+            mInitialFocusAtZero = initialFocusAtZero;
+        }
     }
 
     private enum TestCase {
@@ -172,13 +181,22 @@
                 kqs.dismiss();
                 break;
             case LAUNCH_LAST_APP:
-                kqs.launchFocusedAppTask(CALCULATOR_APP_PACKAGE);
+                kqs.launchFocusedAppTask(testSurface.mInitialFocusAtZero
+                        ? getAppPackageName() : CALCULATOR_APP_PACKAGE);
                 break;
             case LAUNCH_SELECTED_APP:
-                kqs.moveFocusForward().launchFocusedAppTask(CALCULATOR_APP_PACKAGE);
+                kqs.moveFocusForward();
+                if (testSurface.mInitialFocusAtZero) {
+                    kqs.moveFocusForward();
+                }
+                kqs.launchFocusedAppTask(CALCULATOR_APP_PACKAGE);
                 break;
             case LAUNCH_OVERVIEW:
-                kqs.moveFocusBackward().moveFocusBackward().launchFocusedOverviewTask();
+                kqs.moveFocusBackward();
+                if (!testSurface.mInitialFocusAtZero) {
+                    kqs.moveFocusBackward();
+                }
+                kqs.launchFocusedOverviewTask();
                 break;
             default:
                 throw new IllegalStateException("Cannot run test case: " + testCase);
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsPersistentTaskbar.java b/quickstep/tests/src/com/android/quickstep/TaplTestsPersistentTaskbar.java
index c9e536a..a71d74a 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsPersistentTaskbar.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsPersistentTaskbar.java
@@ -15,18 +15,28 @@
  */
 package com.android.quickstep;
 
+import static com.android.launcher3.util.rule.TestStabilityRule.LOCAL;
+import static com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_POSTSUBMIT;
 import static com.android.quickstep.TaskbarModeSwitchRule.Mode.PERSISTENT;
 
+import android.graphics.Rect;
+
 import androidx.test.filters.LargeTest;
+import androidx.test.platform.app.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape;
+import com.android.launcher3.util.rule.TestStabilityRule;
 import com.android.quickstep.NavigationModeSwitchRule.NavigationModeSwitch;
 import com.android.quickstep.TaskbarModeSwitchRule.TaskbarModeSwitch;
 
+import org.junit.Assert;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.io.Closeable;
+import java.io.IOException;
+
 @LargeTest
 @RunWith(AndroidJUnit4.class)
 public class TaplTestsPersistentTaskbar extends AbstractTaplTestsTaskbar {
@@ -39,4 +49,29 @@
         // Width check is performed inside TAPL whenever getTaskbar() is called.
         getTaskbar();
     }
+
+    @Test
+    @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) // b/320490387
+    @NavigationModeSwitch(mode = NavigationModeSwitchRule.Mode.THREE_BUTTON)
+    public void testThreeButtonsTaskbarBoundsAfterConfigChangeDuringIme() {
+        // Start off in light mode.
+        try (Closeable c = InstrumentationRegistry.getInstrumentation().getUiAutomation()
+                .executeShellCommand("cmd uimode night no")) {
+            Rect taskbarBoundsBefore = getTaskbar().getVisibleBounds();
+            startImeTestActivity();
+            // IME should stash the taskbar, which hides icons even in 3 button mode.
+            mLauncher.getLaunchedAppState().assertTaskbarHidden();
+            // Switch to dark mode (any configuration change here would do).
+            InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(
+                    "cmd uimode night yes").close();
+            // Close IME to check new taskbar bounds.
+            mLauncher.pressBack();
+            Rect taskbarBoundsAfter = getTaskbar().getVisibleBounds();
+            Assert.assertEquals(
+                    "Taskbar bounds are not the same after a configuration change while stashed.",
+                    taskbarBoundsBefore, taskbarBoundsAfter);
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
 }
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
index 0bcdb19..1b8866a 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
@@ -28,7 +28,6 @@
 
 import android.content.Intent;
 import android.content.res.Configuration;
-import android.platform.test.annotations.PlatinumTest;
 
 import androidx.test.filters.LargeTest;
 import androidx.test.platform.app.InstrumentationRegistry;
@@ -44,6 +43,7 @@
 import com.android.launcher3.tapl.Overview;
 import com.android.launcher3.tapl.OverviewActions;
 import com.android.launcher3.tapl.OverviewTask;
+import com.android.launcher3.tapl.SelectModeButtons;
 import com.android.launcher3.ui.AbstractLauncherUiTest;
 import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape;
 import com.android.launcher3.util.Wait;
@@ -112,7 +112,6 @@
 
     @Test
     @PortraitLandscape
-    @PlatinumTest(focusArea = "launcher")
     public void testOverview() throws Exception {
         startTestAppsWithCheck();
         // mLauncher.pressHome() also tests an important case of pressing home while in background.
@@ -194,6 +193,43 @@
         actionsView.clickAndDismissScreenshot();
     }
 
+    @Test
+    public void testDismissOverviewWithEscKey() throws Exception {
+        startTestAppsWithCheck();
+        final Overview overview = mLauncher.goHome().switchToOverview();
+        assertTrue("Launcher internal state is not Overview",
+                isInState(() -> LauncherState.OVERVIEW));
+
+        overview.dismissByEscKey();
+        assertTrue("Launcher internal state is not Home",
+                isInState(() -> LauncherState.NORMAL));
+    }
+
+    @Test
+    public void testDismissModalTaskAndOverviewWithEscKey() throws Exception {
+        startTestAppsWithCheck();
+        final Overview overview = mLauncher.goHome().switchToOverview();
+
+        final SelectModeButtons selectModeButtons;
+
+        if (mLauncher.isTablet() && mLauncher.isGridOnlyOverviewEnabled()) {
+            selectModeButtons = overview.getCurrentTask().tapMenu().tapSelectMenuItem();
+        } else {
+            selectModeButtons = overview.getOverviewActions().clickSelect();
+        }
+
+        assertTrue("Launcher internal state is not Overview Modal Task",
+                isInState(() -> LauncherState.OVERVIEW_MODAL_TASK));
+
+        selectModeButtons.dismissByEscKey();
+
+        assertTrue("Launcher internal state is not Overview",
+                isInState(() -> LauncherState.OVERVIEW));
+        overview.dismissByEscKey();
+        assertTrue("Launcher internal state is not Home",
+                isInState(() -> LauncherState.NORMAL));
+    }
+
     private int getCurrentOverviewPage(Launcher launcher) {
         return launcher.<RecentsView>getOverviewPanel().getCurrentPage();
     }
@@ -210,11 +246,12 @@
         return launcher.<RecentsView>getOverviewPanel().getBottomRowTaskCountForTablet();
     }
 
-    @Ignore
+    // Staging; will be promoted to presubmit if stable
+    @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT)
+
     @Test
     @NavigationModeSwitch
     @PortraitLandscape
-    @ScreenRecord // b/238461765
     public void testSwitchToOverview() throws Exception {
         startTestAppsWithCheck();
         assertNotNull("Workspace.switchToOverview() returned null",
@@ -236,7 +273,9 @@
         }
     }
 
-    @Ignore
+    // Staging; will be promoted to presubmit if stable
+    @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT)
+
     @Test
     @NavigationModeSwitch
     @PortraitLandscape
@@ -295,7 +334,9 @@
 
     @Test
     @TaskbarModeSwitch
-    @Ignore // b/314873201
+    @ScreenRecord // b/314873201
+    // Staging; will be promoted to presubmit if stable
+    @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT)
     public void testQuickSwitchToPreviousAppForTablet() throws Exception {
         assumeTrue(mLauncher.isTablet());
         startTestActivity(2);
@@ -355,22 +396,20 @@
     public void testPressBack() throws Exception {
         InstrumentationRegistry.getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(
                 READ_DEVICE_CONFIG_PERMISSION);
+        // Debug if we need to goHome to prevent wrong previous state b/315525621
+        mLauncher.goHome();
         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(CALCULATOR_APP_PACKAGE);
-        mLauncher.pressBack();
-        mLauncher.getWorkspace();
+        mLauncher.getLaunchedAppState().pressBackToWorkspace();
         waitForState("Launcher internal state didn't switch to Home", () -> LauncherState.NORMAL);
     }
 
     @Test
     @PortraitLandscape
     @TaskbarModeSwitch()
-    @PlatinumTest(focusArea = "launcher")
     @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) // b/309820115
     @ScreenRecord // b/309820115
     public void testOverviewForTablet() throws Exception {
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/quickstep/tests/src/com/android/quickstep/TaplTestsTransientTaskbar.java b/quickstep/tests/src/com/android/quickstep/TaplTestsTransientTaskbar.java
index 7109bbf..38d6046 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsTransientTaskbar.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsTransientTaskbar.java
@@ -25,6 +25,7 @@
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape;
+import com.android.launcher3.util.rule.ScreenRecordRule.ScreenRecord;
 import com.android.quickstep.TaskbarModeSwitchRule.TaskbarModeSwitch;
 
 import org.junit.Test;
@@ -69,6 +70,7 @@
     @Test
     @TaskbarModeSwitch(mode = TRANSIENT)
     @PortraitLandscape
+    @ScreenRecord // b/317798731
     public void testSwipeToStashAndUnstash() {
         getTaskbar().swipeDownToStash();
         mLauncher.getLaunchedAppState().swipeUpToUnstashTaskbar();
diff --git a/quickstep/tests/src/com/android/quickstep/taskbar/controllers/TaskbarPinningControllerTest.kt b/quickstep/tests/src/com/android/quickstep/taskbar/controllers/TaskbarPinningControllerTest.kt
new file mode 100644
index 0000000..dbe4624
--- /dev/null
+++ b/quickstep/tests/src/com/android/quickstep/taskbar/controllers/TaskbarPinningControllerTest.kt
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.taskbar.controllers
+
+import android.animation.AnimatorSet
+import android.animation.ObjectAnimator
+import android.view.View
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.LauncherPrefs
+import com.android.launcher3.LauncherPrefs.Companion.TASKBAR_PINNING
+import com.android.launcher3.logging.StatsLogManager
+import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_DIVIDER_MENU_CLOSE
+import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_DIVIDER_MENU_OPEN
+import com.android.launcher3.taskbar.TaskbarActivityContext
+import com.android.launcher3.taskbar.TaskbarBaseTestCase
+import com.android.launcher3.taskbar.TaskbarDividerPopupView
+import com.android.launcher3.taskbar.TaskbarDragLayer
+import com.android.launcher3.taskbar.TaskbarPinningController
+import com.android.launcher3.taskbar.TaskbarPinningController.Companion.PINNING_PERSISTENT
+import com.android.launcher3.taskbar.TaskbarPinningController.Companion.PINNING_TRANSIENT
+import com.android.launcher3.taskbar.TaskbarSharedState
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.doNothing
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class TaskbarPinningControllerTest : TaskbarBaseTestCase() {
+    private val taskbarDragLayer = mock<TaskbarDragLayer>()
+    private val taskbarSharedState = mock<TaskbarSharedState>()
+    private val launcherPrefs = mock<LauncherPrefs> { on { get(TASKBAR_PINNING) } doReturn false }
+    private val statsLogger = mock<StatsLogManager.StatsLogger>()
+    private val statsLogManager = mock<StatsLogManager> { on { logger() } doReturn statsLogger }
+    private lateinit var pinningController: TaskbarPinningController
+
+    @Before
+    override fun setup() {
+        super.setup()
+        whenever(taskbarActivityContext.launcherPrefs).thenReturn(launcherPrefs)
+        whenever(taskbarActivityContext.dragLayer).thenReturn(taskbarDragLayer)
+        whenever(taskbarActivityContext.statsLogManager).thenReturn(statsLogManager)
+        pinningController = spy(TaskbarPinningController(taskbarActivityContext))
+        pinningController.init(taskbarControllers, taskbarSharedState)
+    }
+
+    @Test
+    fun testOnCloseCallback_whenClosingPopupView_shouldLogStatsForClosingPopupMenu() {
+        pinningController.onCloseCallback(false)
+        verify(statsLogger, times(1)).log(LAUNCHER_TASKBAR_DIVIDER_MENU_CLOSE)
+    }
+
+    @Test
+    fun testOnCloseCallback_whenClosingPopupView_shouldPostVisibilityChangedToDragLayer() {
+        val argumentCaptor = argumentCaptor<Runnable>()
+        pinningController.onCloseCallback(false)
+        verify(taskbarDragLayer, times(1)).post(argumentCaptor.capture())
+
+        val runnable = argumentCaptor.lastValue
+        assertThat(runnable).isNotNull()
+
+        runnable.run()
+        verify(taskbarActivityContext, times(1)).onPopupVisibilityChanged(false)
+    }
+
+    @Test
+    fun testOnCloseCallback_whenPreferenceUnchanged_shouldNotAnimateTaskbarPinning() {
+        pinningController.onCloseCallback(false)
+        verify(taskbarSharedState, never()).taskbarWasPinned = true
+        verify(pinningController, never()).animateTaskbarPinning(any())
+    }
+
+    @Test
+    fun testOnCloseCallback_whenPreferenceChanged_shouldAnimateToPinnedTaskbar() {
+        whenever(launcherPrefs.get(TASKBAR_PINNING)).thenReturn(false)
+        doNothing().whenever(pinningController).animateTaskbarPinning(any())
+
+        pinningController.onCloseCallback(true)
+
+        verify(taskbarSharedState, times(1)).taskbarWasPinned = false
+        verify(pinningController, times(1)).animateTaskbarPinning(PINNING_PERSISTENT)
+    }
+
+    @Test
+    fun testOnCloseCallback_whenPreferenceChanged_shouldAnimateToTransientTaskbar() {
+        whenever(launcherPrefs.get(TASKBAR_PINNING)).thenReturn(true)
+        doNothing().whenever(pinningController).animateTaskbarPinning(any())
+
+        pinningController.onCloseCallback(true)
+
+        verify(taskbarSharedState, times(1)).taskbarWasPinned = true
+        verify(pinningController, times(1)).animateTaskbarPinning(PINNING_TRANSIENT)
+    }
+
+    @Test
+    fun testShowPinningView_whenShowingPinningView_shouldSetTaskbarWindowFullscreenAndPostRunnableToView() {
+        val popupView =
+            mock<TaskbarDividerPopupView<TaskbarActivityContext>> {
+                on { requestFocus() } doReturn true
+            }
+        val view = mock<View>()
+        val argumentCaptor = argumentCaptor<Runnable>()
+        doReturn(popupView).whenever(pinningController).getPopupView(view)
+
+        pinningController.showPinningView(view)
+
+        verify(view, times(1)).post(argumentCaptor.capture())
+
+        val runnable = argumentCaptor.lastValue
+        assertThat(runnable).isNotNull()
+        runnable.run()
+
+        verify(pinningController, times(1)).getPopupView(view)
+        verify(popupView, times(1)).requestFocus()
+        verify(popupView, times(1)).onCloseCallback = any()
+        verify(taskbarActivityContext, times(1)).onPopupVisibilityChanged(true)
+        verify(popupView, times(1)).show()
+        verify(statsLogger, times(1)).log(LAUNCHER_TASKBAR_DIVIDER_MENU_OPEN)
+    }
+
+    @Test
+    fun testAnimateTaskbarPinning_whenAnimationEnds_shouldInvokeCallbackDoOnEnd() {
+        val animatorSet = spy(AnimatorSet())
+        doReturn(animatorSet)
+            .whenever(pinningController)
+            .getAnimatorSetForTaskbarPinningAnimation(PINNING_PERSISTENT)
+        doNothing().whenever(animatorSet).start()
+        pinningController.animateTaskbarPinning(PINNING_PERSISTENT)
+        animatorSet.listeners[0].onAnimationEnd(ObjectAnimator())
+        verify(pinningController, times(1)).recreateTaskbarAndUpdatePinningValue()
+    }
+
+    @Test
+    fun testAnimateTaskbarPinning_whenAnimatingToPersistentTaskbar_shouldAnimateToPinnedTaskbar() {
+        val animatorSet = spy(AnimatorSet())
+        doReturn(animatorSet)
+            .whenever(pinningController)
+            .getAnimatorSetForTaskbarPinningAnimation(PINNING_PERSISTENT)
+        doNothing().whenever(animatorSet).start()
+        pinningController.animateTaskbarPinning(PINNING_PERSISTENT)
+
+        verify(taskbarOverlayController, times(1)).hideWindow()
+        verify(pinningController, times(1))
+            .getAnimatorSetForTaskbarPinningAnimation(PINNING_PERSISTENT)
+        verify(taskbarViewController, times(1))
+            .animateAwayNotificationDotsDuringTaskbarPinningAnimation()
+        verify(taskbarDragLayer, times(1)).setAnimatingTaskbarPinning(true)
+        assertThat(pinningController.isAnimatingTaskbarPinning).isTrue()
+        assertThat(animatorSet.listeners).isNotNull()
+    }
+
+    @Test
+    fun testAnimateTaskbarPinning_whenAnimatingToTransientTaskbar_shouldAnimateToTransientTaskbar() {
+        val animatorSet = spy(AnimatorSet())
+        doReturn(animatorSet)
+            .whenever(pinningController)
+            .getAnimatorSetForTaskbarPinningAnimation(PINNING_TRANSIENT)
+        doNothing().whenever(animatorSet).start()
+        pinningController.animateTaskbarPinning(PINNING_TRANSIENT)
+
+        verify(taskbarOverlayController, times(1)).hideWindow()
+        verify(pinningController, times(1))
+            .getAnimatorSetForTaskbarPinningAnimation(PINNING_TRANSIENT)
+        verify(taskbarDragLayer, times(1)).setAnimatingTaskbarPinning(true)
+        assertThat(pinningController.isAnimatingTaskbarPinning).isTrue()
+        verify(taskbarViewController, times(1))
+            .animateAwayNotificationDotsDuringTaskbarPinningAnimation()
+        assertThat(animatorSet.listeners).isNotNull()
+    }
+
+    @Test
+    fun testRecreateTaskbarAndUpdatePinningValue_whenAnimationEnds_shouldUpdateTaskbarPinningLauncherPref() {
+        pinningController.recreateTaskbarAndUpdatePinningValue()
+        verify(taskbarDragLayer, times(1)).setAnimatingTaskbarPinning(false)
+        assertThat(pinningController.isAnimatingTaskbarPinning).isFalse()
+        verify(launcherPrefs, times(1)).put(TASKBAR_PINNING, true)
+    }
+}
diff --git a/quickstep/tests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt b/quickstep/tests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt
index 50803fe..de152fa 100644
--- a/quickstep/tests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt
+++ b/quickstep/tests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt
@@ -19,8 +19,15 @@
 
 import android.graphics.Bitmap
 import android.graphics.drawable.Drawable
+import android.view.ContextThemeWrapper
+import android.view.SurfaceControl.Transaction
 import android.view.View
+import android.window.TransitionInfo
 import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.launcher3.apppairs.AppPairIcon
+import com.android.launcher3.statehandlers.DepthController
+import com.android.launcher3.statemanager.StateManager
+import com.android.launcher3.taskbar.TaskbarActivityContext
 import com.android.launcher3.util.SplitConfigurationOptions
 import com.android.quickstep.views.GroupedTaskView
 import com.android.quickstep.views.IconView
@@ -32,13 +39,18 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doNothing
 import org.mockito.kotlin.mock
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.verify
 import org.mockito.kotlin.whenever
 
 @RunWith(AndroidJUnit4::class)
 class SplitAnimationControllerTest {
 
     private val taskId = 9
+    private val taskId2 = 10
 
     private val mockSplitSelectStateController: SplitSelectStateController = mock()
     // TaskView
@@ -52,12 +64,21 @@
     private val mockTask: Task = mock()
     private val mockTaskKey: Task.TaskKey = mock()
     private val mockTaskIdAttributeContainer: TaskIdAttributeContainer = mock()
+    // AppPairIcon
+    private val mockAppPairIcon: AppPairIcon = mock()
+    private val mockContextThemeWrapper: ContextThemeWrapper = mock()
+    private val mockTaskbarActivityContext: TaskbarActivityContext = mock()
 
     // SplitSelectSource
     private val splitSelectSource: SplitConfigurationOptions.SplitSelectSource = mock()
     private val mockSplitSourceDrawable: Drawable = mock()
     private val mockSplitSourceView: View = mock()
 
+    private val stateManager: StateManager<*> = mock()
+    private val depthController: DepthController = mock()
+    private val transitionInfo: TransitionInfo = mock()
+    private val transaction: Transaction = mock()
+
     lateinit var splitAnimationController: SplitAnimationController
 
     @Before
@@ -172,4 +193,138 @@
             splitAnimInitProps.iconDrawable
         )
     }
+
+    @Test
+    fun playsAppropriateSplitLaunchAnimation_playsLegacyLaunchCorrectly() {
+        val spySplitAnimationController = spy(splitAnimationController)
+        doNothing()
+            .whenever(spySplitAnimationController)
+            .composeRecentsSplitLaunchAnimatorLegacy(
+                any(), any(), any(), any(), any(), any(), any(), any(), any())
+
+        spySplitAnimationController.playSplitLaunchAnimation(
+            mockGroupedTaskView,
+            null /* launchingIconView */,
+            taskId,
+            taskId2,
+            arrayOf() /* apps */,
+            arrayOf() /* wallpapers */,
+            arrayOf() /* nonApps */,
+            stateManager,
+            depthController,
+            null /* info */,
+            null /* t */,
+            {} /* finishCallback */
+        )
+
+        verify(spySplitAnimationController)
+            .composeRecentsSplitLaunchAnimatorLegacy(
+                any(), any(), any(), any(), any(), any(), any(), any(), any())
+    }
+
+    @Test
+    fun playsAppropriateSplitLaunchAnimation_playsRecentsLaunchCorrectly() {
+        val spySplitAnimationController = spy(splitAnimationController)
+        doNothing()
+            .whenever(spySplitAnimationController)
+            .composeRecentsSplitLaunchAnimator(any(), any(), any(), any(), any(), any())
+
+        spySplitAnimationController.playSplitLaunchAnimation(
+            mockGroupedTaskView,
+            null /* launchingIconView */,
+            taskId,
+            taskId2,
+            null /* apps */,
+            null /* wallpapers */,
+            null /* nonApps */,
+            stateManager,
+            depthController,
+            transitionInfo,
+            transaction,
+            {} /* finishCallback */
+        )
+
+        verify(spySplitAnimationController)
+            .composeRecentsSplitLaunchAnimator(any(), any(), any(), any(), any(), any())
+    }
+
+    @Test
+    fun playsAppropriateSplitLaunchAnimation_playsIconLaunchCorrectly() {
+        val spySplitAnimationController = spy(splitAnimationController)
+        whenever(mockAppPairIcon.context).thenReturn(mockContextThemeWrapper)
+        doNothing()
+            .whenever(spySplitAnimationController)
+            .composeIconSplitLaunchAnimator(any(), any(), any(), any())
+
+        spySplitAnimationController.playSplitLaunchAnimation(
+            null /* launchingTaskView */,
+            mockAppPairIcon,
+            taskId,
+            taskId2,
+            null /* apps */,
+            null /* wallpapers */,
+            null /* nonApps */,
+            stateManager,
+            depthController,
+            transitionInfo,
+            transaction,
+            {} /* finishCallback */
+        )
+
+        verify(spySplitAnimationController)
+            .composeIconSplitLaunchAnimator(any(), any(), any(), any())
+    }
+
+    @Test
+    fun playsAppropriateSplitLaunchAnimation_playsIconLaunchFromTaskbarContextCorrectly() {
+        val spySplitAnimationController = spy(splitAnimationController)
+        whenever(mockAppPairIcon.context).thenReturn(mockTaskbarActivityContext)
+        doNothing()
+            .whenever(spySplitAnimationController)
+            .composeFadeInSplitLaunchAnimator(any(), any(), any(), any(), any())
+
+        spySplitAnimationController.playSplitLaunchAnimation(
+            null /* launchingTaskView */,
+            mockAppPairIcon,
+            taskId,
+            taskId2,
+            null /* apps */,
+            null /* wallpapers */,
+            null /* nonApps */,
+            stateManager,
+            depthController,
+            transitionInfo,
+            transaction,
+            {} /* finishCallback */
+        )
+
+        verify(spySplitAnimationController)
+            .composeFadeInSplitLaunchAnimator(any(), any(), any(), any(), any())
+    }
+
+    @Test
+    fun playsAppropriateSplitLaunchAnimation_playsFadeInLaunchCorrectly() {
+        val spySplitAnimationController = spy(splitAnimationController)
+        doNothing()
+            .whenever(spySplitAnimationController)
+            .composeFadeInSplitLaunchAnimator(any(), any(), any(), any(), any())
+
+        spySplitAnimationController.playSplitLaunchAnimation(
+            null /* launchingTaskView */,
+            null /* launchingIconView */,
+            taskId,
+            taskId2,
+            null /* apps */,
+            null /* wallpapers */,
+            null /* nonApps */,
+            stateManager,
+            depthController,
+            transitionInfo,
+            transaction,
+            {} /* finishCallback */
+        )
+
+        verify(spySplitAnimationController)
+            .composeFadeInSplitLaunchAnimator(any(), any(), any(), any(), any())
+    }
 }
diff --git a/quickstep/tests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt b/quickstep/tests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt
index f41ac48..1e39a34 100644
--- a/quickstep/tests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt
+++ b/quickstep/tests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt
@@ -107,7 +107,7 @@
         // Assertions happen in the callback we get from what we pass into
         // #findLastActiveTasksAndRunCallback
         val taskConsumer =
-            Consumer<List<Task>> { assertNull("No tasks should have matched", it[0] /*task*/) }
+            Consumer<Array<Task>> { assertNull("No tasks should have matched", it[0] /*task*/) }
 
         // Capture callback from recentsModel#getTasks()
         val consumer =
@@ -148,7 +148,7 @@
         // Assertions happen in the callback we get from what we pass into
         // #findLastActiveTasksAndRunCallback
         val taskConsumer =
-            Consumer<List<Task>> {
+            Consumer<Array<Task>> {
                 assertEquals(
                     "ComponentName package mismatched",
                     it[0].key.baseIntent.component?.packageName,
@@ -201,7 +201,7 @@
         // Assertions happen in the callback we get from what we pass into
         // #findLastActiveTasksAndRunCallback
         val taskConsumer =
-            Consumer<List<Task>> { assertNull("No tasks should have matched", it[0] /*task*/) }
+            Consumer<Array<Task>> { assertNull("No tasks should have matched", it[0] /*task*/) }
 
         // Capture callback from recentsModel#getTasks()
         val consumer =
@@ -244,7 +244,7 @@
         // Assertions happen in the callback we get from what we pass into
         // #findLastActiveTasksAndRunCallback
         val taskConsumer =
-            Consumer<List<Task>> {
+            Consumer<Array<Task>> {
                 assertEquals(
                     "ComponentName package mismatched",
                     it[0].key.baseIntent.component?.packageName,
@@ -298,7 +298,7 @@
         // Assertions happen in the callback we get from what we pass into
         // #findLastActiveTasksAndRunCallback
         val taskConsumer =
-            Consumer<List<Task>> {
+            Consumer<Array<Task>> {
                 assertEquals(
                     "ComponentName package mismatched",
                     it[0].key.baseIntent.component?.packageName,
@@ -350,7 +350,7 @@
         // Assertions happen in the callback we get from what we pass into
         // #findLastActiveTasksAndRunCallback
         val taskConsumer =
-            Consumer<List<Task>> {
+            Consumer<Array<Task>> {
                 assertEquals("Expected array length 2", 2, it.size)
                 assertNull("No tasks should have matched", it[0] /*task*/)
                 assertEquals(
@@ -403,7 +403,7 @@
         // Assertions happen in the callback we get from what we pass into
         // #findLastActiveTasksAndRunCallback
         val taskConsumer =
-            Consumer<List<Task>> {
+            Consumer<Array<Task>> {
                 assertEquals("Expected array length 2", 2, it.size)
                 assertEquals(
                     "ComponentName package mismatched",
@@ -459,7 +459,7 @@
         // Assertions happen in the callback we get from what we pass into
         // #findLastActiveTasksAndRunCallback
         val taskConsumer =
-            Consumer<List<Task>> {
+            Consumer<Array<Task>> {
                 assertEquals("Expected array length 2", 2, it.size)
                 assertEquals(
                     "ComponentName package mismatched",
@@ -532,8 +532,8 @@
         // Assertions happen in the callback we get from what we pass into
         // #findLastActiveTasksAndRunCallback
         val taskConsumer =
-            Consumer<List<Task>> {
-                assertEquals("Expected array length 1", 1, it.size)
+            Consumer<Array<Task>> {
+                assertEquals("Expected array length 2", 2, it.size)
                 assertEquals("Found wrong task", it[0], groupTask2.task1)
             }
 
diff --git a/res/color-night-v31/folder_preview_dark.xml b/res/color-night-v31/folder_preview_dark.xml
index 644d61a..6dd20a1 100644
--- a/res/color-night-v31/folder_preview_dark.xml
+++ b/res/color-night-v31/folder_preview_dark.xml
@@ -16,5 +16,5 @@
 <selector xmlns:android="http://schemas.android.com/apk/res/android" >
     <item
         android:color="@android:color/system_neutral1_900"
-        android:lStar="17" />
+        android:lStar="12" />
 </selector>
diff --git a/res/drawable-v31/bg_deferred_app_widget.xml b/res/drawable-v31/bg_deferred_app_widget.xml
deleted file mode 100644
index a08998d..0000000
--- a/res/drawable-v31/bg_deferred_app_widget.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  Copyright (C) 2021 The Android Open Source Project
-
-  Licensed under the Apache License, Version 2.0 (the "License");
-  you may not use this file except in compliance with the License.
-  You may obtain a copy of the License at
-
-       http://www.apache.org/licenses/LICENSE-2.0
-
-  Unless required by applicable law or agreed to in writing, software
-  distributed under the License is distributed on an "AS IS" BASIS,
-  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  See the License for the specific language governing permissions and
-  limitations under the License.
--->
-<inset xmlns:android="http://schemas.android.com/apk/res/android"
-    android:inset="8dp">
-    <shape android:shape="rectangle">
-        <corners android:radius="@android:dimen/system_app_widget_background_radius" />
-        <solid android:color="#77000000" />
-    </shape>
-</inset>
diff --git a/res/drawable/bg_deferred_app_widget.xml b/res/drawable/bg_deferred_app_widget.xml
deleted file mode 100644
index 07bae48..0000000
--- a/res/drawable/bg_deferred_app_widget.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
-**
-** Copyright 2015, 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.
-*/
--->
-
-<inset
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:inset="8dp">
-    <color android:color="#77000000" />
-</inset>
diff --git a/res/drawable/bg_rounded_corner_bottom_sheet_handle.xml b/res/drawable/bg_rounded_corner_bottom_sheet_handle.xml
index c502178..379e0e5 100644
--- a/res/drawable/bg_rounded_corner_bottom_sheet_handle.xml
+++ b/res/drawable/bg_rounded_corner_bottom_sheet_handle.xml
@@ -17,6 +17,6 @@
 <shape xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
     android:shape="rectangle" >
-    <solid android:color="?androidprv:attr/colorSurfaceVariant"/>
+    <solid android:color="?androidprv:attr/materialColorOutlineVariant"/>
     <corners android:radius="@dimen/bottom_sheet_handle_corner_radius" />
 </shape>
diff --git a/res/drawable/ic_install_to_private.xml b/res/drawable/ic_install_to_private.xml
new file mode 100644
index 0000000..7f00f8d
--- /dev/null
+++ b/res/drawable/ic_install_to_private.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2024 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.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="960"
+    android:viewportHeight="960"
+    android:tint="?attr/colorControlNormal">
+
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M420,600L540,600L517,471Q537,461 548.5,442Q560,423 560,400Q560,367 536.5,343.5Q513,320 480,320Q447,320 423.5,343.5Q400,367 400,400Q400,423 411.5,442Q423,461 443,471L420,600ZM480,880Q341,845 250.5,720.5Q160,596 160,444L160,200L480,80L800,200L800,444Q800,596 709.5,720.5Q619,845 480,880ZM480,796Q584,763 652,664Q720,565 720,444L720,255L480,165L240,255L240,444Q240,565 308,664Q376,763 480,796ZM480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480Z"/>
+</vector>
diff --git a/res/drawable/private_space_install_app_icon.xml b/res/drawable/private_space_install_app_icon.xml
new file mode 100644
index 0000000..4c167ba
--- /dev/null
+++ b/res/drawable/private_space_install_app_icon.xml
@@ -0,0 +1,31 @@
+<!--
+  ~ Copyright (C) 2024 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.
+  -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="60dp"
+    android:height="60dp"
+    android:viewportWidth="60"
+    android:viewportHeight="60">
+    <group>
+        <clip-path
+            android:pathData="M30 0H30A30 30 0 0 1 60 30V30A30 30 0 0 1 30 60H30A30 30 0 0 1 0 30V30A30 30 0 0 1 30 0Z" />
+        <path
+            android:pathData="M30 0H30A30 30 0 0 1 60 30V30A30 30 0 0 1 30 60H30A30 30 0 0 1 0 30V30A30 30 0 0 1 30 0Z"
+            android:fillColor="@color/material_color_surface_bright" />
+        <path
+            android:pathData="M29 31h-6v-2h6v-6h2v6h6v2h-6v6h-2v-6Z"
+            android:fillColor="@color/material_color_on_surface_variant" />
+    </group>
+</vector>
diff --git a/res/drawable/widget_internal_focus_bg.xml b/res/drawable/widget_internal_focus_bg.xml
index 4d4bea6..b1f45a4 100644
--- a/res/drawable/widget_internal_focus_bg.xml
+++ b/res/drawable/widget_internal_focus_bg.xml
@@ -23,6 +23,7 @@
     <item android:state_selected="true">
         <shape android:shape="rectangle">
             <stroke android:color="#fff" android:width="2dp" />
+            <corners android:radius="@dimen/focus_outline_radius" />
         </shape>
     </item>
 </selector>
\ No newline at end of file
diff --git a/res/layout/app_pair_icon.xml b/res/layout/app_pair_icon.xml
index 2b9a98b..4e2dd58 100644
--- a/res/layout/app_pair_icon.xml
+++ b/res/layout/app_pair_icon.xml
@@ -20,6 +20,11 @@
     android:layout_height="match_parent"
     android:orientation="vertical"
     android:focusable="true" >
+    <com.android.launcher3.apppairs.AppPairIconGraphic
+        android:id="@+id/app_pair_icon_graphic"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:focusable="false" />
     <com.android.launcher3.views.DoubleShadowBubbleTextView
         style="@style/BaseIcon.Workspace"
         android:id="@+id/app_pair_icon_name"
diff --git a/res/layout/floating_app_pair_view.xml b/res/layout/floating_app_pair_view.xml
new file mode 100644
index 0000000..88ec655
--- /dev/null
+++ b/res/layout/floating_app_pair_view.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<com.android.quickstep.views.FloatingAppPairView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+</com.android.quickstep.views.FloatingAppPairView>
\ No newline at end of file
diff --git a/res/layout/folder_icon.xml b/res/layout/folder_icon.xml
index 4093744..6af346e 100644
--- a/res/layout/folder_icon.xml
+++ b/res/layout/folder_icon.xml
@@ -19,7 +19,8 @@
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:orientation="vertical"
-    android:focusable="true" >
+    android:focusable="true"
+    android:defaultFocusHighlightEnabled="false">
     <com.android.launcher3.views.DoubleShadowBubbleTextView
         style="@style/BaseIcon.Workspace"
         android:id="@+id/folder_icon_name"
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/layout/work_apps_edu.xml b/res/layout/work_apps_edu.xml
index eeb7f4f..f557fb6 100644
--- a/res/layout/work_apps_edu.xml
+++ b/res/layout/work_apps_edu.xml
@@ -19,7 +19,7 @@
     android:paddingTop="@dimen/work_edu_card_margin"
     android:paddingBottom="@dimen/work_edu_card_bottom_margin"
     android:gravity="center">
-    <RelativeLayout
+    <LinearLayout
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:orientation="horizontal"
@@ -28,40 +28,33 @@
         android:paddingEnd="@dimen/work_card_margin"
         android:paddingStart="@dimen/work_card_margin"
         android:paddingTop="@dimen/work_card_margin"
+        android:paddingBottom="@dimen/work_card_margin"
         android:id="@+id/wrapper">
         <TextView
             style="@style/PrimaryHeadline"
             android:textColor="?android:attr/textColorPrimary"
             android:id="@+id/work_apps_paused_title"
-            android:layout_width="wrap_content"
+            android:layout_width="0dp"
             android:layout_height="wrap_content"
-            android:layout_marginBottom="@dimen/work_card_margin"
-            android:layout_marginEnd="@dimen/work_card_margin"
+            android:layout_weight="1"
+            android:paddingEnd="@dimen/work_edu_card_text_end_margin"
             android:text="@string/work_profile_edu_work_apps"
             android:textDirection="locale"
             android:textSize="18sp" />
-        <RelativeLayout
-            android:layout_width="match_parent"
-            android:layout_height="@dimen/padded_rounded_button_height"
-            android:orientation="horizontal">
-            <FrameLayout
-                android:layout_width="@dimen/rounded_button_width"
-                android:layout_height="@dimen/rounded_button_width"
-                android:layout_alignParentEnd="true"
-                android:background="@drawable/rounded_action_button"
-                android:padding="@dimen/rounded_button_padding">
-                <ImageButton
-                    android:id="@+id/action_btn"
-                    android:layout_width="@dimen/x_icon_size"
-                    android:layout_height="@dimen/x_icon_size"
-                    android:layout_gravity="center"
-                    android:padding="@dimen/x_icon_padding"
-                    android:contentDescription="@string/accessibility_close"
-                    android:src="@drawable/ic_remove_no_shadow" />
-            </FrameLayout>
-        </RelativeLayout>
-    </RelativeLayout>
-
-
-
-</com.android.launcher3.allapps.WorkEduCard>
\ No newline at end of file
+        <FrameLayout
+            android:layout_width="@dimen/rounded_button_width"
+            android:layout_height="@dimen/rounded_button_width"
+            android:background="@drawable/rounded_action_button"
+            android:padding="@dimen/rounded_button_padding">
+            <ImageButton
+                android:id="@+id/action_btn"
+                android:layout_width="@dimen/x_icon_size"
+                android:layout_height="@dimen/x_icon_size"
+                android:layout_gravity="center"
+                android:contentDescription="@string/accessibility_close"
+                android:padding="@dimen/x_icon_padding"
+                android:background="@android:color/transparent"
+                android:src="@drawable/ic_remove_no_shadow" />
+        </FrameLayout>
+    </LinearLayout>
+</com.android.launcher3.allapps.WorkEduCard>
diff --git a/res/values-af/strings.xml b/res/values-af/strings.xml
index ea6e0fa..44c29ec 100644
--- a/res/values-af/strings.xml
+++ b/res/values-af/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"Privaat Ruimte-instellings"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"Sluit/ontsluit Privaat Ruimte"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"Privaat Ruimte-oorgang"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"Oorvloei"</string>
 </resources>
diff --git a/res/values-am/strings.xml b/res/values-am/strings.xml
index 893f4c4..b56c79a 100644
--- a/res/values-am/strings.xml
+++ b/res/values-am/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"የግል ቦታ ቅንብሮች"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"የግል ቦታን ቆልፍ/ክፈት"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"የግል ቦታ ሽግግር"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"ትርፍ ፍሰት"</string>
 </resources>
diff --git a/res/values-ar/strings.xml b/res/values-ar/strings.xml
index a2a43db..1394661 100644
--- a/res/values-ar/strings.xml
+++ b/res/values-ar/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"إعدادات المساحة الخاصة"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"قفل المساحة الخاصة أو فتح قفلها"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"النقل إلى المساحة الخاصة"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"القائمة الكاملة"</string>
 </resources>
diff --git a/res/values-as/strings.xml b/res/values-as/strings.xml
index 93c5e3a..9deda79 100644
--- a/res/values-as/strings.xml
+++ b/res/values-as/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"ব্যক্তিগত স্পে’চৰ ছেটিং"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"ব্যক্তিগত স্পে’চ লক/আনলক কৰক"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"ব্যক্তিগত স্পে’চৰ স্থানান্তৰণ"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"অ’ভাৰফ্ল’"</string>
 </resources>
diff --git a/res/values-az/strings.xml b/res/values-az/strings.xml
index 31d485d..971163e 100644
--- a/res/values-az/strings.xml
+++ b/res/values-az/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"Şəxsi məkan ayarları"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"Şəxsi məkanı kilidləyin/kiliddən çıxarın"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"Şəxsi məkana keçid"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"Kənara çıxma"</string>
 </resources>
diff --git a/res/values-b+sr+Latn/strings.xml b/res/values-b+sr+Latn/strings.xml
index aa08cc4..c13989c 100644
--- a/res/values-b+sr+Latn/strings.xml
+++ b/res/values-b+sr+Latn/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"Podešavanja privatnog prostora"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"Zaključaj/otključaj privatni prostor"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"Prenos privatnog prostora"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"Preklopno"</string>
 </resources>
diff --git a/res/values-be/strings.xml b/res/values-be/strings.xml
index ae47fa0..31709c0 100644
--- a/res/values-be/strings.xml
+++ b/res/values-be/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"Налады прыватнай вобласці"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"Заблакіраваць (разблакіраваць) прыватную вобласць"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"Пераход у прыватную вобласць"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"Дадатковае меню"</string>
 </resources>
diff --git a/res/values-bg/strings.xml b/res/values-bg/strings.xml
index 2f196ca..01ee2df 100644
--- a/res/values-bg/strings.xml
+++ b/res/values-bg/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"Настройки за личното пространство"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"Заключване/отключване на личното пространство"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"Преминаване към личното пространство"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"Препълване"</string>
 </resources>
diff --git a/res/values-bn/strings.xml b/res/values-bn/strings.xml
index 8c08794..665a95f 100644
--- a/res/values-bn/strings.xml
+++ b/res/values-bn/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"ব্যক্তিগত স্পেসের সেটিংস"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"ব্যক্তিগত স্পেস লক/আনলক করুন"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"ব্যক্তিগত স্পেস ট্রানজিট করা"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"ওভারফ্লো"</string>
 </resources>
diff --git a/res/values-bs/strings.xml b/res/values-bs/strings.xml
index 9641416..f0ee1b4 100644
--- a/res/values-bs/strings.xml
+++ b/res/values-bs/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"Postavke privatnog prostora"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"Zaključavanje/otključavanje privatnog prostora"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"Prelazak u privatan prostor"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"Preklopni meni"</string>
 </resources>
diff --git a/res/values-ca/strings.xml b/res/values-ca/strings.xml
index 25a8ce6..e1c9a2b 100644
--- a/res/values-ca/strings.xml
+++ b/res/values-ca/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"Configuració d\'Espai privat"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"Bloqueja o desbloqueja Espai privat"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"Canvia a Espai privat"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"Menú addicional"</string>
 </resources>
diff --git a/res/values-cs/strings.xml b/res/values-cs/strings.xml
index 4927508..7a5e838 100644
--- a/res/values-cs/strings.xml
+++ b/res/values-cs/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"Nastavení soukromého prostoru"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"Zamknout/odemknout soukromý prostor"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"Převádění soukromého prostoru"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"Rozbalovací nabídka"</string>
 </resources>
diff --git a/res/values-da/strings.xml b/res/values-da/strings.xml
index 9d5a55f..9c46dd9 100644
--- a/res/values-da/strings.xml
+++ b/res/values-da/strings.xml
@@ -173,10 +173,14 @@
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Genoptag"</string>
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Filter"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Mislykket: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
-    <string name="private_space_label" msgid="2359721649407947001">"Privat rum"</string>
+    <string name="private_space_label" msgid="2359721649407947001">"Privat område"</string>
     <string name="ps_container_title" msgid="4391796149519594205">"Privat"</string>
     <string name="ps_container_settings" msgid="6059734123353320479">"Indstillinger for privat rum"</string>
-    <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"Lås/oplås det private rum"</string>
-    <string name="ps_container_transition" msgid="8667331812048014412">"Ændringer af tilstanden for det private rum"</string>
+    <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"Lås/oplås det private område"</string>
+    <string name="ps_container_transition" msgid="8667331812048014412">"Ændringer af tilstanden for det private område"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"Overløb"</string>
 </resources>
diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml
index 04d9682..f27cb90 100644
--- a/res/values-de/strings.xml
+++ b/res/values-de/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"Einstellungen für privaten Bereich"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"Privaten Bereich sperren/entsperren"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"Sperrzustand des privaten Bereichs wird gerade geändert"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"Weitere Optionen"</string>
 </resources>
diff --git a/res/values-el/strings.xml b/res/values-el/strings.xml
index bc8458b..d4740d4 100644
--- a/res/values-el/strings.xml
+++ b/res/values-el/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"Ρυθμίσεις Ιδιωτικού χώρου"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"Κλείδωμα/Ξεκλείδωμα Ιδιωτικού χώρου"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"Μετάβαση στον Ιδιωτικό χώρο"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"Υπερχείλιση"</string>
 </resources>
diff --git a/res/values-en-rAU/strings.xml b/res/values-en-rAU/strings.xml
index c002ef9..85ab76b 100644
--- a/res/values-en-rAU/strings.xml
+++ b/res/values-en-rAU/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"Private Space Settings"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"Lock/Unlock Private Space"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"Private Space transitioning"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"Overflow"</string>
 </resources>
diff --git a/res/values-en-rCA/strings.xml b/res/values-en-rCA/strings.xml
index 1ad42f3..bb03769 100644
--- a/res/values-en-rCA/strings.xml
+++ b/res/values-en-rCA/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"Private Space Settings"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"Lock/Unlock Private Space"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"Private Space Transitioning"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"Overflow"</string>
 </resources>
diff --git a/res/values-en-rGB/strings.xml b/res/values-en-rGB/strings.xml
index c002ef9..85ab76b 100644
--- a/res/values-en-rGB/strings.xml
+++ b/res/values-en-rGB/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"Private Space Settings"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"Lock/Unlock Private Space"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"Private Space transitioning"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"Overflow"</string>
 </resources>
diff --git a/res/values-en-rIN/strings.xml b/res/values-en-rIN/strings.xml
index c002ef9..85ab76b 100644
--- a/res/values-en-rIN/strings.xml
+++ b/res/values-en-rIN/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"Private Space Settings"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"Lock/Unlock Private Space"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"Private Space transitioning"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"Overflow"</string>
 </resources>
diff --git a/res/values-en-rXC/strings.xml b/res/values-en-rXC/strings.xml
index 81bb474..ef85c5c 100644
--- a/res/values-en-rXC/strings.xml
+++ b/res/values-en-rXC/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‏‎‏‎‎‎‎‎‏‏‎‎‎‏‎‎‎‎‎‎‎‎‎‎‏‎‏‎‎‎‏‎‎‏‏‏‏‏‏‎‎‏‎‎‏‏‎‏‎‏‏‎‎‎‎‎‏‏‏‏‏‎Private Space Settings‎‏‎‎‏‎"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‎‏‎‎‏‏‎‎‎‏‏‎‎‏‎‎‎‎‏‏‏‎‎‎‎‎‏‏‎‎‏‏‎‎‏‎‎‏‏‎‏‏‎‎‏‎‎‎‎‏‎‎‎‏‎‎‎‏‎‏‏‎Lock/Unlock Private Space‎‏‎‎‏‎"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‏‏‎‎‎‎‏‎‎‏‎‎‎‏‎‎‎‏‏‎‎‎‏‎‏‏‏‎‎‏‎‎‎‏‏‏‏‏‎‎‏‎‎‎‎‏‎‎‎‏‏‎‎‎‏‎‎‏‏‎‎‎Private Space Transitioning‎‏‎‎‏‎"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‎‎‏‏‎‏‏‎‏‏‎‎‏‎‎‏‎‎‏‎‏‎‎‏‎‏‎‏‏‏‏‎‏‎‎‎‏‎‎‏‎‏‏‎‎‏‎‎‎‏‏‎‏‎‏‏‎‏‎‎‎‎Overflow‎‏‎‎‏‎"</string>
 </resources>
diff --git a/res/values-es-rUS/strings.xml b/res/values-es-rUS/strings.xml
index d37b1e9..bce0430 100644
--- a/res/values-es-rUS/strings.xml
+++ b/res/values-es-rUS/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"Configuración de Espacio privado"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"Bloquear o desbloquear Espacio privado"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"Pasar a Espacio privado"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"Ampliada"</string>
 </resources>
diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml
index 7db8239..6bb325c 100644
--- a/res/values-es/strings.xml
+++ b/res/values-es/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"Ajustes del espacio privado"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"Bloquear/Desbloquear espacio privado"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"Cambiar a espacio privado"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"Desplegable"</string>
 </resources>
diff --git a/res/values-et/strings.xml b/res/values-et/strings.xml
index 9e4a534..cabd7ab 100644
--- a/res/values-et/strings.xml
+++ b/res/values-et/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"Privaatse ruumi seaded"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"Privaatse ruumi lukustamine/avamine"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"Privaatse ruumi üleviimine"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"Ületäide"</string>
 </resources>
diff --git a/res/values-eu/strings.xml b/res/values-eu/strings.xml
index 4123096..ff68d74 100644
--- a/res/values-eu/strings.xml
+++ b/res/values-eu/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"Eremu pribatuaren ezarpenak"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"Blokeatu/Desblokeatu eremu pribatua"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"Eremu pribaturako trantsizioa"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"Luzapena"</string>
 </resources>
diff --git a/res/values-fa/strings.xml b/res/values-fa/strings.xml
index 161c7c5..444d550 100644
--- a/res/values-fa/strings.xml
+++ b/res/values-fa/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"تنظیمات «فضای خصوصی»"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"قفل/ باز کردن «فضای خصوصی»"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"انتقال «فضای خصوصی»"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"سرریز"</string>
 </resources>
diff --git a/res/values-fi/strings.xml b/res/values-fi/strings.xml
index 87e3040..d812e99 100644
--- a/res/values-fi/strings.xml
+++ b/res/values-fi/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"Yksityisen tilan asetukset"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"Lukitse yksityinen tila / avaa sen lukitus"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"Yksityisen tilan siirtäminen"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"Ylivuoto"</string>
 </resources>
diff --git a/res/values-fr-rCA/strings.xml b/res/values-fr-rCA/strings.xml
index 0e8fe9c..0fc6129 100644
--- a/res/values-fr-rCA/strings.xml
+++ b/res/values-fr-rCA/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"Paramètres de l\'Espace privé"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"Verrouiller/Déverrouiller l\'Espace privé"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"Transition vers l\'Espace privé"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"Menu à développer"</string>
 </resources>
diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml
index 9aa6ad2..fb658f5 100644
--- a/res/values-fr/strings.xml
+++ b/res/values-fr/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"Paramètres d\'Espace privé"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"Verrouiller/Déverrouiller Espace privé"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"Transition vers Espace privé"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"Dépassement"</string>
 </resources>
diff --git a/res/values-gl/strings.xml b/res/values-gl/strings.xml
index 4eda3cf..4a455f5 100644
--- a/res/values-gl/strings.xml
+++ b/res/values-gl/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"Configuración do espazo privado"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"Bloquear ou desbloquear o espazo privado"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"Transición ao espazo privado"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"Menú adicional"</string>
 </resources>
diff --git a/res/values-gu/strings.xml b/res/values-gu/strings.xml
index deaee86..be0fdba 100644
--- a/res/values-gu/strings.xml
+++ b/res/values-gu/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"ખાનગી સ્પેસના સેટિંગ"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"ખાનગી સ્પેસને લૉક/અનલૉક કરો"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"ખાનગી સ્પેસ પર સ્થાનાંતરણ"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"ઓવરફ્લો"</string>
 </resources>
diff --git a/res/values-hi/strings.xml b/res/values-hi/strings.xml
index 52bc50a..f7972eb 100644
--- a/res/values-hi/strings.xml
+++ b/res/values-hi/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"प्राइवेट स्पेस सेटिंग"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"प्राइवेट स्पेस को लॉक करें/अनलॉक करें"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"प्राइवेट स्पेस की सेटिंग में बदलाव किया जा रहा है"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"ओवरफ़्लो"</string>
 </resources>
diff --git a/res/values-hr/strings.xml b/res/values-hr/strings.xml
index 9694428..9f6ae3a 100644
--- a/res/values-hr/strings.xml
+++ b/res/values-hr/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"Postavke privatnog prostora"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"Zaključavanje/otključavanje privatnog prostora"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"Prelazak na privatni prostor"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"Dodatni izbornik"</string>
 </resources>
diff --git a/res/values-hu/strings.xml b/res/values-hu/strings.xml
index 7c83ec0..8e0fa14 100644
--- a/res/values-hu/strings.xml
+++ b/res/values-hu/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"Privát terület beállításai"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"Privát terület zárolása/zárolásának feloldása"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"Átállás privát területre…"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"Túlcsordulás"</string>
 </resources>
diff --git a/res/values-hy/strings.xml b/res/values-hy/strings.xml
index 7aa432b..c909b54 100644
--- a/res/values-hy/strings.xml
+++ b/res/values-hy/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"Անձնական տարածքի կարգավորումներ"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"Կողպել/ապակողպել անձնական տարածքը"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"Անցում անձնական տարածք"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"Լրացուցիչ ընտրացանկ"</string>
 </resources>
diff --git a/res/values-in/strings.xml b/res/values-in/strings.xml
index b7ce5e3..d7bb8dc 100644
--- a/res/values-in/strings.xml
+++ b/res/values-in/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"Setelan Ruang Pribadi"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"Kunci/Buka Kunci Ruang Pribadi"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"Ruang Pribadi Bertransisi"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"Menu tambahan"</string>
 </resources>
diff --git a/res/values-is/strings.xml b/res/values-is/strings.xml
index 4ba0623..1c9759f 100644
--- a/res/values-is/strings.xml
+++ b/res/values-is/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"Stillingar einkarýmis"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"Læsaeinkarými/taka einkarými úr lás"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"Einkarými að breytast"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"Yfirflæði"</string>
 </resources>
diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml
index 644d680..fd7c4bf 100644
--- a/res/values-it/strings.xml
+++ b/res/values-it/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"Impostazioni dello Spazio privato"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"Blocca/sblocca Spazio privato"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"Transizione dello Spazio privato in corso…"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"Extra"</string>
 </resources>
diff --git a/res/values-iw/strings.xml b/res/values-iw/strings.xml
index c91e57c..79a6e88 100644
--- a/res/values-iw/strings.xml
+++ b/res/values-iw/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"הגדרות המרחב הפרטי"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"נעילה או ביטול הנעילה של המרחב הפרטי"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"מעבר למרחב הפרטי"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"אפשרויות נוספות"</string>
 </resources>
diff --git a/res/values-ja/strings.xml b/res/values-ja/strings.xml
index 00b061f..ac63dab 100644
--- a/res/values-ja/strings.xml
+++ b/res/values-ja/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"プライベート スペースの設定"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"プライベート スペースをロック / ロック解除する"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"プライベート スペース移行中"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"オーバーフロー"</string>
 </resources>
diff --git a/res/values-ka/strings.xml b/res/values-ka/strings.xml
index d428447..eeb33fb 100644
--- a/res/values-ka/strings.xml
+++ b/res/values-ka/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"პირადი სივრცის პარამეტრები"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"პირადი სივრცის ჩაკეტვა/განბლოკვა"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"პირად სივრცეზე გადასვლა"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"გადავსება"</string>
 </resources>
diff --git a/res/values-kk/strings.xml b/res/values-kk/strings.xml
index 4f6c6fd..85e7b6c 100644
--- a/res/values-kk/strings.xml
+++ b/res/values-kk/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"Жеке бөлме параметрлері"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"Жеке бөлмені құлыптау/оның құлпын ашу"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"Жеке бөлмеге өту"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"Қосымша мәзір"</string>
 </resources>
diff --git a/res/values-km/strings.xml b/res/values-km/strings.xml
index 91622f0..49c2b35 100644
--- a/res/values-km/strings.xml
+++ b/res/values-km/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"ការកំណត់ Private Space"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"ចាក់សោ/ដោះសោ Private Space"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"ការផ្លាស់ប្ដូរ Private Space"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"ម៉ឺនុយបន្ថែម"</string>
 </resources>
diff --git a/res/values-kn/strings.xml b/res/values-kn/strings.xml
index ad208f2..21e226c 100644
--- a/res/values-kn/strings.xml
+++ b/res/values-kn/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"ಖಾಸಗಿ ಸ್ಪೇಸ್ ಸೆಟ್ಟಿಂಗ್‌ಗಳು"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"ಖಾಸಗಿ ಸ್ಪೇಸ್ ಅನ್ನು ಲಾಕ್/ಅನ್‌ಲಾಕ್ ಮಾಡಿ"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"ಖಾಸಗಿ ಸ್ಪೇಸ್ ಪರಿವರ್ತನೆಯಾಗುತ್ತಿದೆ"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"ಓವರ್‌ಫ್ಲೋ"</string>
 </resources>
diff --git a/res/values-ko/strings.xml b/res/values-ko/strings.xml
index b2b0c0c..643a513 100644
--- a/res/values-ko/strings.xml
+++ b/res/values-ko/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"비공개 스페이스 설정"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"비공개 스페이스 잠금/잠금 해제"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"비공개 스페이스 전환"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"오버플로"</string>
 </resources>
diff --git a/res/values-ky/strings.xml b/res/values-ky/strings.xml
index 78e96a6..2274389 100644
--- a/res/values-ky/strings.xml
+++ b/res/values-ky/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"Жеке чөйрөнүн параметрлери"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"Жеке чөйрөнү кулпулоо/кулпусун ачуу"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"Жеке чөйрөгө өтүү"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"Кошумча меню"</string>
 </resources>
diff --git a/res/values-lo/strings.xml b/res/values-lo/strings.xml
index bb99150..175e8f5 100644
--- a/res/values-lo/strings.xml
+++ b/res/values-lo/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"ການຕັ້ງຄ່າພື້ນທີ່ສ່ວນຕົວ"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"ລັອກ/ປົດລັອກພື້ນທີ່ສ່ວນຕົວ"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"ການປ່ຽນແປງພື້ນທີ່ສ່ວນຕົວ"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"ການດຳເນີນການເພີ່ມເຕີມ"</string>
 </resources>
diff --git a/res/values-lt/strings.xml b/res/values-lt/strings.xml
index 075bbb2..ef56c1a 100644
--- a/res/values-lt/strings.xml
+++ b/res/values-lt/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"Privačios erdvės nustatymai"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"Užrakinti ir (arba) atrakinti privačią erdvę"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"Privačios erdvės perkėlimas"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"Perpildymas"</string>
 </resources>
diff --git a/res/values-lv/strings.xml b/res/values-lv/strings.xml
index 5edee25..f2e8a87 100644
--- a/res/values-lv/strings.xml
+++ b/res/values-lv/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"Privātās mapes iestatījumi"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"Bloķēt/atbloķēt privāto mapi"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"Pāriet uz privāto mapi"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"Pārpilde"</string>
 </resources>
diff --git a/res/values-mk/strings.xml b/res/values-mk/strings.xml
index a95b7bf..cfbd3ba 100644
--- a/res/values-mk/strings.xml
+++ b/res/values-mk/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"Поставки за „Приватен простор“"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"Заклучување/отклучување на „Приватен простор“"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"Префрлање на „Приватен простор“"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"Проширено балонче"</string>
 </resources>
diff --git a/res/values-ml/strings.xml b/res/values-ml/strings.xml
index 1cdcb09..2ae3c2f 100644
--- a/res/values-ml/strings.xml
+++ b/res/values-ml/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"സ്വകാര്യ സ്‌പേസ് ക്രമീകരണം"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"സ്വകാര്യ സ്‌പേസ് ലോക്ക് ചെയ്യുക/അൺലോക്ക് ചെയ്യുക"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"പ്രൈവറ്റ് സ്‌പേസ് ട്രാൻസിഷനിംഗ്"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"ഓവർഫ്ലോ"</string>
 </resources>
diff --git a/res/values-mn/strings.xml b/res/values-mn/strings.xml
index 2d53dea..31dda29 100644
--- a/res/values-mn/strings.xml
+++ b/res/values-mn/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"Private Space-н тохиргоо"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"Private Space-г түгжих/түгжээг тайлах"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"Private Space-н шилжилт"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"Урт цэс"</string>
 </resources>
diff --git a/res/values-mr/strings.xml b/res/values-mr/strings.xml
index 9e44733..635a422 100644
--- a/res/values-mr/strings.xml
+++ b/res/values-mr/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"खाजगी स्पेस ची सेटिंग्ज"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"खाजगी स्पेस लॉक/अनलॉक करा"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"खाजगी स्पेस वर स्विच करणे"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"ओव्हरफ्लो"</string>
 </resources>
diff --git a/res/values-ms/strings.xml b/res/values-ms/strings.xml
index c90082a..566ea85 100644
--- a/res/values-ms/strings.xml
+++ b/res/values-ms/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"Tetapan Ruang Peribadi"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"Kunci/Buka kunci Ruang Peribadi"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"Peralihan Ruang Peribadi"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"Limpahan"</string>
 </resources>
diff --git a/res/values-my/strings.xml b/res/values-my/strings.xml
index d1bef97..ced9f61 100644
--- a/res/values-my/strings.xml
+++ b/res/values-my/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"သီးသန့်ချတ်ခန်း ဆက်တင်များ"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"သီးသန့်ချတ်ခန်း လော့ခ်ချ/ဖွင့်ရန်"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"သီးသန့်ချတ်ခန်း အပြောင်းအလဲ"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"မီနူးအပို"</string>
 </resources>
diff --git a/res/values-nb/strings.xml b/res/values-nb/strings.xml
index cd2fb1f..cb0fd95 100644
--- a/res/values-nb/strings.xml
+++ b/res/values-nb/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"Innstillinger for Private Space"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"Lås / lås opp Private Space"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"Private Space-overgang"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"Overflyt"</string>
 </resources>
diff --git a/res/values-ne/strings.xml b/res/values-ne/strings.xml
index b9397a0..d8df060 100644
--- a/res/values-ne/strings.xml
+++ b/res/values-ne/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"निजी स्पेससम्बन्धी सेटिङ"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"निजी स्पेस लक/अनलक गर्नुहोस्"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"निजी स्पेस ट्रान्जिसन गरिँदै छ"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"ओभरफ्लो"</string>
 </resources>
diff --git a/res/values-nl/strings.xml b/res/values-nl/strings.xml
index 84d5c15..86a5f40 100644
--- a/res/values-nl/strings.xml
+++ b/res/values-nl/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"Instellingen voor privéruimte"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"Privéruimte vergrendelen/ontgrendelen"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"Overschakelen naar privéruimte"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"Overloop"</string>
 </resources>
diff --git a/res/values-or/strings.xml b/res/values-or/strings.xml
index 3fa8754..c726e50 100644
--- a/res/values-or/strings.xml
+++ b/res/values-or/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"ପ୍ରାଇଭେଟ ସ୍ପେସ ସେଟିଂସ"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"ପ୍ରାଇଭେଟ ସ୍ପେସକୁ ଲକ/ଅନଲକ କରନ୍ତୁ"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"ପ୍ରାଇଭେଟ ସ୍ପେସ ଟ୍ରାଞ୍ଜିସନିଂ"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"ଓଭରଫ୍ଲୋ"</string>
 </resources>
diff --git a/res/values-pa/strings.xml b/res/values-pa/strings.xml
index 98bc36d..46e750a 100644
--- a/res/values-pa/strings.xml
+++ b/res/values-pa/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"ਨਿੱਜੀ ਸਪੇਸ ਸੰਬੰਧੀ ਸੈਟਿੰਗਾਂ"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"ਨਿੱਜੀ ਸਪੇਸ ਨੂੰ ਲਾਕ/ਅਣਲਾਕ ਕਰੋ"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"ਨਿੱਜੀ ਸਪੇਸ ਨੂੰ ਤਬਦੀਲ ਕਰਨਾ"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"ਓਵਰਫ਼ਲੋ"</string>
 </resources>
diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml
index 30a953c..4b6a612 100644
--- a/res/values-pl/strings.xml
+++ b/res/values-pl/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"Ustawienia obszaru prywatnego"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"Zablokuj/odblokuj obszar prywatny"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"Przenoszenie obszaru prywatnego"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"Rozwiń menu"</string>
 </resources>
diff --git a/res/values-pt-rPT/strings.xml b/res/values-pt-rPT/strings.xml
index af760f8..4803138 100644
--- a/res/values-pt-rPT/strings.xml
+++ b/res/values-pt-rPT/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"Definições do espaço privado"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"Bloquear/desbloquear espaço privado"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"Transição do espaço privado"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"Menu adicional"</string>
 </resources>
diff --git a/res/values-pt/strings.xml b/res/values-pt/strings.xml
index 4359e9a..40af95d 100644
--- a/res/values-pt/strings.xml
+++ b/res/values-pt/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"Configurações do Espaço particular"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"Bloquear/desbloquear o Espaço particular"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"Espaço particular em transição"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"Balão flutuante"</string>
 </resources>
diff --git a/res/values-ro/strings.xml b/res/values-ro/strings.xml
index 836ac99..2d66f9b 100644
--- a/res/values-ro/strings.xml
+++ b/res/values-ro/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"Setări spațiu privat"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"Blochează / deblochează spațiul privat"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"Tranziție pentru spațiul privat"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"Suplimentar"</string>
 </resources>
diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml
index bb7b0a0..063126d 100644
--- a/res/values-ru/strings.xml
+++ b/res/values-ru/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"Настройки личного пространства"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"Блокировка и разблокировка личного пространства"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"Переход к личному пространству"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"Дополнительное меню"</string>
 </resources>
diff --git a/res/values-si/strings.xml b/res/values-si/strings.xml
index 401e0fb..e5660ce 100644
--- a/res/values-si/strings.xml
+++ b/res/values-si/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"පෞද්ගලික අවකාශ සැකසීම්"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"පෞද්ගලික අවකාශය අගුළු දමන්න/අගුළු හරින්න"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"පෞද්ගලික අවකාශ සංක්‍රමණය"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"පිටාර යාම"</string>
 </resources>
diff --git a/res/values-sk/strings.xml b/res/values-sk/strings.xml
index fbe64a8..89c64bf 100644
--- a/res/values-sk/strings.xml
+++ b/res/values-sk/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"Nastavenia súkromného priestoru"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"Súkromný priestor zamykania a odomykania"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"Prechod súkromného priestoru"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"Rozšírená ponuka"</string>
 </resources>
diff --git a/res/values-sl/strings.xml b/res/values-sl/strings.xml
index d91e83f..5f18f26 100644
--- a/res/values-sl/strings.xml
+++ b/res/values-sl/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"Nastavitve zasebnega prostora"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"Zaklepanje/odklepanje zasebnega prostora"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"Preklapljanje zasebnega prostora"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"Oblaček z dodatnimi elementi"</string>
 </resources>
diff --git a/res/values-sq/strings.xml b/res/values-sq/strings.xml
index a3bd673..c06fc7e 100644
--- a/res/values-sq/strings.xml
+++ b/res/values-sq/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"Cilësimet e \"Hapësirës private\""</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"Kyç/Shkyç \"Hapësirën private\""</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"Kalimi te \"Hapësira private\""</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"Tejkalimi"</string>
 </resources>
diff --git a/res/values-sr/strings.xml b/res/values-sr/strings.xml
index 5242848..eb3bc64 100644
--- a/res/values-sr/strings.xml
+++ b/res/values-sr/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"Подешавања приватног простора"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"Закључај/откључај приватни простор"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"Пренос приватног простора"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"Преклопно"</string>
 </resources>
diff --git a/res/values-sv/strings.xml b/res/values-sv/strings.xml
index 1e1436f..6824cc1 100644
--- a/res/values-sv/strings.xml
+++ b/res/values-sv/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"Inställningar för privat rum"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"Lås eller lås upp ditt privata rum"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"Överföring av privat rum"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"Fler alternativ"</string>
 </resources>
diff --git a/res/values-sw/strings.xml b/res/values-sw/strings.xml
index 2486cae..788a06a 100644
--- a/res/values-sw/strings.xml
+++ b/res/values-sw/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"Mipangilio ya Nafasi ya Faragha"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"Funga/Fungua Nafasi ya Faragha"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"Mabadiliko ya Nafasi ya Faragha"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"Menyu ya vipengee vya ziada"</string>
 </resources>
diff --git a/res/values-ta/strings.xml b/res/values-ta/strings.xml
index cb0ca67..6c89de9 100644
--- a/res/values-ta/strings.xml
+++ b/res/values-ta/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"தனிப்பட்ட சேமிப்பிட அமைப்புகள்"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"தனிப்பட்ட சேமிப்பிடத்தை லாக்/அன்லாக் செய்யும்"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"தனிப்பட்ட சேமிப்பிடத்திற்கு மாற்றுகிறது"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"கூடுதல் விருப்பங்களைக் காட்டும்"</string>
 </resources>
diff --git a/res/values-te/strings.xml b/res/values-te/strings.xml
index 27cec72..e58039f 100644
--- a/res/values-te/strings.xml
+++ b/res/values-te/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"ప్రైవేట్ స్పేస్ సెట్టింగ్‌లు"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"ప్రైవేట్ స్పేస్‌ను లాక్/అన్‌లాక్ చేయండి"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"ప్రైవేట్ స్పేస్ కేటాయించడం జరుగుతుంది"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"ఓవర్‌ఫ్లో"</string>
 </resources>
diff --git a/res/values-th/strings.xml b/res/values-th/strings.xml
index 78cbcfa..0eff887 100644
--- a/res/values-th/strings.xml
+++ b/res/values-th/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"การตั้งค่าพื้นที่ส่วนตัว"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"ล็อก/ปลดล็อกพื้นที่ส่วนตัว"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"การเปลี่ยนไปใช้พื้นที่ส่วนตัว"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"การดำเนินการเพิ่มเติม"</string>
 </resources>
diff --git a/res/values-tl/strings.xml b/res/values-tl/strings.xml
index acf60ff..3db2aba 100644
--- a/res/values-tl/strings.xml
+++ b/res/values-tl/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"Mga Setting ng Pribadong Space"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"I-lock/I-unlock ang Pribadong Space"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"Pag-transition ng Pribadong Space"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"Overflow"</string>
 </resources>
diff --git a/res/values-tr/strings.xml b/res/values-tr/strings.xml
index 1449202..78fc596 100644
--- a/res/values-tr/strings.xml
+++ b/res/values-tr/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"Gizli Alan Ayarları"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"Gizli Alanı Kilitleyin/Kilidini Açın"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"Gizli Alana Geçiş"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"Taşma"</string>
 </resources>
diff --git a/res/values-uk/strings.xml b/res/values-uk/strings.xml
index ad325cc..b69558f 100644
--- a/res/values-uk/strings.xml
+++ b/res/values-uk/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"Налаштування приватного простору"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"Заблокувати/розблокувати приватний простір"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"Перехід у приватний простір"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"Додаткове меню"</string>
 </resources>
diff --git a/res/values-ur/strings.xml b/res/values-ur/strings.xml
index 1c0e3ae..549f2a1 100644
--- a/res/values-ur/strings.xml
+++ b/res/values-ur/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"نجی اسپیس کی ترتیبات"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"نجی اسپیس کو مقفل کریں/غیر مقفل کریں"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"نجی اسپیس کی منتقلی"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"اوورفلو"</string>
 </resources>
diff --git a/res/values-uz/strings.xml b/res/values-uz/strings.xml
index 8e6d392..7cacd96 100644
--- a/res/values-uz/strings.xml
+++ b/res/values-uz/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"Shaxsiy xona sozlamalari"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"Shaxsiy xonani ochish/qulflash"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"Maxfiy joyga almashtirish"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"Kengaytirish"</string>
 </resources>
diff --git a/res/values-vi/strings.xml b/res/values-vi/strings.xml
index 8ed3089..7f4487c 100644
--- a/res/values-vi/strings.xml
+++ b/res/values-vi/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"Cài đặt không gian riêng tư"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"Khoá/mở khoá không gian riêng tư"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"Chuyển đổi sang không gian riêng tư"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"Bong bóng bổ sung"</string>
 </resources>
diff --git a/res/values-zh-rCN/strings.xml b/res/values-zh-rCN/strings.xml
index 53b85f2..cfae584 100644
--- a/res/values-zh-rCN/strings.xml
+++ b/res/values-zh-rCN/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"私密空间设置"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"锁定/解锁私密空间"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"私密空间转换"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"菜单"</string>
 </resources>
diff --git a/res/values-zh-rHK/strings.xml b/res/values-zh-rHK/strings.xml
index e6aaef5..e94a808 100644
--- a/res/values-zh-rHK/strings.xml
+++ b/res/values-zh-rHK/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"「私人空間」設定"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"鎖定/解鎖「私人空間」"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"轉為「私人空間」"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"顯示更多"</string>
 </resources>
diff --git a/res/values-zh-rTW/strings.xml b/res/values-zh-rTW/strings.xml
index 462b651..957bf45 100644
--- a/res/values-zh-rTW/strings.xml
+++ b/res/values-zh-rTW/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"私人空間設定"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"鎖定/取消鎖定私人空間"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"轉換私人空間狀態"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"溢位"</string>
 </resources>
diff --git a/res/values-zu/strings.xml b/res/values-zu/strings.xml
index efc0824..3051d09 100644
--- a/res/values-zu/strings.xml
+++ b/res/values-zu/strings.xml
@@ -178,5 +178,9 @@
     <string name="ps_container_settings" msgid="6059734123353320479">"Amasethingi Esikhala Esiyimfihlo"</string>
     <string name="ps_container_lock_unlock_button" msgid="7605602332253423755">"Khiya/Vula Isikhala Esiyimfihlo"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"Ukuguqulwa Kwendawo Yangasese"</string>
+    <!-- no translation found for ps_add_button_label (8611055839242385935) -->
+    <skip />
+    <!-- no translation found for ps_add_button_content_description (3254274107740952556) -->
+    <skip />
     <string name="bubble_bar_overflow_description" msgid="7410995531938041192">"Ukugcwala kakhulu"</string>
 </resources>
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index 8d84c90..754f0cb 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -42,6 +42,7 @@
     <attr name="overviewScrimColor" format="color" />
     <attr name="popupNotificationDotColor" format="color" />
     <attr name="notificationDotColor" format="color" />
+    <attr name="focusOutlineColor" format="color" />
 
     <attr name="folderPaginationColor" format="color" />
     <attr name="folderPreviewColor" format="color" />
@@ -189,6 +190,10 @@
          defaults to 2 * numAllAppsColumns -->
         <attr name="numExtendedAllAppsColumns" format="integer" />
 
+        <!-- Number of rows to calculate the cell height for all apps when it's necessary.
+          Defaults to numRows. Requires FeatureFlags.ENABLE_RESPONSIVE_WORKSPACE to be enabled. -->
+        <attr name="numAllAppsRowsForCellHeightCalculation" format="integer" />
+
         <!-- numHotseatIcons defaults to numColumns, if not specified -->
         <attr name="numHotseatIcons" format="integer" />
         <!-- Number of icons to use when extending the hotseat size,
@@ -219,26 +224,32 @@
         <!-- File that contains the specs for the workspace.
         Needs FeatureFlags.ENABLE_RESPONSIVE_WORKSPACE enabled -->
         <attr name="workspaceSpecsId" format="reference" />
+        <!-- defaults to workspaceSpecsId, if not specified -->
         <attr name="workspaceSpecsTwoPanelId" format="reference" />
         <!-- File that contains the specs for all apps.
         Needs FeatureFlags.ENABLE_RESPONSIVE_WORKSPACE enabled -->
         <attr name="allAppsSpecsId" format="reference" />
+        <!-- defaults to allAppsSpecsId, if not specified -->
         <attr name="allAppsSpecsTwoPanelId" format="reference" />
         <!-- File that contains the specs for the workspace.
         Needs FeatureFlags.ENABLE_RESPONSIVE_WORKSPACE enabled -->
         <attr name="folderSpecsId" format="reference" />
+        <!-- defaults to folderSpecsId, if not specified -->
         <attr name="folderSpecsTwoPanelId" format="reference" />
         <!-- File that contains the specs for hotseat bar.
         Needs FeatureFlags.ENABLE_RESPONSIVE_WORKSPACE enabled -->
         <attr name="hotseatSpecsId" format="reference" />
+        <!-- defaults to hotseatSpecsId, if not specified -->
         <attr name="hotseatSpecsTwoPanelId" format="reference" />
         <!-- File that contains the specs for workspace icon and text size.
         Needs FeatureFlags.ENABLE_RESPONSIVE_WORKSPACE enabled -->
         <attr name="workspaceCellSpecsId" format="reference" />
+        <!-- defaults to workspaceCellSpecsId, if not specified -->
         <attr name="workspaceCellSpecsTwoPanelId" format="reference" />
         <!-- File that contains the specs for all apps icon and text size.
         Needs FeatureFlags.ENABLE_RESPONSIVE_WORKSPACE enabled -->
         <attr name="allAppsCellSpecsId" format="reference" />
+        <!-- defaults to allAppsCellSpecsId, if not specified -->
         <attr name="allAppsCellSpecsTwoPanelId" format="reference" />
 
         <!-- By default all categories are enabled -->
diff --git a/res/values/colors.xml b/res/values/colors.xml
index 6c3b54c..6a484d7 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -71,7 +71,7 @@
     <color name="folder_background_dark">#1F2020</color>
 
     <color name="folder_preview_light">#7FCFFF</color>
-    <color name="folder_preview_dark">#2A2A2A</color>
+    <color name="folder_preview_dark">#1E1F20</color>
 
     <color name="folder_pagination_color_light">#0B57D0</color>
     <color name="folder_pagination_color_dark">#A8C7FA</color>
diff --git a/res/values/config.xml b/res/values/config.xml
index 154312a..47756ba 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" />
@@ -200,7 +201,6 @@
 
     <!-- Swipe back to home related -->
     <dimen name="swipe_back_window_scale_x_margin">10dp</dimen>
-    <dimen name="swipe_back_window_max_delta_y">160dp</dimen>
     <dimen name="swipe_back_window_corner_radius">40dp</dimen>
 
     <!-- The duration of the bottom sheet opening and closing animation -->
@@ -253,4 +253,10 @@
 
     <!--  Used for custom widgets  -->
     <array name="custom_widget_providers"/>
+
+
+    <!-- Skip "Install to private" long-press shortcut packages name -->
+    <string-array name="skip_private_profile_shortcut_packages" translatable="false">
+        <item>com.android.settings</item>
+    </string-array>
 </resources>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 242c439..603e697 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -103,6 +103,7 @@
     <dimen name="all_apps_search_bar_content_overlap">24dp</dimen>
     <dimen name="all_apps_search_bar_bottom_padding">30dp</dimen>
     <dimen name="all_apps_empty_search_message_top_offset">40dp</dimen>
+    <dimen name="all_apps_search_top_row_extra_height">4dp</dimen>
     <dimen name="all_apps_header_pill_height">48dp</dimen>
     <dimen name="all_apps_header_pill_corner_radius">12dp</dimen>
     <dimen name="all_apps_header_tab_height">48dp</dimen>
@@ -125,6 +126,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>
@@ -152,6 +156,7 @@
     <dimen name="work_edu_card_margin">16dp</dimen>
     <dimen name="work_edu_card_radius">16dp</dimen>
     <dimen name="work_edu_card_bottom_margin">26dp</dimen>
+    <dimen name="work_edu_card_text_end_margin">32dp</dimen>
     <dimen name="work_apps_paused_button_stroke">1dp</dimen>
 
     <dimen name="work_card_margin">24dp</dimen>
@@ -364,6 +369,12 @@
 
     <!-- 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_phone_home_button_size">80dp</dimen>
+    <dimen name="taskbar_phone_content_padding">8dp</dimen>
+    <dimen name="taskbar_phone_rounded_corner_content_margin">
+        @*android:dimen/rounded_corner_content_padding
+    </dimen>
     <dimen name="taskbar_stashed_size">0dp</dimen>
     <dimen name="qsb_widget_height">0dp</dimen>
     <dimen name="qsb_shadow_height">0dp</dimen>
@@ -432,6 +443,9 @@
     <dimen name="split_instructions_bottom_margin_phone_portrait">60dp</dimen>
     <dimen name="split_instructions_start_margin_cancel">8dp</dimen>
 
+    <dimen name="focus_outline_radius">16dp</dimen>
+    <dimen name="focus_outline_stroke_width">3dp</dimen>
+
     <!-- Workspace grid visualization parameters -->
     <dimen name="grid_visualization_rounding_radius">16dp</dimen>
     <dimen name="grid_visualization_horizontal_cell_spacing">6dp</dimen>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 6a4a9a4..fb55c75 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -171,9 +171,11 @@
     <string name="uninstall_drop_target_label">Uninstall</string>
     <!-- Label for app info drop target. [CHAR_LIMIT=20] -->
     <string name="app_info_drop_target_label">App info</string>
+    <!-- Label for install to private profile shortcut label. [CHAR_LIMIT=20] -->
+    <string name="install_private_system_shortcut_label">Install in private</string>
     <!-- Label for install drop target. [CHAR_LIMIT=20] -->
     <string name="install_drop_target_label">Install</string>
-    <!-- Label for install dismiss prediction. -->
+    <!-- Label for dismiss prediction. -->
     <string name="dismiss_prediction_label">Don\'t suggest app</string>
     <!-- Label for pinning predicted app. -->
     <string name="pin_prediction">Pin Prediction</string>
@@ -461,6 +463,10 @@
     <string name="ps_container_lock_unlock_button">Lock/Unlock Private Space</string>
     <!-- Description for Private Space Transition button -->
     <string name="ps_container_transition">Private Space Transitioning</string>
+    <!-- Title for Private Space install app icon -->
+    <string name="ps_add_button_label">Install apps</string>
+    <!-- Content description for install app icon -->
+    <string name="ps_add_button_content_description">Install apps to Private Space</string>
 
     <!-- Strings for bubble bar -->
     <!-- content description for the overflow bubble [CHAR_LIMIT=none] -->
diff --git a/res/values/styles.xml b/res/values/styles.xml
index 36991b1..b83be35 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -52,6 +52,7 @@
         <item name="workspaceAmbientShadowColor">#40000000</item>
         <item name="workspaceKeyShadowColor">#89000000</item>
         <item name="widgetsTheme">@style/WidgetContainerTheme</item>
+        <item name="focusOutlineColor">@color/material_color_on_secondary_container</item>
         <item name="folderPaginationColor">@color/folder_pagination_color_light</item>
         <item name="folderPreviewColor">@color/folder_preview_light</item>
         <item name="folderBackgroundColor">@color/folder_background_light</item>
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/BaseDraggingActivity.java b/src/com/android/launcher3/BaseDraggingActivity.java
index f8ed4df..b6f6615 100644
--- a/src/com/android/launcher3/BaseDraggingActivity.java
+++ b/src/com/android/launcher3/BaseDraggingActivity.java
@@ -39,7 +39,6 @@
 import com.android.launcher3.util.DisplayController.Info;
 import com.android.launcher3.util.OnColorHintListener;
 import com.android.launcher3.util.Themes;
-import com.android.launcher3.util.TraceHelper;
 import com.android.launcher3.util.WallpaperColorHints;
 import com.android.launcher3.util.WindowBounds;
 
@@ -55,16 +54,12 @@
     public static final Object AUTO_CANCEL_ACTION_MODE = new Object();
 
     private ActionMode mCurrentActionMode;
-    protected boolean mIsSafeModeEnabled;
 
     private int mThemeRes = R.style.AppTheme;
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
-
-        mIsSafeModeEnabled = TraceHelper.allowIpcs("isSafeMode",
-                () -> getPackageManager().isSafeMode());
         DisplayController.INSTANCE.get(this).addChangeListener(this);
 
         // Update theme
@@ -183,6 +178,6 @@
 
     @Override
     public boolean isAppBlockedForSafeMode() {
-        return mIsSafeModeEnabled;
+        return LauncherAppState.getInstance(this).isSafeModeEnabled();
     }
 }
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index e2e528c..5b497f2 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -22,6 +22,7 @@
 import static com.android.launcher3.config.FeatureFlags.ENABLE_ICON_LABEL_AUTO_SCALING;
 import static com.android.launcher3.graphics.PreloadIconDrawable.newPendingIcon;
 import static com.android.launcher3.icons.BitmapInfo.FLAG_NO_BADGE;
+import static com.android.launcher3.icons.BitmapInfo.FLAG_SKIP_USER_BADGE;
 import static com.android.launcher3.icons.BitmapInfo.FLAG_THEMED;
 import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound;
 import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_INCREMENTAL_DOWNLOAD_ACTIVE;
@@ -164,6 +165,8 @@
     @ViewDebug.ExportedProperty(category = "launcher")
     private boolean mHideBadge = false;
     @ViewDebug.ExportedProperty(category = "launcher")
+    private boolean mSkipUserBadge = false;
+    @ViewDebug.ExportedProperty(category = "launcher")
     private boolean mIsIconVisible = true;
     @ViewDebug.ExportedProperty(category = "launcher")
     private int mTextColor;
@@ -175,6 +178,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 +254,7 @@
 
         mDotParams = new DotRenderer.DrawParams();
 
+        mCurrentLocale = context.getResources().getConfiguration().locale;
         setEllipsize(TruncateAt.END);
         setAccessibilityDelegate(mActivity.getAccessibilityDelegate());
         setTextAlpha(1f);
@@ -266,6 +271,10 @@
         mHideBadge = hideBadge;
     }
 
+    public void setSkipUserBadge(boolean skipUserBadge) {
+        mSkipUserBadge = skipUserBadge;
+    }
+
     /**
      * Resets the view so it can be recycled.
      */
@@ -395,6 +404,9 @@
         if (mHideBadge || mDisplay == DISPLAY_SEARCH_RESULT_SMALL) {
             flags |= FLAG_NO_BADGE;
         }
+        if (mSkipUserBadge) {
+            flags |= FLAG_SKIP_USER_BADGE;
+        }
         FastBitmapDrawable iconDrawable = info.newIcon(getContext(), flags);
         mDotParams.appColor = iconDrawable.getIconColor();
         mDotParams.dotColor = Themes.getAttrColor(getContext(), R.attr.notificationDotColor);
@@ -411,10 +423,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
@@ -919,6 +933,8 @@
             if (mIcon instanceof PreloadIconDrawable) {
                 preloadIconDrawable = (PreloadIconDrawable) mIcon;
                 preloadIconDrawable.setLevel(progressLevel);
+                // TODO(b/302115555): For archived apps, show icon as disabled if active session
+                //  exists.
                 preloadIconDrawable.setIsDisabled(info.getProgressLevel() == 0);
             } else {
                 preloadIconDrawable = makePreloadIcon();
@@ -944,6 +960,7 @@
         final PreloadIconDrawable preloadDrawable = newPendingIcon(getContext(), info);
 
         preloadDrawable.setLevel(progressLevel);
+        // TODO(b/302115555): For archived apps, show icon as disabled if active session exists.
         preloadDrawable.setIsDisabled(info.getProgressLevel() == 0);
         return preloadDrawable;
     }
diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index b6e8ec3..e9545c8 100644
--- a/src/com/android/launcher3/DeviceProfile.java
+++ b/src/com/android/launcher3/DeviceProfile.java
@@ -78,10 +78,12 @@
 public class DeviceProfile {
 
     private static final int DEFAULT_DOT_SIZE = 100;
-    private static final float ALL_APPS_TABLET_MAX_ROWS = 5.5f;
     private static final float MIN_FOLDER_TEXT_SIZE_SP = 16f;
     private static final float MIN_WIDGET_PADDING_DP = 6f;
 
+    // Minimum aspect ratio beyond which an extra top padding may be applied to a bottom sheet.
+    private static final float MIN_ASPECT_RATIO_FOR_EXTRA_TOP_PADDING = 1.5f;
+
     public static final PointF DEFAULT_SCALE = new PointF(1.0f, 1.0f);
     public static final ViewScaleProvider DEFAULT_PROVIDER = itemInfo -> DEFAULT_SCALE;
     public static final Consumer<DeviceProfile> DEFAULT_DIMENSION_PROVIDER = dp -> {
@@ -356,7 +358,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();
@@ -415,8 +417,14 @@
         gridVisualizationPaddingY = res.getDimensionPixelSize(
                 R.dimen.grid_visualization_vertical_cell_spacing);
 
+        // Tablet portrait mode uses a single pane widget picker and extra padding may be applied on
+        // top to avoid making it look too elongated.
+        final boolean applyExtraTopPadding = isTablet
+                && !isLandscape
+                && (aspectRatio > MIN_ASPECT_RATIO_FOR_EXTRA_TOP_PADDING);
         bottomSheetTopPadding = mInsets.top // statusbar height
-                + res.getDimensionPixelSize(R.dimen.bottom_sheet_extra_top_padding)
+                + (applyExtraTopPadding ? res.getDimensionPixelSize(
+                R.dimen.bottom_sheet_extra_top_padding) : 0)
                 + (isTablet ? 0 : edgeMarginPx); // phones need edgeMarginPx additional padding
         bottomSheetOpenDuration = res.getInteger(R.integer.config_bottomSheetOpenDuration);
         bottomSheetCloseDuration = res.getInteger(R.integer.config_bottomSheetCloseDuration);
@@ -641,8 +649,8 @@
                     DimensionType.WIDTH, numShownAllAppsColumns, availableWidthPx,
                     mResponsiveWorkspaceWidthSpec);
             mResponsiveAllAppsHeightSpec = allAppsSpecs.getCalculatedSpec(responsiveAspectRatio,
-                    DimensionType.HEIGHT, inv.numRows,  heightPx - mInsets.top,
-                    mResponsiveWorkspaceHeightSpec);
+                    DimensionType.HEIGHT, inv.numAllAppsRowsForCellHeightCalculation,
+                    heightPx - mInsets.top, mResponsiveWorkspaceHeightSpec);
 
             ResponsiveSpecsProvider folderSpecs = ResponsiveSpecsProvider.create(
                     new ResourceHelper(context,
@@ -734,14 +742,9 @@
             hotseatBorderSpace = cellLayoutBorderSpacePx.y;
         }
 
-        // AllApps height calculation depends on updated cellSize
         if (isTablet) {
-            int collapseHandleHeight =
-                    res.getDimensionPixelOffset(R.dimen.bottom_sheet_handle_area_height);
-            int contentHeight = heightPx - collapseHandleHeight - hotseatQsbHeight;
-            int targetContentHeight = (int) (allAppsCellHeightPx * ALL_APPS_TABLET_MAX_ROWS);
-            allAppsPadding.top = Math.max(mInsets.top, contentHeight - targetContentHeight);
-            allAppsShiftRange = heightPx - allAppsPadding.top;
+            allAppsPadding.top = mInsets.top;
+            allAppsShiftRange = heightPx;
         } else {
             allAppsPadding.top = 0;
             allAppsShiftRange =
@@ -789,14 +792,16 @@
      * width of the hotseat.
      */
     private int calculateQsbWidth(int hotseatBorderSpace) {
+        int iconExtraSpacePx = iconSizePx - getIconVisibleSizePx(iconSizePx);
         if (isQsbInline) {
             int columns = getPanelCount() * inv.numColumns;
             return getIconToIconWidthForColumns(columns)
                     - iconSizePx * numShownHotseatIcons
-                    - hotseatBorderSpace * numShownHotseatIcons;
+                    - hotseatBorderSpace * numShownHotseatIcons
+                    - iconExtraSpacePx;
         } else {
             int columns = inv.hotseatColumnSpan[mTypeIndex];
-            return getIconToIconWidthForColumns(columns);
+            return getIconToIconWidthForColumns(columns) - iconExtraSpacePx;
         }
     }
 
@@ -992,16 +997,6 @@
         float workspaceCellPaddingY = getCellSize().y - iconSizePx - iconDrawablePaddingPx
                 - iconTextHeight;
 
-        if (mIsResponsiveGrid) {
-            iconTextSizePx = 0;
-            iconDrawablePaddingPx = 0;
-            int iconSizeWithOverlap = getIconSizeWithOverlap(iconSizePx);
-            cellYPaddingPx = Math.max(0, getCellSize().y - iconSizeWithOverlap) / 2;
-            autoResizeAllAppsCells();
-
-            return;
-        }
-
         // We want enough space so that the text is closer to its corresponding icon.
         if (workspaceCellPaddingY < iconTextHeight) {
             iconTextSizePx = 0;
@@ -1074,11 +1069,8 @@
     }
 
     private int getNormalizedIconDrawablePadding(int iconSizePx, int iconDrawablePadding) {
-        // TODO(b/235886078): workaround needed because of this bug
-        // Icons are 10% larger on XML than their visual size,
-        // so remove that extra space to get labels closer to the correct padding
-        int iconVisibleSizePx = Math.round(ICON_VISIBLE_AREA_FACTOR * iconSizePx);
-        return Math.max(0, iconDrawablePadding - ((iconSizePx - iconVisibleSizePx) / 2));
+        return Math.max(0, iconDrawablePadding
+                - ((iconSizePx - getIconVisibleSizePx(iconSizePx)) / 2));
     }
 
     private int getNormalizedIconDrawablePadding() {
@@ -1091,8 +1083,7 @@
         // so remove that extra space to get labels closer to the correct padding
         int drawablePadding = (folderCellHeightPx - folderChildIconSizePx - textHeight) / 3;
 
-        int iconVisibleSizePx = Math.round(ICON_VISIBLE_AREA_FACTOR * folderChildIconSizePx);
-        int iconSizeDiff = folderChildIconSizePx - iconVisibleSizePx;
+        int iconSizeDiff = folderChildIconSizePx - getIconVisibleSizePx(folderChildIconSizePx);
         return Math.max(0, drawablePadding - iconSizeDiff / 2);
     }
 
@@ -1123,25 +1114,28 @@
                 iconSizePx = mIconSizeSteps.getIconSmallerThan(cellWidthPx);
             }
 
-            iconDrawablePaddingPx = getNormalizedIconDrawablePadding();
+            if (isVerticalLayout) {
+                iconDrawablePaddingPx = 0;
+                iconTextSizePx = 0;
+            } else {
+                iconDrawablePaddingPx = getNormalizedIconDrawablePadding();
+            }
 
             CellContentDimensions cellContentDimensions = new CellContentDimensions(iconSizePx,
                     iconDrawablePaddingPx,
                     iconTextSizePx);
-            if (isVerticalLayout) {
-                if (cellHeightPx < iconSizePx) {
-                    cellContentDimensions.setIconSizePx(
-                            mIconSizeSteps.getIconSmallerThan(cellHeightPx));
-                }
-            } else {
-                cellContentDimensions.resizeToFitCellHeight(cellHeightPx, mIconSizeSteps);
-            }
+            int cellContentHeight = cellContentDimensions.resizeToFitCellHeight(cellHeightPx,
+                    mIconSizeSteps);
             iconSizePx = cellContentDimensions.getIconSizePx();
             iconDrawablePaddingPx = cellContentDimensions.getIconDrawablePaddingPx();
             iconTextSizePx = cellContentDimensions.getIconTextSizePx();
-            int cellContentHeight = cellContentDimensions.getCellContentHeight();
 
-            cellYPaddingPx = Math.max(0, cellHeightPx - cellContentHeight) / 2;
+            if (isVerticalLayout) {
+                cellYPaddingPx = Math.max(0, getCellSize().y - getIconSizeWithOverlap(iconSizePx))
+                        / 2;
+            } else {
+                cellYPaddingPx = Math.max(0, cellHeightPx - cellContentHeight) / 2;
+            }
         } else if (mIsScalableGrid) {
             iconDrawablePaddingPx = (int) (getNormalizedIconDrawablePadding() * iconScale);
             cellWidthPx = pxFromDp(inv.minCellSize[mTypeIndex].x, mMetrics, scale);
@@ -1225,7 +1219,7 @@
             updateAllAppsIconSize(scale, res);
         }
         updateAllAppsContainerWidth();
-        if (isVerticalBarLayout()) {
+        if (isVerticalLayout && !mIsResponsiveGrid) {
             hideWorkspaceLabelsIfNotEnoughSpace();
         }
         if (FeatureFlags.enableTwolineAllapps()) {
@@ -1349,7 +1343,7 @@
 
         if (allAppsCellHeightPx < cellContentDimensions.getCellContentHeight()) {
             if (isVerticalBarLayout()) {
-                if (allAppsCellHeightPx < iconSizePx) {
+                if (allAppsCellHeightPx < allAppsIconSizePx) {
                     cellContentDimensions.setIconSizePx(
                             mIconSizeSteps.getIconSmallerThan(allAppsCellHeightPx));
                 }
@@ -1363,6 +1357,10 @@
         }
 
         allAppsCellHeightPx += mResponsiveAllAppsHeightSpec.getGutterPx();
+
+        if (isVerticalBarLayout()) {
+            autoResizeAllAppsCells();
+        }
     }
 
     /**
@@ -1486,6 +1484,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 +1502,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);
@@ -1780,7 +1786,8 @@
             }
 
         } else if (mIsScalableGrid) {
-            int sideSpacing = (availableWidthPx - hotseatQsbWidth) / 2;
+            int iconExtraSpacePx = iconSizePx - getIconVisibleSizePx(iconSizePx);
+            int sideSpacing = (availableWidthPx - (hotseatQsbWidth + iconExtraSpacePx)) / 2;
             hotseatBarPadding.set(sideSpacing,
                     0,
                     sideSpacing,
@@ -1819,13 +1826,24 @@
                 availableWidthPx - allAppsSpacing,
                 0 /* borderSpace */,
                 numShownAllAppsColumns);
-        int iconVisibleSize = Math.round(ICON_VISIBLE_AREA_FACTOR * allAppsIconSizePx);
-        int iconAlignmentMargin = (cellWidth - iconVisibleSize) / 2;
+        int iconAlignmentMargin = (cellWidth - getIconVisibleSizePx(allAppsIconSizePx)) / 2;
 
         return (Utilities.isRtl(context.getResources()) ? allAppsPadding.right
                 : allAppsPadding.left) + iconAlignmentMargin;
     }
 
+    /**
+     * TODO(b/235886078): workaround needed because of this bug
+     * Icons are 10% larger on XML than their visual size, so remove that extra space to get
+     * some dimensions correct.
+     *
+     * When this bug is resolved this method will no longer be needed and we would be able to
+     * replace all instances where this method is called with iconSizePx.
+     */
+    private int getIconVisibleSizePx(int iconSizePx) {
+        return Math.round(ICON_VISIBLE_AREA_FACTOR * iconSizePx);
+    }
+
     private int getAdditionalQsbSpace() {
         return isQsbInline ? hotseatQsbWidth + hotseatBorderSpace : 0;
     }
diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java
index dfbbcaa..42d4d50 100644
--- a/src/com/android/launcher3/InvariantDeviceProfile.java
+++ b/src/com/android/launcher3/InvariantDeviceProfile.java
@@ -170,6 +170,7 @@
      * Number of columns in the all apps list.
      */
     public int numAllAppsColumns;
+    public int numAllAppsRowsForCellHeightCalculation;
     public int numDatabaseAllAppsColumns;
     public @StyleRes int allAppsStyle;
 
@@ -247,7 +248,7 @@
     public InvariantDeviceProfile(Context context, String gridName) {
         String newName = initGrid(context, gridName);
         if (newName == null || !newName.equals(gridName)) {
-            throw new IllegalArgumentException("Unknown grid name");
+            throw new IllegalArgumentException("Unknown grid name: " + gridName);
         }
     }
 
@@ -298,11 +299,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
@@ -389,6 +394,8 @@
         workspaceCellSpecsTwoPanelId = closestProfile.mWorkspaceCellSpecsTwoPanelId;
         allAppsCellSpecsId = closestProfile.mAllAppsCellSpecsId;
         allAppsCellSpecsTwoPanelId = closestProfile.mAllAppsCellSpecsTwoPanelId;
+        numAllAppsRowsForCellHeightCalculation =
+                closestProfile.mNumAllAppsRowsForCellHeightCalculation;
         this.deviceType = deviceType;
 
         inlineNavButtonsEndSpacing = closestProfile.inlineNavButtonsEndSpacing;
@@ -419,6 +426,7 @@
         allAppsStyle = closestProfile.allAppsStyle;
 
         numAllAppsColumns = closestProfile.numAllAppsColumns;
+
         numDatabaseAllAppsColumns = deviceType == TYPE_MULTI_DISPLAY
                 ? closestProfile.numDatabaseAllAppsColumns : closestProfile.numAllAppsColumns;
 
@@ -817,6 +825,7 @@
 
         private final @StyleRes int allAppsStyle;
         private final int numAllAppsColumns;
+        private final int mNumAllAppsRowsForCellHeightCalculation;
         private final int numDatabaseAllAppsColumns;
         private final int numHotseatIcons;
         private final int numDatabaseHotseatIcons;
@@ -939,34 +948,37 @@
                         R.styleable.GridDisplayOption_workspaceSpecsId, INVALID_RESOURCE_HANDLE);
                 mWorkspaceSpecsTwoPanelId = a.getResourceId(
                         R.styleable.GridDisplayOption_workspaceSpecsTwoPanelId,
-                        INVALID_RESOURCE_HANDLE);
+                        mWorkspaceSpecsId);
                 mAllAppsSpecsId = a.getResourceId(
                         R.styleable.GridDisplayOption_allAppsSpecsId, INVALID_RESOURCE_HANDLE);
                 mAllAppsSpecsTwoPanelId = a.getResourceId(
                         R.styleable.GridDisplayOption_allAppsSpecsTwoPanelId,
-                        INVALID_RESOURCE_HANDLE);
+                        mAllAppsSpecsId);
                 mFolderSpecsId = a.getResourceId(
                         R.styleable.GridDisplayOption_folderSpecsId, INVALID_RESOURCE_HANDLE);
                 mFolderSpecsTwoPanelId = a.getResourceId(
                         R.styleable.GridDisplayOption_folderSpecsTwoPanelId,
-                        INVALID_RESOURCE_HANDLE);
+                        mFolderSpecsId);
                 mHotseatSpecsId = a.getResourceId(
                         R.styleable.GridDisplayOption_hotseatSpecsId, INVALID_RESOURCE_HANDLE);
                 mHotseatSpecsTwoPanelId = a.getResourceId(
                         R.styleable.GridDisplayOption_hotseatSpecsTwoPanelId,
-                        INVALID_RESOURCE_HANDLE);
+                        mHotseatSpecsId);
                 mWorkspaceCellSpecsId = a.getResourceId(
                         R.styleable.GridDisplayOption_workspaceCellSpecsId,
                         INVALID_RESOURCE_HANDLE);
                 mWorkspaceCellSpecsTwoPanelId = a.getResourceId(
                         R.styleable.GridDisplayOption_workspaceCellSpecsTwoPanelId,
-                        INVALID_RESOURCE_HANDLE);
+                        mWorkspaceCellSpecsId);
                 mAllAppsCellSpecsId = a.getResourceId(
                         R.styleable.GridDisplayOption_allAppsCellSpecsId,
                         INVALID_RESOURCE_HANDLE);
                 mAllAppsCellSpecsTwoPanelId = a.getResourceId(
                         R.styleable.GridDisplayOption_allAppsCellSpecsTwoPanelId,
-                        INVALID_RESOURCE_HANDLE);
+                        mAllAppsCellSpecsId);
+                mNumAllAppsRowsForCellHeightCalculation = a.getInt(
+                        R.styleable.GridDisplayOption_numAllAppsRowsForCellHeightCalculation,
+                        numRows);
             } else {
                 mWorkspaceSpecsId = INVALID_RESOURCE_HANDLE;
                 mWorkspaceSpecsTwoPanelId = INVALID_RESOURCE_HANDLE;
@@ -980,6 +992,7 @@
                 mWorkspaceCellSpecsTwoPanelId = INVALID_RESOURCE_HANDLE;
                 mAllAppsCellSpecsId = INVALID_RESOURCE_HANDLE;
                 mAllAppsCellSpecsTwoPanelId = INVALID_RESOURCE_HANDLE;
+                mNumAllAppsRowsForCellHeightCalculation = numRows;
             }
 
             int inlineForRotation = a.getInt(R.styleable.GridDisplayOption_inlineQsb,
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 17bcfa4..826eeb2 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -54,6 +54,7 @@
 import static com.android.launcher3.LauncherConstants.TraceEvents.ON_START_EVT;
 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP;
 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
+import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET;
 import static com.android.launcher3.LauncherState.ALL_APPS;
 import static com.android.launcher3.LauncherState.EDIT_MODE;
 import static com.android.launcher3.LauncherState.FLAG_MULTI_PAGE;
@@ -85,7 +86,6 @@
 import static com.android.launcher3.logging.StatsLogManager.StatsLatencyLogger.LatencyType.WARM;
 import static com.android.launcher3.model.ItemInstallQueue.FLAG_ACTIVITY_PAUSED;
 import static com.android.launcher3.model.ItemInstallQueue.FLAG_DRAG_AND_DROP;
-import static com.android.launcher3.model.data.LauncherAppWidgetInfo.CUSTOM_WIDGET_ID;
 import static com.android.launcher3.popup.SystemShortcut.APP_INFO;
 import static com.android.launcher3.popup.SystemShortcut.INSTALL;
 import static com.android.launcher3.popup.SystemShortcut.WIDGETS;
@@ -191,7 +191,6 @@
 import com.android.launcher3.model.ItemInstallQueue;
 import com.android.launcher3.model.ModelWriter;
 import com.android.launcher3.model.StringCache;
-import com.android.launcher3.model.WidgetsModel;
 import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.model.data.FolderInfo;
 import com.android.launcher3.model.data.ItemInfo;
@@ -203,7 +202,6 @@
 import com.android.launcher3.popup.ArrowPopup;
 import com.android.launcher3.popup.PopupDataProvider;
 import com.android.launcher3.popup.SystemShortcut;
-import com.android.launcher3.qsb.QsbContainerView;
 import com.android.launcher3.statemanager.StateManager;
 import com.android.launcher3.statemanager.StateManager.StateHandler;
 import com.android.launcher3.statemanager.StatefulActivity;
@@ -247,6 +245,8 @@
 import com.android.launcher3.widget.PendingAddWidgetInfo;
 import com.android.launcher3.widget.PendingAppWidgetHostView;
 import com.android.launcher3.widget.WidgetAddFlowHandler;
+import com.android.launcher3.widget.WidgetInflater;
+import com.android.launcher3.widget.WidgetInflater.InflationResult;
 import com.android.launcher3.widget.WidgetManagerHelper;
 import com.android.launcher3.widget.custom.CustomWidgetManager;
 import com.android.launcher3.widget.model.WidgetsListBaseEntry;
@@ -328,6 +328,7 @@
 
     private WidgetManagerHelper mAppWidgetManager;
     private LauncherWidgetHolder mAppWidgetHolder;
+    private WidgetInflater mWidgetInflater;
 
     private final int[] mTmpAddItemCellCoordinates = new int[2];
 
@@ -510,8 +511,10 @@
         mStateManager = new StateManager<>(this, NORMAL);
 
         setupViews();
+        updateDisallowBack();
 
         mAppWidgetManager = new WidgetManagerHelper(this);
+        mWidgetInflater = new WidgetInflater(this);
         mAppWidgetHolder = createAppWidgetHolder();
         mAppWidgetHolder.startListening();
         mAppWidgetHolder.addProviderChangeListener(() -> refreshAndBindWidgetsForPackageUser(null));
@@ -1006,7 +1009,7 @@
         AppWidgetHostView boundWidget = null;
         if (resultCode == RESULT_OK) {
             animationType = Workspace.COMPLETE_TWO_STAGE_WIDGET_DROP_ANIMATION;
-            final AppWidgetHostView layout = mAppWidgetHolder.createView(this, appWidgetId,
+            final AppWidgetHostView layout = mAppWidgetHolder.createView(appWidgetId,
                     requestArgs.getWidgetHandler().getProviderInfo(this));
             boundWidget = layout;
             onCompleteRunnable = () -> {
@@ -1459,7 +1462,7 @@
 
         if (hostView == null) {
             // Perform actual inflation because we're live
-            hostView = mAppWidgetHolder.createView(this, appWidgetId, appWidgetInfo);
+            hostView = mAppWidgetHolder.createView(appWidgetId, appWidgetInfo);
         }
 
         LauncherAppWidgetInfo launcherInfo;
@@ -1509,7 +1512,6 @@
 
     private void prepareAppWidget(AppWidgetHostView hostView, LauncherAppWidgetInfo item) {
         hostView.setTag(item);
-        item.onBindAppWidget(this, hostView);
         hostView.setFocusable(true);
         hostView.setOnFocusChangeListener(mFocusHandler);
     }
@@ -1710,11 +1712,7 @@
         mModel.removeCallbacks(this);
         mRotationHelper.destroy();
 
-        try {
-            mAppWidgetHolder.stopListening();
-        } catch (NullPointerException ex) {
-            Log.w(TAG, "problem while stopping AppWidgetHost during Launcher destruction", ex);
-        }
+        mAppWidgetHolder.stopListening();
         mAppWidgetHolder.destroy();
 
         TextKeyListener.getInstance().release();
@@ -2160,7 +2158,6 @@
         View newView = null;
         for (int i = 0; i < end; i++) {
             final ItemInfo item = items.get(i);
-
             // Short circuit if we are loading dock items for a configuration which has no dock
             if (item.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT &&
                     mHotseat == null) {
@@ -2187,7 +2184,7 @@
                             (FolderInfo) item);
                     break;
                 }
-                case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET:
+                case ITEM_TYPE_APPWIDGET:
                 case LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET: {
                     view = inflateAppWidget((LauncherAppWidgetInfo) item);
                     if (view == null) {
@@ -2289,156 +2286,26 @@
     }
 
     private View inflateAppWidget(LauncherAppWidgetInfo item) {
-        if (item.hasOptionFlag(LauncherAppWidgetInfo.OPTION_SEARCH_WIDGET)) {
-            item.providerName = QsbContainerView.getSearchComponentName(this);
-            if (item.providerName == null) {
-                getModelWriter().deleteItemFromDatabase(item,
-                        "search widget removed because search component cannot be found");
+        TraceHelper.INSTANCE.beginSection("BIND_WIDGET_id=" + item.appWidgetId);
+        try {
+            InflationResult inflationResult = mWidgetInflater.inflateAppWidget(item);
+            if (inflationResult.getType() == WidgetInflater.TYPE_DELETE) {
+                getModelWriter().deleteItemFromDatabase(item, inflationResult.getReason());
                 return null;
             }
-        }
-        final AppWidgetHostView view;
-        if (mIsSafeModeEnabled) {
-            view = new PendingAppWidgetHostView(this, item, mIconCache, true);
+
+            if (inflationResult.isUpdate()) {
+                getModelWriter().updateItemInDatabase(item);
+            }
+            AppWidgetHostView view = inflationResult.getType() == WidgetInflater.TYPE_PENDING
+                    ? new PendingAppWidgetHostView(this, item, inflationResult.getWidgetInfo())
+                    : mAppWidgetHolder.createView(
+                            item.appWidgetId, inflationResult.getWidgetInfo());
             prepareAppWidget(view, item);
             return view;
-        }
-
-        TraceHelper.INSTANCE.beginSection("BIND_WIDGET_id=" + item.appWidgetId);
-
-        try {
-            final LauncherAppWidgetProviderInfo appWidgetInfo;
-            String removalReason = "";
-
-            if (item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY)) {
-                // If the provider is not ready, bind as a pending widget.
-                appWidgetInfo = null;
-                removalReason = "the provider isn't ready.";
-            } else if (item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_ID_NOT_VALID)) {
-                // The widget id is not valid. Try to find the widget based on the provider info.
-                appWidgetInfo = mAppWidgetManager.findProvider(item.providerName, item.user);
-                if (appWidgetInfo == null) {
-                    if (WidgetsModel.GO_DISABLE_WIDGETS) {
-                        removalReason = "widgets are disabled on go device.";
-                    } else {
-                        removalReason =
-                                "WidgetManagerHelper cannot find a provider from provider info.";
-                    }
-                }
-            } else {
-                appWidgetInfo = mAppWidgetManager.getLauncherAppWidgetInfo(item.appWidgetId,
-                        item.getTargetComponent());
-                if (appWidgetInfo == null) {
-                    if (item.appWidgetId <= CUSTOM_WIDGET_ID) {
-                        removalReason =
-                                "CustomWidgetManager cannot find provider from that widget id.";
-                    } else {
-                        removalReason = "AppWidgetManager cannot find provider for that widget id."
-                                + " It could be because AppWidgetService is not available, or the"
-                                + " appWidgetId has not been bound to a the provider yet, or you"
-                                + " don't have access to that appWidgetId.";
-                    }
-                }
-            }
-
-            // If the provider is ready, but the width is not yet restored, try to restore it.
-            if (!item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY)
-                    && (item.restoreStatus != LauncherAppWidgetInfo.RESTORE_COMPLETED)) {
-                if (appWidgetInfo == null) {
-                    getModelWriter().deleteItemFromDatabase(item,
-                            "Removing restored widget: id=" + item.appWidgetId
-                            + " belongs to component " + item.providerName + " user " + item.user
-                            + ", as the provider is null and " + removalReason);
-                    return null;
-                }
-
-                // If we do not have a valid id, try to bind an id.
-                if (item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_ID_NOT_VALID)) {
-                    if (!item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_ID_ALLOCATED)) {
-                        // Id has not been allocated yet. Allocate a new id.
-                        item.appWidgetId = mAppWidgetHolder.allocateAppWidgetId();
-                        item.restoreStatus |= LauncherAppWidgetInfo.FLAG_ID_ALLOCATED;
-
-                        // Also try to bind the widget. If the bind fails, the user will be shown
-                        // a click to setup UI, which will ask for the bind permission.
-                        PendingAddWidgetInfo pendingInfo =
-                                new PendingAddWidgetInfo(appWidgetInfo, item.sourceContainer);
-                        pendingInfo.spanX = item.spanX;
-                        pendingInfo.spanY = item.spanY;
-                        pendingInfo.minSpanX = item.minSpanX;
-                        pendingInfo.minSpanY = item.minSpanY;
-                        Bundle options = pendingInfo.getDefaultSizeOptions(this);
-
-                        boolean isDirectConfig =
-                                item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_DIRECT_CONFIG);
-                        if (isDirectConfig && item.bindOptions != null) {
-                            Bundle newOptions = item.bindOptions.getExtras();
-                            if (options != null) {
-                                newOptions.putAll(options);
-                            }
-                            options = newOptions;
-                        }
-                        boolean success = mAppWidgetManager.bindAppWidgetIdIfAllowed(
-                                item.appWidgetId, appWidgetInfo, options);
-
-                        // We tried to bind once. If we were not able to bind, we would need to
-                        // go through the permission dialog, which means we cannot skip the config
-                        // activity.
-                        item.bindOptions = null;
-                        item.restoreStatus &= ~LauncherAppWidgetInfo.FLAG_DIRECT_CONFIG;
-
-                        // Bind succeeded
-                        if (success) {
-                            // If the widget has a configure activity, it is still needs to set it
-                            // up, otherwise the widget is ready to go.
-                            item.restoreStatus = (appWidgetInfo.configure == null) || isDirectConfig
-                                    ? LauncherAppWidgetInfo.RESTORE_COMPLETED
-                                    : LauncherAppWidgetInfo.FLAG_UI_NOT_READY;
-                        }
-
-                        getModelWriter().updateItemInDatabase(item);
-                    }
-                } else if (item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_UI_NOT_READY)
-                        && (appWidgetInfo.configure == null)) {
-                    // The widget was marked as UI not ready, but there is no configure activity to
-                    // update the UI.
-                    item.restoreStatus = LauncherAppWidgetInfo.RESTORE_COMPLETED;
-                    getModelWriter().updateItemInDatabase(item);
-                }
-                else if (item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_UI_NOT_READY)
-                        && appWidgetInfo.configure != null) {
-                    if (mAppWidgetManager.isAppWidgetRestored(item.appWidgetId)) {
-                        item.restoreStatus = LauncherAppWidgetInfo.RESTORE_COMPLETED;
-                        getModelWriter().updateItemInDatabase(item);
-                    }
-                }
-            }
-
-            if (item.restoreStatus == LauncherAppWidgetInfo.RESTORE_COMPLETED) {
-                // Verify that we own the widget
-                if (appWidgetInfo == null) {
-                    FileLog.e(TAG, "Removing invalid widget: id=" + item.appWidgetId);
-                    getModelWriter().deleteWidgetInfo(item, getAppWidgetHolder(), removalReason);
-                    return null;
-                }
-
-                item.minSpanX = appWidgetInfo.minSpanX;
-                item.minSpanY = appWidgetInfo.minSpanY;
-                view = mAppWidgetHolder.createView(this, item.appWidgetId, appWidgetInfo);
-            } else if (!item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_ID_NOT_VALID)
-                    && appWidgetInfo != null) {
-                mAppWidgetHolder.addPendingView(item.appWidgetId,
-                        new PendingAppWidgetHostView(this, item, mIconCache, false));
-                view = mAppWidgetHolder.createView(this, item.appWidgetId, appWidgetInfo);
-            } else {
-                view = new PendingAppWidgetHostView(this, item, mIconCache, false);
-            }
-            prepareAppWidget(view, item);
         } finally {
             TraceHelper.INSTANCE.endSection();
         }
-
-        return view;
     }
 
     /**
@@ -2448,7 +2315,7 @@
      */
     private LauncherAppWidgetInfo completeRestoreAppWidget(int appWidgetId, int finalRestoreFlag) {
         LauncherAppWidgetHostView view = mWorkspace.getWidgetForAppWidgetId(appWidgetId);
-        if ((view == null) || !(view instanceof PendingAppWidgetHostView)) {
+        if (!(view instanceof PendingAppWidgetHostView)) {
             Log.e(TAG, "Widget update called, when the widget no longer exists.");
             return null;
         }
@@ -2459,8 +2326,9 @@
             info.pendingItemInfo = null;
         }
 
-        if (((PendingAppWidgetHostView) view).isReinflateIfNeeded()) {
-            view.reInflate();
+        PendingAppWidgetHostView pv = (PendingAppWidgetHostView) view;
+        if (pv.isReinflateIfNeeded()) {
+            pv.reInflate();
         }
 
         getModelWriter().updateItemInDatabase(info);
@@ -2905,6 +2773,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();
@@ -3189,7 +3065,7 @@
      * Handles an app pair launch; overridden in
      * {@link com.android.launcher3.uioverrides.QuickstepLauncher}
      */
-    public void launchAppPair(WorkspaceItemInfo app1, WorkspaceItemInfo app2) {
+    public void launchAppPair(AppPairIcon appPairIcon) {
         // Overridden
     }
 
diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java
index 9a19526..e015021 100644
--- a/src/com/android/launcher3/LauncherAppState.java
+++ b/src/com/android/launcher3/LauncherAppState.java
@@ -36,11 +36,7 @@
 import android.content.pm.LauncherApps;
 import android.os.UserHandle;
 import android.util.Log;
-import android.util.SparseArray;
-import android.widget.RemoteViews;
 
-import androidx.annotation.GuardedBy;
-import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.graphics.IconShape;
@@ -48,6 +44,7 @@
 import com.android.launcher3.icons.IconProvider;
 import com.android.launcher3.icons.LauncherIconProvider;
 import com.android.launcher3.icons.LauncherIcons;
+import com.android.launcher3.model.ModelLauncherCallbacks;
 import com.android.launcher3.notification.NotificationListener;
 import com.android.launcher3.pm.InstallSessionHelper;
 import com.android.launcher3.pm.InstallSessionTracker;
@@ -60,6 +57,7 @@
 import com.android.launcher3.util.SettingsCache;
 import com.android.launcher3.util.SimpleBroadcastReceiver;
 import com.android.launcher3.util.Themes;
+import com.android.launcher3.util.TraceHelper;
 import com.android.launcher3.widget.custom.CustomWidgetManager;
 
 public class LauncherAppState implements SafeCloseable {
@@ -75,13 +73,9 @@
     private final LauncherIconProvider mIconProvider;
     private final IconCache mIconCache;
     private final InvariantDeviceProfile mInvariantDeviceProfile;
-    private final RunnableList mOnTerminateCallback = new RunnableList();
+    private boolean mIsSafeModeEnabled;
 
-    // WORKAROUND: b/269335387 remove this after widget background listener is enabled
-    /* Array of RemoteViews cached by Launcher process */
-    @GuardedBy("itself")
-    @NonNull
-    public final SparseArray<RemoteViews> mCachedRemoteViews = new SparseArray<>();
+    private final RunnableList mOnTerminateCallback = new RunnableList();
 
     public static LauncherAppState getInstance(final Context context) {
         return INSTANCE.get(context);
@@ -100,15 +94,18 @@
         Log.v(Launcher.TAG, "LauncherAppState initiated");
         Preconditions.assertUIThread();
 
+        mIsSafeModeEnabled = TraceHelper.allowIpcs("isSafeMode",
+                () -> context.getPackageManager().isSafeMode());
         mInvariantDeviceProfile.addOnChangeListener(modelPropertiesChanged -> {
             if (modelPropertiesChanged) {
                 refreshAndReloadLauncher();
             }
         });
 
-        mContext.getSystemService(LauncherApps.class).registerCallback(mModel);
+        ModelLauncherCallbacks callbacks = mModel.newModelCallbacks();
+        mContext.getSystemService(LauncherApps.class).registerCallback(callbacks);
         mOnTerminateCallback.add(() ->
-                mContext.getSystemService(LauncherApps.class).unregisterCallback(mModel));
+                mContext.getSystemService(LauncherApps.class).unregisterCallback(callbacks));
 
         SimpleBroadcastReceiver modelChangeReceiver =
                 new SimpleBroadcastReceiver(mModel::onBroadcastIntent);
@@ -234,6 +231,10 @@
         return mInvariantDeviceProfile;
     }
 
+    public boolean isSafeModeEnabled() {
+        return mIsSafeModeEnabled;
+    }
+
     /**
      * Shorthand for {@link #getInvariantDeviceProfile()}
      */
diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java
index c81db63..d124746 100644
--- a/src/com/android/launcher3/LauncherModel.java
+++ b/src/com/android/launcher3/LauncherModel.java
@@ -20,6 +20,7 @@
 
 import static com.android.launcher3.LauncherAppState.ACTION_FORCE_ROLOAD;
 import static com.android.launcher3.config.FeatureFlags.IS_STUDIO_BUILD;
+import static com.android.launcher3.model.PackageUpdatedTask.OP_UPDATE;
 import static com.android.launcher3.pm.UserCache.ACTION_PROFILE_AVAILABLE;
 import static com.android.launcher3.pm.UserCache.ACTION_PROFILE_UNAVAILABLE;
 import static com.android.launcher3.testing.shared.TestProtocol.sDebugTracing;
@@ -28,7 +29,6 @@
 
 import android.content.Context;
 import android.content.Intent;
-import android.content.pm.LauncherApps;
 import android.content.pm.PackageInstaller;
 import android.content.pm.ShortcutInfo;
 import android.os.UserHandle;
@@ -43,7 +43,6 @@
 import com.android.launcher3.celllayout.CellPosMapper;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.icons.IconCache;
-import com.android.launcher3.logging.FileLog;
 import com.android.launcher3.model.AddWorkspaceItemsTask;
 import com.android.launcher3.model.AllAppsList;
 import com.android.launcher3.model.BaseModelUpdateTask;
@@ -55,8 +54,8 @@
 import com.android.launcher3.model.LoaderTask;
 import com.android.launcher3.model.ModelDbController;
 import com.android.launcher3.model.ModelDelegate;
+import com.android.launcher3.model.ModelLauncherCallbacks;
 import com.android.launcher3.model.ModelWriter;
-import com.android.launcher3.model.PackageIncrementalDownloadUpdatedTask;
 import com.android.launcher3.model.PackageInstallStateChangedTask;
 import com.android.launcher3.model.PackageUpdatedTask;
 import com.android.launcher3.model.ReloadStringCacheTask;
@@ -89,7 +88,7 @@
  * LauncherModel object held in a static. Also provide APIs for updating the database state
  * for the Launcher.
  */
-public class LauncherModel extends LauncherApps.Callback implements InstallSessionTracker.Callback {
+public class LauncherModel implements InstallSessionTracker.Callback {
     private static final boolean DEBUG_RECEIVER = false;
 
     static final String TAG = "Launcher.Model";
@@ -168,6 +167,10 @@
         return mModelDbController;
     }
 
+    public ModelLauncherCallbacks newModelCallbacks() {
+        return new ModelLauncherCallbacks(this::enqueueModelUpdateTask);
+    }
+
     /**
      * Adds the provided items to the workspace.
      */
@@ -186,77 +189,6 @@
                 owner);
     }
 
-    @Override
-    public void onPackageChanged(
-            @NonNull final String packageName, @NonNull final UserHandle user) {
-        int op = PackageUpdatedTask.OP_UPDATE;
-        enqueueModelUpdateTask(new PackageUpdatedTask(op, user, packageName));
-    }
-
-    @Override
-    public void onPackageRemoved(
-            @NonNull final String packageName, @NonNull final UserHandle user) {
-        onPackagesRemoved(user, packageName);
-    }
-
-    public void onPackagesRemoved(
-            @NonNull final UserHandle user, @NonNull final String... packages) {
-        int op = PackageUpdatedTask.OP_REMOVE;
-        FileLog.d(TAG, "package removed received " + TextUtils.join(",", packages));
-        enqueueModelUpdateTask(new PackageUpdatedTask(op, user, packages));
-    }
-
-    @Override
-    public void onPackageAdded(@NonNull final String packageName, @NonNull final UserHandle user) {
-        int op = PackageUpdatedTask.OP_ADD;
-        enqueueModelUpdateTask(new PackageUpdatedTask(op, user, packageName));
-    }
-
-    @Override
-    public void onPackagesAvailable(@NonNull final String[] packageNames,
-            @NonNull final UserHandle user, final boolean replacing) {
-        enqueueModelUpdateTask(
-                new PackageUpdatedTask(PackageUpdatedTask.OP_UPDATE, user, packageNames));
-    }
-
-    @Override
-    public void onPackagesUnavailable(@NonNull final String[] packageNames,
-            @NonNull final UserHandle user, final boolean replacing) {
-        if (!replacing) {
-            enqueueModelUpdateTask(new PackageUpdatedTask(
-                    PackageUpdatedTask.OP_UNAVAILABLE, user, packageNames));
-        }
-    }
-
-    @Override
-    public void onPackagesSuspended(
-            @NonNull final String[] packageNames, @NonNull final UserHandle user) {
-        enqueueModelUpdateTask(new PackageUpdatedTask(
-                PackageUpdatedTask.OP_SUSPEND, user, packageNames));
-    }
-
-    @Override
-    public void onPackagesUnsuspended(
-            @NonNull final String[] packageNames, @NonNull final UserHandle user) {
-        enqueueModelUpdateTask(new PackageUpdatedTask(
-                PackageUpdatedTask.OP_UNSUSPEND, user, packageNames));
-    }
-
-    @Override
-    public void onPackageLoadingProgressChanged(@NonNull final String packageName,
-            @NonNull final UserHandle user, final float progress) {
-        if (Utilities.ATLEAST_S) {
-            enqueueModelUpdateTask(new PackageIncrementalDownloadUpdatedTask(
-                    packageName, user, progress));
-        }
-    }
-
-    @Override
-    public void onShortcutsChanged(@NonNull final String packageName,
-            @NonNull final List<ShortcutInfo> shortcuts, @NonNull final UserHandle user) {
-        enqueueModelUpdateTask(new ShortcutsChangedTask(packageName, shortcuts, user, true));
-    }
-
     /**
      * Called when the icon for an app changes, outside of package event
      */
@@ -265,7 +197,7 @@
             @NonNull final UserHandle user) {
         // Update the icon for the calendar package
         Context context = mApp.getContext();
-        onPackageChanged(packageName, user);
+        enqueueModelUpdateTask(new PackageUpdatedTask(OP_UPDATE, user, packageName));
 
         List<ShortcutInfo> pinnedShortcuts = new ShortcutRequest(context, user)
                 .forPackage(packageName).query(ShortcutRequest.PINNED);
diff --git a/src/com/android/launcher3/LauncherPrefs.kt b/src/com/android/launcher3/LauncherPrefs.kt
index a05b0f5..067d150 100644
--- a/src/com/android/launcher3/LauncherPrefs.kt
+++ b/src/com/android/launcher3/LauncherPrefs.kt
@@ -34,6 +34,7 @@
 import com.android.launcher3.model.DeviceGridState
 import com.android.launcher3.pm.InstallSessionHelper
 import com.android.launcher3.provider.RestoreDbTask
+import com.android.launcher3.provider.RestoreDbTask.FIRST_LOAD_AFTER_RESTORE_KEY
 import com.android.launcher3.states.RotationHelper
 import com.android.launcher3.util.DisplayController
 import com.android.launcher3.util.MainThreadInitializedObject
@@ -302,15 +303,11 @@
         const val SHOULD_SHOW_SMARTSPACE_KEY = "SHOULD_SHOW_SMARTSPACE_KEY"
         @JvmField
         val ICON_STATE =
-            nonRestorableItem(
-                    "pref_icon_shape_path",
-                "",
-                EncryptionType.MOVE_TO_DEVICE_PROTECTED
-            )
+            nonRestorableItem("pref_icon_shape_path", "", EncryptionType.MOVE_TO_DEVICE_PROTECTED)
         @JvmField
         val ALL_APPS_OVERVIEW_THRESHOLD =
             nonRestorableItem(
-                    "pref_all_apps_overview_threshold",
+                "pref_all_apps_overview_threshold",
                 180,
                 EncryptionType.MOVE_TO_DEVICE_PROTECTED
             )
@@ -318,51 +315,54 @@
         val LONG_PRESS_NAV_HANDLE_SLOP_PERCENTAGE =
             nonRestorableItem(
                 "pref_long_press_nav_handle_slop_percentage",
-                        LPNH_SLOP_PERCENTAGE.get(),
-                        EncryptionType.MOVE_TO_DEVICE_PROTECTED
+                LPNH_SLOP_PERCENTAGE.get(),
+                EncryptionType.MOVE_TO_DEVICE_PROTECTED
             )
         @JvmField
         val LONG_PRESS_NAV_HANDLE_TIMEOUT_MS =
-                nonRestorableItem(
-                        "pref_long_press_nav_handle_timeout_ms",
-                        LPNH_TIMEOUT_MS.get(),
-                        EncryptionType.MOVE_TO_DEVICE_PROTECTED
-                )
+            nonRestorableItem(
+                "pref_long_press_nav_handle_timeout_ms",
+                LPNH_TIMEOUT_MS.get(),
+                EncryptionType.MOVE_TO_DEVICE_PROTECTED
+            )
         @JvmField
         val LONG_PRESS_NAV_HANDLE_HAPTIC_HINT_START_SCALE_PERCENT =
-                nonRestorableItem(
-                        "pref_long_press_nav_handle_haptic_hint_start_scale_percent",
-                        LPNH_HAPTIC_HINT_START_SCALE_PERCENT.get(),
-                        EncryptionType.MOVE_TO_DEVICE_PROTECTED
-                )
+            nonRestorableItem(
+                "pref_long_press_nav_handle_haptic_hint_start_scale_percent",
+                LPNH_HAPTIC_HINT_START_SCALE_PERCENT.get(),
+                EncryptionType.MOVE_TO_DEVICE_PROTECTED
+            )
         @JvmField
         val LONG_PRESS_NAV_HANDLE_HAPTIC_HINT_END_SCALE_PERCENT =
-                nonRestorableItem(
-                        "pref_long_press_nav_handle_haptic_hint_end_scale_percent",
-                        LPNH_HAPTIC_HINT_END_SCALE_PERCENT.get(),
-                        EncryptionType.MOVE_TO_DEVICE_PROTECTED
-                )
+            nonRestorableItem(
+                "pref_long_press_nav_handle_haptic_hint_end_scale_percent",
+                LPNH_HAPTIC_HINT_END_SCALE_PERCENT.get(),
+                EncryptionType.MOVE_TO_DEVICE_PROTECTED
+            )
         @JvmField
         val LONG_PRESS_NAV_HANDLE_HAPTIC_HINT_SCALE_EXPONENT =
-                nonRestorableItem(
-                        "pref_long_press_nav_handle_haptic_hint_scale_exponent",
-                        LPNH_HAPTIC_HINT_SCALE_EXPONENT.get(),
-                        EncryptionType.MOVE_TO_DEVICE_PROTECTED
-                )
+            nonRestorableItem(
+                "pref_long_press_nav_handle_haptic_hint_scale_exponent",
+                LPNH_HAPTIC_HINT_SCALE_EXPONENT.get(),
+                EncryptionType.MOVE_TO_DEVICE_PROTECTED
+            )
         @JvmField
         val LONG_PRESS_NAV_HANDLE_HAPTIC_HINT_ITERATIONS =
-                nonRestorableItem(
-                        "pref_long_press_nav_handle_haptic_hint_iterations",
-                        LPNH_HAPTIC_HINT_ITERATIONS.get(),
-                        EncryptionType.MOVE_TO_DEVICE_PROTECTED
-                )
+            nonRestorableItem(
+                "pref_long_press_nav_handle_haptic_hint_iterations",
+                LPNH_HAPTIC_HINT_ITERATIONS.get(),
+                EncryptionType.MOVE_TO_DEVICE_PROTECTED
+            )
         @JvmField
         val LONG_PRESS_NAV_HANDLE_HAPTIC_HINT_DELAY =
-                nonRestorableItem(
-                        "pref_long_press_nav_handle_haptic_hint_delay",
-                        LPNH_HAPTIC_HINT_DELAY.get(),
-                        EncryptionType.MOVE_TO_DEVICE_PROTECTED
-                )
+            nonRestorableItem(
+                "pref_long_press_nav_handle_haptic_hint_delay",
+                LPNH_HAPTIC_HINT_DELAY.get(),
+                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)
@@ -410,6 +410,13 @@
                 InvariantDeviceProfile.TYPE_PHONE,
                 EncryptionType.MOVE_TO_DEVICE_PROTECTED
             )
+        @JvmField
+        val IS_FIRST_LOAD_AFTER_RESTORE =
+            nonRestorableItem(
+                FIRST_LOAD_AFTER_RESTORE_KEY,
+                false,
+                EncryptionType.MOVE_TO_DEVICE_PROTECTED
+            )
         @JvmField val APP_WIDGET_IDS = backedUpItem(RestoreDbTask.APPWIDGET_IDS, "")
         @JvmField val OLD_APP_WIDGET_IDS = backedUpItem(RestoreDbTask.APPWIDGET_OLD_IDS, "")
         @JvmField
diff --git a/src/com/android/launcher3/SecondaryDropTarget.java b/src/com/android/launcher3/SecondaryDropTarget.java
index 2dd610cb..0b92c28 100644
--- a/src/com/android/launcher3/SecondaryDropTarget.java
+++ b/src/com/android/launcher3/SecondaryDropTarget.java
@@ -155,6 +155,9 @@
             }
             return INVALID;
         } else if (info.isPredictedItem()) {
+            if (Flags.enableShortcutDontSuggestApp()) {
+                return INVALID;
+            }
             return DISMISS_PREDICTION;
         }
 
diff --git a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
index e5a223a..55438fe 100644
--- a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
@@ -260,7 +260,7 @@
         mMainAdapterProvider = mSearchUiDelegate.createMainAdapterProvider();
         if (Flags.enablePrivateSpace()) {
             mPrivateSpaceHeaderViewController =
-                    new PrivateSpaceHeaderViewController(mPrivateProfileManager);
+                    new PrivateSpaceHeaderViewController(this, mPrivateProfileManager);
         }
 
         mAH.set(AdapterHolder.MAIN, new AdapterHolder(AdapterHolder.MAIN,
@@ -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();
@@ -980,6 +980,11 @@
         return mWorkManager;
     }
 
+    /** Returns whether Private Profile has been setup. */
+    public boolean hasPrivateProfile() {
+        return mHasPrivateApps;
+    }
+
     @Override
     public void onDeviceProfileChanged(DeviceProfile dp) {
         for (AdapterHolder holder : mAH) {
@@ -1150,14 +1155,15 @@
 
         applyAdapterSideAndBottomPaddings(grid);
 
-        MarginLayoutParams mlp = (MarginLayoutParams) getLayoutParams();
-        mlp.leftMargin = insets.left;
-        mlp.rightMargin = insets.right;
-        setLayoutParams(mlp);
+        // Ignore left/right insets on tablet because we are already centered in-screen.
+        if (grid.isPhone) {
+            MarginLayoutParams mlp = (MarginLayoutParams) getLayoutParams();
+            mlp.leftMargin = insets.left;
+            mlp.rightMargin = insets.right;
+            setLayoutParams(mlp);
+        }
 
-        if (grid.isVerticalBarLayout() && !FeatureFlags.enableResponsiveWorkspace()) {
-            setPadding(grid.workspacePadding.left, 0, grid.workspacePadding.right, 0);
-        } else {
+        if (!grid.isVerticalBarLayout() || FeatureFlags.enableResponsiveWorkspace()) {
             int topPadding = grid.allAppsPadding.top;
             if (isSearchBarFloating() && !grid.isTablet) {
                 topPadding += getResources().getDimensionPixelSize(
@@ -1314,6 +1320,10 @@
                 : mViewPager == null ? AdapterHolder.MAIN : mViewPager.getNextPage();
     }
 
+    public PrivateProfileManager getPrivateProfileManager() {
+        return mPrivateProfileManager;
+    }
+
     /**
      * Adds an update listener to animator that adds springs to the animation.
      */
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/AllAppsTransitionController.java b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
index 03ac9df..e2c5795 100644
--- a/src/com/android/launcher3/allapps/AllAppsTransitionController.java
+++ b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
@@ -342,7 +342,7 @@
             });
         }
 
-        if(FeatureFlags.ENABLE_PREMIUM_HAPTICS_ALL_APPS.get() && config.userControlled
+        if (FeatureFlags.ENABLE_PREMIUM_HAPTICS_ALL_APPS.get() && config.isUserControlled()
                 && Utilities.ATLEAST_S) {
             if (toState == ALL_APPS) {
                 builder.addOnFrameListener(
@@ -367,7 +367,7 @@
 
         // need to decide depending on the release velocity
         Interpolator verticalProgressInterpolator = config.getInterpolator(ANIM_VERTICAL_PROGRESS,
-                config.userControlled ? LINEAR : DECELERATE_1_7);
+                config.isUserControlled() ? LINEAR : DECELERATE_1_7);
         Animator anim = createSpringAnimation(mProgress, targetProgress);
         anim.setInterpolator(verticalProgressInterpolator);
         builder.add(anim);
diff --git a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
index 328516e..ad875e0 100644
--- a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
+++ b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
@@ -15,9 +15,14 @@
  */
 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;
+import androidx.annotation.VisibleForTesting;
 import androidx.recyclerview.widget.DiffUtil;
 
 import com.android.launcher3.Flags;
@@ -317,6 +322,8 @@
                     break;
                 case PrivateProfileManager.STATE_ENABLED:
                     // Add PS Apps only in Enabled State.
+                    mPrivateProviderManager.addPrivateSpaceInstallAppButton(mAdapterItems);
+                    position++;
                     addAppsWithSections(mPrivateApps, position);
                     break;
             }
@@ -325,8 +332,22 @@
 
     private void addAppsWithSections(List<AppInfo> appList, int startPosition) {
         String lastSectionName = null;
-        for (AppInfo info : appList) {
-            mAdapterItems.add(AdapterItem.asApp(info));
+        boolean hasPrivateApps = false;
+        if (mPrivateProviderManager != null) {
+            hasPrivateApps = appList.stream().
+                    allMatch(mPrivateProviderManager.getItemInfoMatcher());
+        }
+        for (int i = 0; i < appList.size(); i++) {
+            AppInfo info = appList.get(i);
+            // Apply decorator to private apps.
+            if (hasPrivateApps) {
+                mAdapterItems.add(AdapterItem.asAppWithDecorationInfo(info,
+                        new SectionDecorationInfo(mActivityContext.getApplicationContext(),
+                                getRoundRegions(i, appList.size()),
+                                true /* decorateTogether */)));
+            } else {
+                mAdapterItems.add(AdapterItem.asApp(info));
+            }
 
             String sectionName = info.sectionName;
             // Create a new section if the section names do not match
@@ -338,6 +359,43 @@
         }
     }
 
+    /**
+     * Determines the corner regions that should be rounded for a specific app icon based on its
+     * position in a grid. Apps that should only be cared about rounding are the apps in the last
+     * row. In the last row on the first column, the app should only be rounded on the bottom left.
+     * Apps in the middle would not be rounded and the last app on the last row will ALWAYS have a
+     * {@link SectionDecorationInfo#ROUND_BOTTOM_RIGHT}.
+     *
+     * @param appIndex The index of the app icon within the app list.
+     * @param appListSize The total number of apps within the app list.
+     * @return  An integer representing the corner regions to be rounded, using bitwise flags:
+     *          - {@link SectionDecorationInfo#ROUND_NOTHING}: No corners should be rounded.
+     *          - {@link SectionDecorationInfo#ROUND_TOP_LEFT}: Round the top-left corner.
+     *          - {@link SectionDecorationInfo#ROUND_TOP_RIGHT}: Round the top-right corner.
+     *          - {@link SectionDecorationInfo#ROUND_BOTTOM_LEFT}: Round the bottom-left corner.
+     *          - {@link SectionDecorationInfo#ROUND_BOTTOM_RIGHT}: Round the bottom-right corner.
+     */
+    @VisibleForTesting
+    int getRoundRegions(int appIndex, int appListSize) {
+        int numberOfAppRows = (int) Math.ceil((double) appListSize / mNumAppsPerRowAllApps);
+        int roundRegion = ROUND_NOTHING;
+        // App is in the last row.
+        if ((appIndex / mNumAppsPerRowAllApps) == numberOfAppRows - 1) {
+            if ((appIndex % mNumAppsPerRowAllApps) == 0) {
+                // App is the first column.
+                roundRegion = ROUND_BOTTOM_LEFT;
+            } else if ((appIndex % mNumAppsPerRowAllApps) == mNumAppsPerRowAllApps-1) {
+                // App is in the last column.
+                roundRegion = ROUND_BOTTOM_RIGHT;
+            }
+            // Ensure the last private app is rounded on the bottom right.
+            if (appIndex == appListSize - 1) {
+                roundRegion |= ROUND_BOTTOM_RIGHT;
+            }
+        }
+        return roundRegion;
+    }
+
     private static class MyDiffCallback extends DiffUtil.Callback {
 
         private final List<AdapterItem> mOldList;
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/FloatingHeaderView.java b/src/com/android/launcher3/allapps/FloatingHeaderView.java
index 1ba5f8e..a1f6ebe 100644
--- a/src/com/android/launcher3/allapps/FloatingHeaderView.java
+++ b/src/com/android/launcher3/allapps/FloatingHeaderView.java
@@ -225,10 +225,10 @@
         for (FloatingHeaderRow row : mAllRows) {
             row.setup(this, mAllRows, tabsHidden);
         }
-        updateExpectedHeight();
 
         mTabsHidden = tabsHidden;
         maybeSetTabVisibility(VISIBLE);
+        updateExpectedHeight();
         mMainRV = mainRV;
         mWorkRV = workRV;
         mSearchRV = searchRV;
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..aee511c 100644
--- a/src/com/android/launcher3/allapps/PrivateProfileManager.java
+++ b/src/com/android/launcher3/allapps/PrivateProfileManager.java
@@ -17,11 +17,15 @@
 package com.android.launcher3.allapps;
 
 import static com.android.launcher3.allapps.ActivityAllAppsContainerView.AdapterHolder.MAIN;
+import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_ICON;
 import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_PRIVATE_SPACE_HEADER;
+import static com.android.launcher3.allapps.SectionDecorationInfo.ROUND_NOTHING;
 import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_PRIVATE_PROFILE_QUIET_MODE_ENABLED;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 import static com.android.launcher3.util.SettingsCache.PRIVATE_SPACE_HIDE_WHEN_LOCKED_URI;
 
+import android.content.Context;
 import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
@@ -30,12 +34,20 @@
 
 import androidx.annotation.VisibleForTesting;
 
+import com.android.launcher3.BuildConfig;
+import com.android.launcher3.R;
+import com.android.launcher3.icons.BitmapInfo;
+import com.android.launcher3.icons.LauncherIcons;
 import com.android.launcher3.logging.StatsLogManager;
+import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.pm.UserCache;
+import com.android.launcher3.uioverrides.ApiWrapper;
 import com.android.launcher3.util.Preconditions;
 import com.android.launcher3.util.SettingsCache;
+import com.android.launcher3.util.UserIconInfo;
 
 import java.util.ArrayList;
+import java.util.List;
 import java.util.function.Predicate;
 
 /**
@@ -51,6 +63,7 @@
     private final Predicate<UserHandle> mPrivateProfileMatcher;
     private PrivateAppsSectionDecorator mPrivateAppsSectionDecorator;
     private boolean mPrivateSpaceSettingsAvailable;
+    private Runnable mUnlockRunnable;
 
     public PrivateProfileManager(UserManager userManager,
             ActivityAllAppsContainerView<?> allApps,
@@ -69,9 +82,50 @@
         return adapterItems.size();
     }
 
-    /** Disables quiet mode for Private Space User Profile. */
-    public void unlockPrivateProfile() {
+    /** Adds Private Space install app button to the layout. */
+    public void addPrivateSpaceInstallAppButton(List<BaseAllAppsAdapter.AdapterItem> adapterItems) {
+        Context context = mAllApps.getContext();
+        // Prepare intent
+        UserCache userCache = UserCache.getInstance(context);
+        UserHandle userHandle = userCache.getUserProfiles().stream()
+                .filter(user -> userCache.getUserInfo(user).type == UserIconInfo.TYPE_PRIVATE)
+                .findFirst()
+                .orElse(null);
+        Intent intent = ApiWrapper.getAppMarketActivityIntent(context,
+                BuildConfig.APPLICATION_ID, userHandle);
+
+        // Prepare bitmapInfo
+        Intent.ShortcutIconResource shortcut = Intent.ShortcutIconResource.fromContext(
+                context, com.android.launcher3.R.drawable.private_space_install_app_icon);
+        BitmapInfo bitmapInfo = LauncherIcons.obtain(context).createIconBitmap(shortcut);
+
+        AppInfo itemInfo = new AppInfo();
+        itemInfo.title = context.getResources().getString(R.string.ps_add_button_label);
+        itemInfo.intent = intent;
+        itemInfo.bitmap = bitmapInfo;
+        itemInfo.contentDescription = context.getResources().getString(
+                com.android.launcher3.R.string.ps_add_button_content_description);
+
+        BaseAllAppsAdapter.AdapterItem item = new BaseAllAppsAdapter.AdapterItem(VIEW_TYPE_ICON);
+        item.itemInfo = itemInfo;
+        item.decorationInfo = new SectionDecorationInfo(context, ROUND_NOTHING,
+                /* decorateTogether */ true);
+
+        adapterItems.add(item);
+        mAllApps.mAH.get(MAIN).mAdapter.notifyItemInserted(adapterItems.size() - 1);
+    }
+
+    /**
+     * Disables quiet mode for Private Space User Profile.
+     * The runnable passed will be executed in the {@link #reset()} method,
+     * when Launcher receives update about profile availability.
+     * The runnable passed is only executed once, and reset after execution.
+     * In case the method is called again, before the previously set runnable was executed,
+     * the runnable will be updated.
+     */
+    public void unlockPrivateProfile(Runnable runnable) {
         enableQuietMode(false);
+        mUnlockRunnable = runnable;
     }
 
     /** Enables quiet mode for Private Space User Profile. */
@@ -87,11 +141,15 @@
 
     /** Resets the current state of Private Profile, w.r.t. to Launcher. */
     public void reset() {
+        int previousState = getCurrentState();
         boolean isEnabled = !mAllApps.getAppsStore()
                 .hasModelFlag(FLAG_PRIVATE_PROFILE_QUIET_MODE_ENABLED);
         int updatedState = isEnabled ? STATE_ENABLED : STATE_DISABLED;
         setCurrentState(updatedState);
         resetPrivateSpaceDecorator(updatedState);
+        if (transitioningFromLockedToUnlocked(previousState, updatedState)) {
+            applyUnlockRunnable();
+        }
     }
 
     /** Opens the Private Space Settings Entry Point. */
@@ -125,7 +183,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++) {
@@ -150,6 +207,18 @@
         setQuietMode(enable);
     }
 
+    void applyUnlockRunnable() {
+        if (mUnlockRunnable != null) {
+            // reset the runnable to prevent re-execution.
+            MAIN_EXECUTOR.post(mUnlockRunnable);
+            mUnlockRunnable = null;
+        }
+    }
+
+    private boolean transitioningFromLockedToUnlocked(int previousState, int updatedState) {
+        return previousState == STATE_DISABLED && updatedState == STATE_ENABLED;
+    }
+
     @Override
     public Predicate<UserHandle> getUserMatcher() {
         return mPrivateProfileMatcher;
diff --git a/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewController.java b/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewController.java
index e0ca947..bc3269d 100644
--- a/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewController.java
+++ b/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewController.java
@@ -16,6 +16,7 @@
 
 package com.android.launcher3.allapps;
 
+import static com.android.launcher3.allapps.ActivityAllAppsContainerView.AdapterHolder.MAIN;
 import static com.android.launcher3.allapps.PrivateProfileManager.STATE_DISABLED;
 import static com.android.launcher3.allapps.PrivateProfileManager.STATE_ENABLED;
 import static com.android.launcher3.allapps.PrivateProfileManager.STATE_TRANSITION;
@@ -28,6 +29,7 @@
 import android.widget.ImageView;
 import android.widget.RelativeLayout;
 
+import com.android.launcher3.Flags;
 import com.android.launcher3.R;
 import com.android.launcher3.allapps.UserProfileManager.UserProfileState;
 
@@ -36,9 +38,13 @@
  * {@link UserProfileState}
  */
 public class PrivateSpaceHeaderViewController {
+    private static final int ANIMATION_DURATION = 2000;
+    private final ActivityAllAppsContainerView mAllApps;
     private final PrivateProfileManager mPrivateProfileManager;
 
-    public PrivateSpaceHeaderViewController(PrivateProfileManager privateProfileManager) {
+    public PrivateSpaceHeaderViewController(ActivityAllAppsContainerView allApps,
+            PrivateProfileManager privateProfileManager) {
+        this.mAllApps = allApps;
         this.mPrivateProfileManager = privateProfileManager;
     }
 
@@ -77,7 +83,8 @@
                 quietModeButton.setOnClickListener(
                         view -> {
                             mPrivateProfileManager.logEvents(LAUNCHER_PRIVATE_SPACE_UNLOCK_TAP);
-                            mPrivateProfileManager.unlockPrivateProfile();
+                            mPrivateProfileManager.unlockPrivateProfile((this::
+                                    onPrivateProfileUnlocked));
                         });
             }
             default -> quietModeButton.setVisibility(View.GONE);
@@ -105,4 +112,23 @@
             transitionImage.setVisibility(View.GONE);
         }
     }
+
+    private void onPrivateProfileUnlocked() {
+        // If we are on main adapter view, we apply the PS Container expansion animation and
+        // then scroll down to load the entire container, making animation visible.
+        ActivityAllAppsContainerView<?>.AdapterHolder mainAdapterHolder =
+                (ActivityAllAppsContainerView<?>.AdapterHolder) mAllApps.mAH.get(MAIN);
+        if (Flags.enablePrivateSpace() && Flags.privateSpaceAnimation()
+                && mAllApps.getActiveRecyclerView() == mainAdapterHolder.mRecyclerView) {
+            RecyclerViewAnimationController recyclerViewAnimationController =
+                    new RecyclerViewAnimationController(mAllApps);
+            recyclerViewAnimationController.animateToState(true /* expand */,
+                    ANIMATION_DURATION, () -> {});
+            mAllApps.getActiveRecyclerView().scrollToBottomWithMotion();
+        }
+    }
+
+    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..c438d19
--- /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 = 0;
+    public static final int ROUND_TOP_LEFT = 1 << 1;
+    public static final int ROUND_TOP_RIGHT = 1 << 2;
+    public static final int ROUND_BOTTOM_LEFT = 1 << 3;
+    public static final int ROUND_BOTTOM_RIGHT = 1 << 4;
+    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/allapps/UserProfileManager.java b/src/com/android/launcher3/allapps/UserProfileManager.java
index 0261010..8894f45 100644
--- a/src/com/android/launcher3/allapps/UserProfileManager.java
+++ b/src/com/android/launcher3/allapps/UserProfileManager.java
@@ -22,7 +22,6 @@
 import android.os.UserManager;
 
 import androidx.annotation.IntDef;
-import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.Utilities;
 import com.android.launcher3.logging.StatsLogManager;
@@ -89,11 +88,23 @@
     }
 
     /** Returns current state for the profile type. */
-    @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
     public int getCurrentState() {
         return mCurrentState;
     }
 
+    /** Returns if user profile is enabled. */
+    public boolean isEnabled() {
+        return mCurrentState == STATE_ENABLED;
+    }
+
+    /** Returns the UserHandle corresponding to the profile type, null in case no matches found. */
+    public UserHandle getProfileUser() {
+        return mUserCache.getUserProfiles().stream()
+                .filter(getUserMatcher())
+                .findAny()
+                .orElse(null);
+    }
+
     /** Logs Event to StatsLogManager. */
     protected void logEvents(StatsLogManager.EventEnum event) {
         mStatsLogManager.logger().log(event);
diff --git a/src/com/android/launcher3/apppairs/AppPairIcon.java b/src/com/android/launcher3/apppairs/AppPairIcon.java
index 46932fb..1d73441 100644
--- a/src/com/android/launcher3/apppairs/AppPairIcon.java
+++ b/src/com/android/launcher3/apppairs/AppPairIcon.java
@@ -17,11 +17,10 @@
 package com.android.launcher3.apppairs;
 
 import android.content.Context;
-import android.graphics.Canvas;
 import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
 import android.util.AttributeSet;
 import android.view.LayoutInflater;
+import android.view.View;
 import android.view.ViewGroup;
 import android.widget.FrameLayout;
 
@@ -33,7 +32,6 @@
 import com.android.launcher3.Reorderable;
 import com.android.launcher3.dragndrop.DraggableView;
 import com.android.launcher3.model.data.FolderInfo;
-import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.util.MultiTranslateDelegate;
 import com.android.launcher3.views.ActivityContext;
 
@@ -47,33 +45,8 @@
  * member apps are set into these rectangles.
  */
 public class AppPairIcon extends FrameLayout implements DraggableView, Reorderable {
-    /**
-     * Design specs -- the below ratios are in relation to the size of a standard app icon.
-     */
-    private static final float OUTER_PADDING_SCALE = 1 / 30f;
-    private static final float INNER_PADDING_SCALE = 1 / 24f;
-    private static final float MEMBER_ICON_SCALE = 11 / 30f;
-    private static final float CENTER_CHANNEL_SCALE = 1 / 30f;
-    private static final float BIG_RADIUS_SCALE = 1 / 5f;
-    private static final float SMALL_RADIUS_SCALE = 1 / 15f;
-
-    // App pair icons are slightly smaller than regular icons, so we pad the icon by this much on
-    // each side.
-    float mOuterPadding;
-    // Inside of the icon, the two member apps are padded by this much.
-    float mInnerPadding;
-    // The two member apps have icons that are this big (in diameter).
-    float mMemberIconSize;
-    // The size of the center channel.
-    float mCenterChannelSize;
-    // The large outer radius of the background rectangles.
-    float mBigRadius;
-    // The small inner radius of the background rectangles.
-    float mSmallRadius;
-    // The app pairs icon appears differently in portrait and landscape.
-    boolean mIsLandscape;
-
-    private ActivityContext mActivity;
+    // A view that holds the app pair icon graphic.
+    private AppPairIconGraphic mIconGraphic;
     // A view that holds the app pair's title.
     private BubbleTextView mAppPairName;
     // The underlying ItemInfo that stores info about the app pair members, etc.
@@ -109,7 +82,10 @@
         icon.setTag(appPairInfo);
         icon.setOnClickListener(activity.getItemOnClickListener());
         icon.mInfo = appPairInfo;
-        icon.mActivity = activity;
+
+        // Set up icon drawable area
+        icon.mIconGraphic = icon.findViewById(R.id.app_pair_icon_graphic);
+        icon.mIconGraphic.init(activity.getDeviceProfile(), icon);
 
         // Set up app pair title
         icon.mAppPairName = icon.findViewById(R.id.app_pair_icon_name);
@@ -127,85 +103,6 @@
         return icon;
     }
 
-    @Override
-    protected void dispatchDraw(Canvas canvas) {
-        super.dispatchDraw(canvas);
-
-        // Calculate device-specific measurements
-        DeviceProfile grid = mActivity.getDeviceProfile();
-        int defaultIconSize = grid.iconSizePx;
-        mOuterPadding = OUTER_PADDING_SCALE * defaultIconSize;
-        mInnerPadding = INNER_PADDING_SCALE * defaultIconSize;
-        mMemberIconSize = MEMBER_ICON_SCALE * defaultIconSize;
-        mCenterChannelSize = CENTER_CHANNEL_SCALE * defaultIconSize;
-        mBigRadius = BIG_RADIUS_SCALE * defaultIconSize;
-        mSmallRadius = SMALL_RADIUS_SCALE * defaultIconSize;
-        mIsLandscape = grid.isLeftRightSplit;
-
-        // Calculate drawable area position
-        float leftBound = (canvas.getWidth() / 2f) - (defaultIconSize / 2f);
-        float topBound = getPaddingTop();
-
-        // Prepare to draw app pair icon background
-        Drawable background = new AppPairIconBackground(getContext(), this);
-        background.setBounds(0, 0, defaultIconSize, defaultIconSize);
-
-        // Draw background
-        canvas.save();
-        canvas.translate(leftBound, topBound);
-        background.draw(canvas);
-        canvas.restore();
-
-        // Prepare to draw icons
-        WorkspaceItemInfo app1 = mInfo.contents.get(0);
-        WorkspaceItemInfo app2 = mInfo.contents.get(1);
-        Drawable app1Icon = app1.newIcon(getContext());
-        Drawable app2Icon = app2.newIcon(getContext());
-        app1Icon.setBounds(0, 0, defaultIconSize, defaultIconSize);
-        app2Icon.setBounds(0, 0, defaultIconSize, defaultIconSize);
-
-        // Draw first icon
-        canvas.save();
-        canvas.translate(leftBound, topBound);
-        // The app icons are placed differently depending on device orientation.
-        if (mIsLandscape) {
-            canvas.translate(
-                    (defaultIconSize / 2f) - (mCenterChannelSize / 2f) - mInnerPadding
-                            - mMemberIconSize,
-                    (defaultIconSize / 2f) - (mMemberIconSize / 2f)
-            );
-        } else {
-            canvas.translate(
-                    (defaultIconSize / 2f) - (mMemberIconSize / 2f),
-                    (defaultIconSize / 2f) - (mCenterChannelSize / 2f) - mInnerPadding
-                            - mMemberIconSize
-            );
-
-        }
-        canvas.scale(MEMBER_ICON_SCALE, MEMBER_ICON_SCALE);
-        app1Icon.draw(canvas);
-        canvas.restore();
-
-        // Draw second icon
-        canvas.save();
-        canvas.translate(leftBound, topBound);
-        // The app icons are placed differently depending on device orientation.
-        if (mIsLandscape) {
-            canvas.translate(
-                    (defaultIconSize / 2f) + (mCenterChannelSize / 2f) + mInnerPadding,
-                    (defaultIconSize / 2f) - (mMemberIconSize / 2f)
-            );
-        } else {
-            canvas.translate(
-                    (defaultIconSize / 2f) - (mMemberIconSize / 2f),
-                    (defaultIconSize / 2f) + (mCenterChannelSize / 2f) + mInnerPadding
-            );
-        }
-        canvas.scale(MEMBER_ICON_SCALE, MEMBER_ICON_SCALE);
-        app2Icon.draw(canvas);
-        canvas.restore();
-    }
-
     /**
      * Returns a formatted accessibility title for app pairs.
      */
@@ -221,8 +118,8 @@
 
     // Required for DraggableView
     @Override
-    public void getWorkspaceVisualDragBounds(Rect bounds) {
-        mAppPairName.getIconBounds(bounds);
+    public void getWorkspaceVisualDragBounds(Rect outBounds) {
+        mIconGraphic.getIconBounds(outBounds);
     }
 
     /** Sets the visibility of the icon's title text */
@@ -257,4 +154,8 @@
     public FolderInfo getInfo() {
         return mInfo;
     }
+
+    public View getIconDrawableArea() {
+        return mIconGraphic;
+    }
 }
diff --git a/src/com/android/launcher3/apppairs/AppPairIconBackground.java b/src/com/android/launcher3/apppairs/AppPairIconBackground.java
index 735c82f..4e60ece 100644
--- a/src/com/android/launcher3/apppairs/AppPairIconBackground.java
+++ b/src/com/android/launcher3/apppairs/AppPairIconBackground.java
@@ -32,8 +32,8 @@
  * A Drawable for the background behind the twin app icons (looks like two rectangles).
  */
 class AppPairIconBackground extends Drawable {
-    // The icon that we will draw this background on.
-    private final AppPairIcon icon;
+    // The underlying view that we are drawing this background on.
+    private final AppPairIconGraphic icon;
     private final Paint mBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
 
     /**
@@ -44,8 +44,8 @@
     private static final RectF EMPTY_RECT = new RectF();
     private static final float[] ARRAY_OF_ZEROES = new float[8];
 
-    AppPairIconBackground(Context context, AppPairIcon appPairIcon) {
-        icon = appPairIcon;
+    AppPairIconBackground(Context context, AppPairIconGraphic iconGraphic) {
+        icon = iconGraphic;
         // Set up background paint color
         TypedArray ta = context.getTheme().obtainStyledAttributes(R.styleable.FolderIconPreview);
         mBackgroundPaint.setStyle(Paint.Style.FILL);
@@ -56,7 +56,7 @@
 
     @Override
     public void draw(Canvas canvas) {
-        if (icon.mIsLandscape) {
+        if (icon.isLeftRightSplit()) {
             drawLeftRightSplit(canvas);
         } else {
             drawTopBottomSplit(canvas);
@@ -73,29 +73,29 @@
 
         // The left half of the background image, excluding center channel
         RectF leftSide = new RectF(
-                icon.mOuterPadding,
-                icon.mOuterPadding,
-                (width / 2f) - (icon.mCenterChannelSize / 2f),
-                height - icon.mOuterPadding
+                0,
+                0,
+                (width / 2f) - (icon.getCenterChannelSize() / 2f),
+                height
         );
         // The right half of the background image, excluding center channel
         RectF rightSide = new RectF(
-                (width / 2f) + (icon.mCenterChannelSize / 2f),
-                icon.mOuterPadding,
-                width - icon.mOuterPadding,
-                height - icon.mOuterPadding
+                (width / 2f) + (icon.getCenterChannelSize() / 2f),
+                0,
+                width,
+                height
         );
 
         drawCustomRoundedRect(canvas, leftSide, new float[]{
-                icon.mBigRadius, icon.mBigRadius,
-                icon.mSmallRadius, icon.mSmallRadius,
-                icon.mSmallRadius, icon.mSmallRadius,
-                icon.mBigRadius, icon.mBigRadius});
+                icon.getBigRadius(), icon.getBigRadius(),
+                icon.getSmallRadius(), icon.getSmallRadius(),
+                icon.getSmallRadius(), icon.getSmallRadius(),
+                icon.getBigRadius(), icon.getBigRadius()});
         drawCustomRoundedRect(canvas, rightSide, new float[]{
-                icon.mSmallRadius, icon.mSmallRadius,
-                icon.mBigRadius, icon.mBigRadius,
-                icon.mBigRadius, icon.mBigRadius,
-                icon.mSmallRadius, icon.mSmallRadius});
+                icon.getSmallRadius(), icon.getSmallRadius(),
+                icon.getBigRadius(), icon.getBigRadius(),
+                icon.getBigRadius(), icon.getBigRadius(),
+                icon.getSmallRadius(), icon.getSmallRadius()});
     }
 
     /**
@@ -108,29 +108,29 @@
 
         // The top half of the background image, excluding center channel
         RectF topSide = new RectF(
-                icon.mOuterPadding,
-                icon.mOuterPadding,
-                width - icon.mOuterPadding,
-                (height / 2f) - (icon.mCenterChannelSize / 2f)
+                0,
+                0,
+                width,
+                (height / 2f) - (icon.getCenterChannelSize() / 2f)
         );
         // The bottom half of the background image, excluding center channel
         RectF bottomSide = new RectF(
-                icon.mOuterPadding,
-                (height / 2f) + (icon.mCenterChannelSize / 2f),
-                width - icon.mOuterPadding,
-                height - icon.mOuterPadding
+                0,
+                (height / 2f) + (icon.getCenterChannelSize() / 2f),
+                width,
+                height
         );
 
         drawCustomRoundedRect(canvas, topSide, new float[]{
-                icon.mBigRadius, icon.mBigRadius,
-                icon.mBigRadius, icon.mBigRadius,
-                icon.mSmallRadius, icon.mSmallRadius,
-                icon.mSmallRadius, icon.mSmallRadius});
+                icon.getBigRadius(), icon.getBigRadius(),
+                icon.getBigRadius(), icon.getBigRadius(),
+                icon.getSmallRadius(), icon.getSmallRadius(),
+                icon.getSmallRadius(), icon.getSmallRadius()});
         drawCustomRoundedRect(canvas, bottomSide, new float[]{
-                icon.mSmallRadius, icon.mSmallRadius,
-                icon.mSmallRadius, icon.mSmallRadius,
-                icon.mBigRadius, icon.mBigRadius,
-                icon.mBigRadius, icon.mBigRadius});
+                icon.getSmallRadius(), icon.getSmallRadius(),
+                icon.getSmallRadius(), icon.getSmallRadius(),
+                icon.getBigRadius(), icon.getBigRadius(),
+                icon.getBigRadius(), icon.getBigRadius()});
     }
 
     /**
@@ -146,7 +146,7 @@
             c.drawDoubleRoundRect(rect, radii, EMPTY_RECT, ARRAY_OF_ZEROES, mBackgroundPaint);
         } else {
             // Fallback rectangle with uniform rounded corners
-            c.drawRoundRect(rect, icon.mBigRadius, icon.mBigRadius, mBackgroundPaint);
+            c.drawRoundRect(rect, icon.getBigRadius(), icon.getBigRadius(), mBackgroundPaint);
         }
     }
 
diff --git a/src/com/android/launcher3/apppairs/AppPairIconGraphic.kt b/src/com/android/launcher3/apppairs/AppPairIconGraphic.kt
new file mode 100644
index 0000000..b2497a3
--- /dev/null
+++ b/src/com/android/launcher3/apppairs/AppPairIconGraphic.kt
@@ -0,0 +1,179 @@
+/*
+ * 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.apppairs
+
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Rect
+import android.graphics.drawable.Drawable
+import android.util.AttributeSet
+import android.util.Log
+import android.view.Gravity
+import android.widget.FrameLayout
+import com.android.launcher3.DeviceProfile
+import com.android.launcher3.icons.BitmapInfo
+import com.android.launcher3.icons.PlaceHolderIconDrawable
+import com.android.launcher3.model.data.WorkspaceItemInfo
+import com.android.launcher3.util.Themes
+
+/**
+ * A FrameLayout marking the area on an [AppPairIcon] where the visual icon will be drawn. One of
+ * two child UI elements on an [AppPairIcon], along with a BubbleTextView holding the text title.
+ */
+class AppPairIconGraphic @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
+    FrameLayout(context, attrs) {
+    private val TAG = "AppPairIconGraphic"
+
+    companion object {
+        // Design specs -- the below ratios are in relation to the size of a standard app icon.
+        private const val OUTER_PADDING_SCALE = 1 / 30f
+        private const val INNER_PADDING_SCALE = 1 / 24f
+        private const val MEMBER_ICON_SCALE = 11 / 30f
+        private const val CENTER_CHANNEL_SCALE = 1 / 30f
+        private const val BIG_RADIUS_SCALE = 1 / 5f
+        private const val SMALL_RADIUS_SCALE = 1 / 15f
+    }
+
+    // App pair icons are slightly smaller than regular icons, so we pad the icon by this much on
+    // each side.
+    private var outerPadding = 0f
+    // Inside of the icon, the two member apps are padded by this much.
+    private var innerPadding = 0f
+    // The colored background (two rectangles in a square area) is this big.
+    private var backgroundSize = 0f
+    // The two member apps have icons that are this big (in diameter).
+    private var memberIconSize = 0f
+    // The size of the center channel.
+    var centerChannelSize = 0f
+    // The large outer radius of the background rectangles.
+    var bigRadius = 0f
+    // The small inner radius of the background rectangles.
+    var smallRadius = 0f
+    // The app pairs icon appears differently in portrait and landscape.
+    var isLeftRightSplit = false
+
+    private lateinit var parentIcon: AppPairIcon
+    private lateinit var appPairBackground: Drawable
+    private var appIcon1: Drawable? = null
+    private var appIcon2: Drawable? = null
+
+    fun init(grid: DeviceProfile, icon: AppPairIcon) {
+        // Calculate device-specific measurements
+        val defaultIconSize = grid.iconSizePx
+        outerPadding = OUTER_PADDING_SCALE * defaultIconSize
+        innerPadding = INNER_PADDING_SCALE * defaultIconSize
+        backgroundSize = defaultIconSize - outerPadding * 2
+        memberIconSize = MEMBER_ICON_SCALE * defaultIconSize
+        centerChannelSize = CENTER_CHANNEL_SCALE * defaultIconSize
+        bigRadius = BIG_RADIUS_SCALE * defaultIconSize
+        smallRadius = SMALL_RADIUS_SCALE * defaultIconSize
+        isLeftRightSplit = grid.isLeftRightSplit
+        parentIcon = icon
+
+        appPairBackground = AppPairIconBackground(context, this)
+        appPairBackground.setBounds(0, 0, backgroundSize.toInt(), backgroundSize.toInt())
+        applyIcons(parentIcon.info.contents)
+    }
+
+    /** Sets up app pair member icons for drawing. */
+    private fun applyIcons(contents: ArrayList<WorkspaceItemInfo>) {
+        // App pair should always contain 2 members; if not 2, return to avoid a crash loop
+        if (contents.size != 2) {
+            Log.w(TAG, "AppPair contents not 2, size: " + contents.size, Throwable())
+            return
+        }
+
+        // Generate new icons, using themed flag if needed
+        val flags = if (Themes.isThemedIconEnabled(context)) BitmapInfo.FLAG_THEMED else 0
+        val newIcon1 = parentIcon.info.contents[0].newIcon(context, flags)
+        val newIcon2 = parentIcon.info.contents[1].newIcon(context, flags)
+
+        // If app icons did not draw fully last time, animate to full icon
+        (appIcon1 as? PlaceHolderIconDrawable)?.animateIconUpdate(newIcon1)
+        (appIcon2 as? PlaceHolderIconDrawable)?.animateIconUpdate(newIcon2)
+
+        appIcon1 = newIcon1
+        appIcon2 = newIcon2
+        appIcon1?.setBounds(0, 0, memberIconSize.toInt(), memberIconSize.toInt())
+        appIcon2?.setBounds(0, 0, memberIconSize.toInt(), memberIconSize.toInt())
+    }
+
+
+    /** Gets this icon graphic's bounds, with respect to the parent icon's coordinate system. */
+    fun getIconBounds(outBounds: Rect) {
+        outBounds.set(0, 0, backgroundSize.toInt(), backgroundSize.toInt())
+        outBounds.offset(
+            // x-coordinate in parent's coordinate system
+            ((parentIcon.width - backgroundSize) / 2).toInt(),
+            // y-coordinate in parent's coordinate system
+            parentIcon.paddingTop + outerPadding.toInt()
+        )
+    }
+
+    override fun dispatchDraw(canvas: Canvas) {
+        super.dispatchDraw(canvas)
+
+        // Center the drawable area in the larger icon canvas
+        val lp: LayoutParams = layoutParams as LayoutParams
+        lp.gravity = Gravity.CENTER_HORIZONTAL
+        lp.topMargin = outerPadding.toInt()
+        lp.height = backgroundSize.toInt()
+        lp.width = backgroundSize.toInt()
+        layoutParams = lp
+
+        // Draw background
+        appPairBackground.draw(canvas)
+
+        // Make sure icons are loaded
+        if (
+            appIcon1 == null ||
+                appIcon2 == null ||
+                appIcon1 is PlaceHolderIconDrawable ||
+                appIcon2 is PlaceHolderIconDrawable
+        ) {
+            applyIcons(parentIcon.info.contents)
+        }
+
+        // Draw first icon
+        canvas.save()
+        // The app icons are placed differently depending on device orientation.
+        if (isLeftRightSplit) {
+            canvas.translate(innerPadding, height / 2f - memberIconSize / 2f)
+        } else {
+            canvas.translate(width / 2f - memberIconSize / 2f, innerPadding)
+        }
+        appIcon1?.draw(canvas)
+        canvas.restore()
+
+        // Draw second icon
+        canvas.save()
+        // The app icons are placed differently depending on device orientation.
+        if (isLeftRightSplit) {
+            canvas.translate(
+                width - (innerPadding + memberIconSize),
+                height / 2f - memberIconSize / 2f
+            )
+        } else {
+            canvas.translate(
+                width / 2f - memberIconSize / 2f,
+                height - (innerPadding + memberIconSize)
+            )
+        }
+        appIcon2?.draw(canvas)
+        canvas.restore()
+    }
+}
diff --git a/src/com/android/launcher3/backuprestore/LauncherRestoreEventLogger.kt b/src/com/android/launcher3/backuprestore/LauncherRestoreEventLogger.kt
new file mode 100644
index 0000000..e6654b1
--- /dev/null
+++ b/src/com/android/launcher3/backuprestore/LauncherRestoreEventLogger.kt
@@ -0,0 +1,132 @@
+package com.android.launcher3.backuprestore
+
+import android.content.Context
+import androidx.annotation.StringDef
+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 {
+
+    /** Enumeration of potential errors returned to calls of pause/resume app updates. */
+    @Retention(AnnotationRetention.SOURCE)
+    @StringDef(
+        RestoreError.PROFILE_DELETED,
+        RestoreError.MISSING_INFO,
+        RestoreError.MISSING_WIDGET_PROVIDER,
+        RestoreError.INVALID_LOCATION,
+        RestoreError.SHORTCUT_NOT_FOUND,
+        RestoreError.APP_NOT_INSTALLED,
+        RestoreError.WIDGETS_DISABLED,
+        RestoreError.PROFILE_NOT_RESTORED,
+        RestoreError.WIDGET_REMOVED,
+        RestoreError.GRID_MIGRATION_FAILURE,
+        RestoreError.NO_SEARCH_WIDGET,
+        RestoreError.INVALID_WIDGET_ID
+    )
+    annotation class RestoreError {
+        companion object {
+            const val PROFILE_DELETED = "user_profile_deleted"
+            const val MISSING_INFO = "missing_information_when_loading"
+            const val MISSING_WIDGET_PROVIDER = "missing_widget_provider"
+            const val INVALID_LOCATION = "invalid_size_or_location"
+            const val SHORTCUT_NOT_FOUND = "shortcut_not_found"
+            const val APP_NOT_INSTALLED = "app_not_installed"
+            const val WIDGETS_DISABLED = "widgets_disabled"
+            const val PROFILE_NOT_RESTORED = "profile_not_restored"
+            const val WIDGET_REMOVED = "widget_not_found"
+            const val GRID_MIGRATION_FAILURE = "grid_migration_failed"
+            const val NO_SEARCH_WIDGET = "no_search_widget"
+            const val INVALID_WIDGET_ID = "invalid_widget_id"
+        }
+    }
+
+    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 successfully restoring multiple items from the Favorites table.
+     *
+     * @param favoritesId The id of the item type from [Favorites] that was restored.
+     * @param count number of items that restored.
+     */
+    open fun logFavoritesItemsRestored(favoritesId: Int, count: 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, @RestoreError 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,
+        @RestoreError 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 1e5316f..1994bd1 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -169,10 +169,6 @@
     // TODO(Block 8): Clean up flags
 
     // TODO(Block 9): Clean up flags
-    public static final BooleanFlag UNFOLDED_WIDGET_PICKER = getDebugFlag(301918659,
-            "UNFOLDED_WIDGET_PICKER", DISABLED, "Enable new widget picker that takes "
-                    + "advantage of the unfolded foldable format");
-
     public static final BooleanFlag MULTI_SELECT_EDIT_MODE = getDebugFlag(270709220,
             "MULTI_SELECT_EDIT_MODE", DISABLED, "Enable new multi-select edit mode "
                     + "for home screen");
@@ -201,10 +197,6 @@
             "ENABLE_PARAMETRIZE_REORDER", DISABLED,
             "Enables generating the reorder using a set of parameters");
 
-    public static final BooleanFlag ENABLE_NO_LONG_PRESS_DRAG = getDebugFlag(299748096,
-            "ENABLE_NO_LONG_PRESS_DRAG", ENABLED,
-            "Don't trigger the drag if we are still under long press");
-
     // TODO(Block 12): Clean up flags
     public static final BooleanFlag ENABLE_MULTI_INSTANCE = getDebugFlag(270396680,
             "ENABLE_MULTI_INSTANCE", DISABLED,
@@ -231,7 +223,7 @@
                 // Task bar pinning and task bar nav bar unification are both dependent on
                 // ENABLE_TASKBAR_NO_RECREATION. We want to turn ENABLE_TASKBAR_NO_RECREATION on
                 // when either of the dependent features is turned on.
-                || ENABLE_TASKBAR_PINNING.get() || ENABLE_TASKBAR_NAVBAR_UNIFICATION;
+                || enableTaskbarPinning() || ENABLE_TASKBAR_NAVBAR_UNIFICATION;
     }
 
     // TODO(Block 16): Clean up flags
@@ -308,12 +300,12 @@
                     "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
     // Aconfig migration complete for ENABLE_TASKBAR_PINNING.
-    private static final BooleanFlag ENABLE_TASKBAR_PINNING = getDebugFlag(270396583,
+    private static final BooleanFlag ENABLE_TASKBAR_PINNING = getDebugFlag(296231746,
             "ENABLE_TASKBAR_PINNING", TEAMFOOD,
             "Enables taskbar pinning to allow user to switch between transient and persistent "
                     + "taskbar flavors");
@@ -387,10 +379,6 @@
             "ENABLE_NEW_MIGRATION_LOGIC", ENABLED,
             "Enable the new grid migration logic, keeping pages when src < dest");
 
-    public static final BooleanFlag ENABLE_CACHED_WIDGET = getDebugFlag(270395008,
-            "ENABLE_CACHED_WIDGET", ENABLED,
-            "Show previously cached widgets as opposed to deferred widget where available");
-
     // TODO(Block 25): Clean up flags
     public static final BooleanFlag ENABLE_NEW_GESTURE_NAV_TUTORIAL = getDebugFlag(270396257,
             "ENABLE_NEW_GESTURE_NAV_TUTORIAL", ENABLED,
@@ -476,10 +464,10 @@
 
     // TODO(Block 33): Clean up flags
     public static final BooleanFlag ENABLE_ALL_APPS_RV_PREINFLATION = getDebugFlag(288161355,
-            "ENABLE_ALL_APPS_RV_PREINFLATION", TEAMFOOD,
+            "ENABLE_ALL_APPS_RV_PREINFLATION", ENABLED,
             "Enables preinflating all apps icons to avoid scrolling jank.");
     public static final BooleanFlag ALL_APPS_GONE_VISIBILITY = getDebugFlag(291651514,
-            "ALL_APPS_GONE_VISIBILITY", TEAMFOOD,
+            "ALL_APPS_GONE_VISIBILITY", ENABLED,
             "Set all apps container view's hidden visibility to GONE instead of INVISIBLE.");
 
     // TODO(Block 34): Empty block
diff --git a/src/com/android/launcher3/dragndrop/DragController.java b/src/com/android/launcher3/dragndrop/DragController.java
index 777f4d5..aa5329b 100644
--- a/src/com/android/launcher3/dragndrop/DragController.java
+++ b/src/com/android/launcher3/dragndrop/DragController.java
@@ -17,7 +17,6 @@
 package com.android.launcher3.dragndrop;
 
 import static com.android.launcher3.Utilities.ATLEAST_Q;
-import static com.android.launcher3.config.FeatureFlags.ENABLE_NO_LONG_PRESS_DRAG;
 
 import android.graphics.Point;
 import android.graphics.Rect;
@@ -464,7 +463,7 @@
 
     private DropTarget checkTouchMove(final int x, final int y) {
         // If we are in predrag, don't trigger any other event until we get out of it
-        if (ENABLE_NO_LONG_PRESS_DRAG.get() && mIsInPreDrag) {
+        if (mIsInPreDrag) {
             return mLastDropTarget;
         }
         DropTarget dropTarget = findDropTarget(x, y);
diff --git a/src/com/android/launcher3/folder/PreviewItemManager.java b/src/com/android/launcher3/folder/PreviewItemManager.java
index b39e968e..871643e 100644
--- a/src/com/android/launcher3/folder/PreviewItemManager.java
+++ b/src/com/android/launcher3/folder/PreviewItemManager.java
@@ -37,6 +37,7 @@
 import android.view.View;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.BubbleTextView;
 import com.android.launcher3.Utilities;
@@ -71,7 +72,8 @@
 
     private final Context mContext;
     private final FolderIcon mIcon;
-    private final int mIconSize;
+    @VisibleForTesting
+    public final int mIconSize;
 
     // These variables are all associated with the drawing of the preview; they are stored
     // as member variables for shared usage and to avoid computation on each frame
@@ -428,7 +430,8 @@
         p.anim = anim;
     }
 
-    private void setDrawable(PreviewItemDrawingParams p, WorkspaceItemInfo item) {
+    @VisibleForTesting
+    public void setDrawable(PreviewItemDrawingParams p, WorkspaceItemInfo item) {
         if (item.hasPromiseIconUi() || (item.runtimeStatusFlags
                     & ItemInfoWithIcon.FLAG_SHOW_DOWNLOAD_PROGRESS_MASK) != 0) {
             PreloadIconDrawable drawable = newPendingIcon(mContext, item);
diff --git a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
index 3330448..7dcc8a8 100644
--- a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
+++ b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
@@ -35,6 +35,7 @@
 import android.appwidget.AppWidgetProviderInfo;
 import android.content.Context;
 import android.content.ContextWrapper;
+import android.content.res.Configuration;
 import android.content.res.TypedArray;
 import android.graphics.PointF;
 import android.graphics.Rect;
@@ -197,7 +198,7 @@
         mUiHandler = new Handler(Looper.getMainLooper());
         mContext = context;
         mIdp = idp;
-        mDp = idp.getDeviceProfile(context).toBuilder(context).setViewScaleProvider(
+        mDp = getDeviceProfileForPreview(context).toBuilder(context).setViewScaleProvider(
                 this::getAppWidgetScale).build();
         if (context instanceof PreviewContext) {
             Context tempContext = ((PreviewContext) context).getBaseContext();
@@ -260,6 +261,21 @@
     }
 
     /**
+     * Returns the device profile based on resource configuration for previewing various display
+     * sizes
+     */
+    private DeviceProfile getDeviceProfileForPreview(Context context) {
+        float density = context.getResources().getDisplayMetrics().density;
+        Configuration config = context.getResources().getConfiguration();
+
+        return mIdp.getBestMatch(
+                config.screenWidthDp * density,
+                config.screenHeightDp * density,
+                WindowManagerProxy.INSTANCE.get(context).getRotation(context)
+        );
+    }
+
+    /**
      * Returns the insets of the screen closest to the display given by the context
      */
     private Rect getInsets(Context context) {
diff --git a/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java b/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java
index 683354b..ec6b94d 100644
--- a/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java
+++ b/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java
@@ -259,7 +259,7 @@
                         query += " or " + LauncherSettings.Favorites.SCREEN + " = "
                                 + Workspace.SECOND_SCREEN_ID;
                     }
-                    loadWorkspace(new ArrayList<>(), query, null);
+                    loadWorkspace(new ArrayList<>(), query, null, null);
 
                     final SparseArray<Size> spanInfo =
                             getLoadedLauncherWidgetInfo(previewContext.getBaseContext());
diff --git a/src/com/android/launcher3/keyboard/FocusIndicatorHelper.java b/src/com/android/launcher3/keyboard/FocusIndicatorHelper.java
index 83003ff..c36f455 100644
--- a/src/com/android/launcher3/keyboard/FocusIndicatorHelper.java
+++ b/src/com/android/launcher3/keyboard/FocusIndicatorHelper.java
@@ -20,7 +20,9 @@
 import android.view.View;
 import android.view.View.OnFocusChangeListener;
 
+import com.android.launcher3.Flags;
 import com.android.launcher3.R;
+import com.android.launcher3.util.Themes;
 
 /**
  * A helper class to draw background of a focused view.
@@ -29,7 +31,9 @@
         implements OnFocusChangeListener {
 
     public FocusIndicatorHelper(View container) {
-        super(container, container.getResources().getColor(R.color.focused_background));
+        super(container, Flags.enableFocusOutline() ? Themes.getAttrColor(container.getContext(),
+                R.attr.focusOutlineColor)
+                : container.getResources().getColor(R.color.focused_background));
     }
 
     @Override
@@ -53,7 +57,18 @@
 
         @Override
         public void viewToRect(View v, Rect outRect) {
-            outRect.set(v.getLeft(), v.getTop(), v.getRight(), v.getBottom());
+            if (Flags.enableFocusOutline()) {
+                // Ensure the left and top would not be negative and drawn outside of canvas
+                outRect.set(Math.max(0, v.getLeft()), Math.max(0, v.getTop()), v.getRight(),
+                        v.getBottom());
+                // Stroke is drawn with half outside and half inside the view. Inset by half
+                // stroke width to move the whole stroke inside the view and avoid other views
+                // occluding it
+                int halfStrokeWidth = (int) mPaint.getStrokeWidth() / 2;
+                outRect.inset(halfStrokeWidth, halfStrokeWidth);
+            } else {
+                outRect.set(v.getLeft(), v.getTop(), v.getRight(), v.getBottom());
+            }
         }
     }
 }
diff --git a/src/com/android/launcher3/keyboard/ItemFocusIndicatorHelper.java b/src/com/android/launcher3/keyboard/ItemFocusIndicatorHelper.java
index 2dc8d81..8eb5c7d 100644
--- a/src/com/android/launcher3/keyboard/ItemFocusIndicatorHelper.java
+++ b/src/com/android/launcher3/keyboard/ItemFocusIndicatorHelper.java
@@ -29,6 +29,7 @@
 import android.util.FloatProperty;
 import android.view.View;
 
+import com.android.launcher3.Flags;
 import com.android.launcher3.R;
 
 /**
@@ -97,13 +98,22 @@
         mContainer = container;
 
         mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
-        mMaxAlpha = Color.alpha(color);
         mPaint.setColor(0xFF000000 | color);
+        if (Flags.enableFocusOutline()) {
+            mPaint.setStyle(Paint.Style.STROKE);
+            mPaint.setStrokeWidth(container.getResources().getDimensionPixelSize(
+                    R.dimen.focus_outline_stroke_width));
+            mRadius = container.getResources().getDimensionPixelSize(
+                    R.dimen.focus_outline_radius);
+        } else {
+            mPaint.setStyle(Paint.Style.FILL);
+            mRadius = container.getResources().getDimensionPixelSize(
+                    R.dimen.grid_visualization_rounding_radius);
+        }
+        mMaxAlpha = Color.alpha(color);
 
         setAlpha(0);
         mShift = 0;
-        mRadius = container.getResources().getDimensionPixelSize(
-                R.dimen.grid_visualization_rounding_radius);
     }
 
     protected void setAlpha(float alpha) {
diff --git a/src/com/android/launcher3/logging/StatsLogManager.java b/src/com/android/launcher3/logging/StatsLogManager.java
index d8388c2..45ff33b 100644
--- a/src/com/android/launcher3/logging/StatsLogManager.java
+++ b/src/com/android/launcher3/logging/StatsLogManager.java
@@ -117,6 +117,9 @@
         @UiEvent(doc = "Task launched from overview using SWIPE DOWN")
         LAUNCHER_TASK_LAUNCH_SWIPE_DOWN(340),
 
+        @UiEvent(doc = "App launched by dragging and dropping, probably from taskbar")
+        LAUNCHER_APP_LAUNCH_DRAGDROP(1552),
+
         @UiEvent(doc = "TASK dismissed from overview using SWIPE UP")
         LAUNCHER_TASK_DISMISS_SWIPE_UP(341),
 
@@ -208,6 +211,9 @@
         @UiEvent(doc = "User tapped on pin system shortcut.")
         LAUNCHER_SYSTEM_SHORTCUT_PIN_TAP(522),
 
+        @UiEvent(doc = "User tapped on don't suggest app system shortcut.")
+        LAUNCHER_SYSTEM_SHORTCUT_DONT_SUGGEST_APP_TAP(1603),
+
         @UiEvent(doc = "User is shown All Apps education view.")
         LAUNCHER_ALL_APPS_EDU_SHOWN(523),
 
@@ -281,6 +287,12 @@
         @UiEvent(doc = "User long presses on the bottom bezel area.")
         LAUNCHER_LONG_PRESS_NAVBAR(1544),
 
+        @UiEvent(doc = "User deep presses on the stashed taskbar")
+        LAUNCHER_DEEP_PRESS_STASHED_TASKBAR(1602),
+
+        @UiEvent(doc = "User long presses on the stashed taskbar")
+        LAUNCHER_LONG_PRESS_STASHED_TASKBAR(1592),
+
         @UiEvent(doc = "User swipes or fling in UP direction from bottom bazel area.")
         LAUNCHER_HOME_GESTURE(574),
 
@@ -698,6 +710,12 @@
         @UiEvent(doc = "User tapped private space settings button")
         LAUNCHER_PRIVATE_SPACE_SETTINGS_TAP(1550),
 
+        @UiEvent(doc = "User tapped on install to private space system shortcut.")
+        LAUNCHER_PRIVATE_SPACE_INSTALL_SYSTEM_SHORTCUT_TAP(1565),
+
+        @UiEvent(doc = "User attempted to create split screen with a widget")
+        LAUNCHER_SPLIT_WIDGET_ATTEMPT(1604)
+
         // ADD MORE
         ;
 
diff --git a/src/com/android/launcher3/model/AllAppsList.java b/src/com/android/launcher3/model/AllAppsList.java
index 190eb78..7cbfc37 100644
--- a/src/com/android/launcher3/model/AllAppsList.java
+++ b/src/com/android/launcher3/model/AllAppsList.java
@@ -205,6 +205,8 @@
                             && installInfo.state == PackageInstallInfo.STATUS_INSTALLING) {
                         continue;
                     }
+                    // TODO(b/302115555): Handle the case when archived apps are to be updated
+                    //  during unarchival start.
                     appInfo.setProgressLevel(installInfo);
 
                     updatedAppInfos.add(appInfo);
diff --git a/src/com/android/launcher3/model/GridSizeMigrationUtil.java b/src/com/android/launcher3/model/GridSizeMigrationUtil.java
index efd5574..af66431 100644
--- a/src/com/android/launcher3/model/GridSizeMigrationUtil.java
+++ b/src/com/android/launcher3/model/GridSizeMigrationUtil.java
@@ -262,7 +262,8 @@
             String srcTableName, String destTableName) {
         int id = copyEntryAndUpdate(helper, entry, srcTableName, destTableName);
 
-        if (entry.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER) {
+        if (entry.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER
+                || entry.itemType == LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR) {
             for (Set<Integer> itemIds : entry.mFolderItems.values()) {
                 for (int itemId : itemIds) {
                     copyEntryAndUpdate(helper, itemId, id, srcTableName, destTableName);
diff --git a/src/com/android/launcher3/model/LoaderCursor.java b/src/com/android/launcher3/model/LoaderCursor.java
index 4370043..1044dfb 100644
--- a/src/com/android/launcher3/model/LoaderCursor.java
+++ b/src/com/android/launcher3/model/LoaderCursor.java
@@ -41,6 +41,8 @@
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.Workspace;
+import com.android.launcher3.backuprestore.LauncherRestoreEventLogger;
+import com.android.launcher3.backuprestore.LauncherRestoreEventLogger.RestoreError;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.logging.FileLog;
@@ -70,6 +72,7 @@
     private final Context mContext;
     private final IconCache mIconCache;
     private final InvariantDeviceProfile mIDP;
+    private final @Nullable LauncherRestoreEventLogger mRestoreEventLogger;
 
     private final IntArray mItemsToRemove = new IntArray();
     private final IntArray mRestoredRows = new IntArray();
@@ -107,7 +110,8 @@
     public int itemType;
     public int restoreFlag;
 
-    public LoaderCursor(Cursor cursor, LauncherAppState app, UserManagerState userManagerState) {
+    public LoaderCursor(Cursor cursor, LauncherAppState app, UserManagerState userManagerState,
+            @Nullable LauncherRestoreEventLogger restoreEventLogger) {
         super(cursor);
 
         mApp = app;
@@ -115,6 +119,7 @@
         mContext = app.getContext();
         mIconCache = app.getIconCache();
         mIDP = app.getInvariantDeviceProfile();
+        mRestoreEventLogger = restoreEventLogger;
 
         // Init column indices
         mIconIndex = getColumnIndexOrThrow(Favorites.ICON);
@@ -390,9 +395,12 @@
     /**
      * Marks the current item for removal
      */
-    public void markDeleted(String reason) {
+    public void markDeleted(String reason, @RestoreError String errorType) {
         FileLog.e(TAG, reason);
         mItemsToRemove.add(id);
+        if (mRestoreEventLogger != null) {
+            mRestoreEventLogger.logSingleFavoritesItemRestoreFailed(itemType, errorType);
+        }
     }
 
     /**
@@ -431,6 +439,9 @@
             mApp.getModel().getModelDbController().update(TABLE_NAME, values,
                     Utilities.createDbSelectionQuery(Favorites._ID, mRestoredRows), null);
         }
+        if (mRestoreEventLogger != null) {
+            mRestoreEventLogger.reportLauncherRestoreResults();
+        }
     }
 
     /**
@@ -473,8 +484,11 @@
         }
         if (checkItemPlacement(info, dataModel.isFirstPagePinnedItemEnabled)) {
             dataModel.addItem(mContext, info, false, logger);
+            if (mRestoreEventLogger != null) {
+                mRestoreEventLogger.logSingleFavoritesItemRestored(itemType);
+            }
         } else {
-            markDeleted("Item position overlap");
+            markDeleted("Item position overlap", RestoreError.INVALID_LOCATION);
         }
     }
 
diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java
index 6ea3e8a..7c3d758 100644
--- a/src/com/android/launcher3/model/LoaderTask.java
+++ b/src/com/android/launcher3/model/LoaderTask.java
@@ -17,6 +17,8 @@
 package com.android.launcher3.model;
 
 import static com.android.launcher3.BuildConfig.WIDGET_ON_FIRST_SCREEN;
+import static com.android.launcher3.Flags.enableLauncherBrMetricsFixed;
+import static com.android.launcher3.LauncherPrefs.IS_FIRST_LOAD_AFTER_RESTORE;
 import static com.android.launcher3.LauncherPrefs.SHOULD_SHOW_SMARTSPACE;
 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR;
 import static com.android.launcher3.LauncherSettings.Favorites.TABLE_NAME;
@@ -28,16 +30,10 @@
 import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_QUIET_MODE_ENABLED;
 import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_WORK_PROFILE_QUIET_MODE_ENABLED;
 import static com.android.launcher3.model.ModelUtils.filterCurrentWorkspaceItems;
-import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_DISABLED_LOCKED_USER;
-import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_DISABLED_SAFEMODE;
-import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_DISABLED_SUSPENDED;
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
 import static com.android.launcher3.util.PackageManagerHelper.hasShortcutsPermission;
-import static com.android.launcher3.util.PackageManagerHelper.isSystemApp;
 
-import android.annotation.SuppressLint;
 import android.appwidget.AppWidgetProviderInfo;
-import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
@@ -47,12 +43,10 @@
 import android.content.pm.PackageInstaller.SessionInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.ShortcutInfo;
-import android.graphics.Point;
 import android.os.Bundle;
 import android.os.Trace;
 import android.os.UserHandle;
 import android.os.UserManager;
-import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.Log;
 import android.util.LongSparseArray;
@@ -60,15 +54,15 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
+import androidx.annotation.WorkerThread;
 
-import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Flags;
-import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherModel;
 import com.android.launcher3.LauncherPrefs;
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.backuprestore.LauncherRestoreEventLogger;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.folder.Folder;
 import com.android.launcher3.folder.FolderGridOrganizer;
@@ -85,13 +79,11 @@
 import com.android.launcher3.model.data.FolderInfo;
 import com.android.launcher3.model.data.IconRequestInfo;
 import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.model.data.ItemInfoWithIcon;
 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.pm.InstallSessionHelper;
 import com.android.launcher3.pm.PackageInstallInfo;
 import com.android.launcher3.pm.UserCache;
-import com.android.launcher3.qsb.QsbContainerView;
 import com.android.launcher3.shortcuts.ShortcutKey;
 import com.android.launcher3.shortcuts.ShortcutRequest;
 import com.android.launcher3.shortcuts.ShortcutRequest.QueryResult;
@@ -103,9 +95,7 @@
 import com.android.launcher3.util.PackageManagerHelper;
 import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.util.TraceHelper;
-import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
-import com.android.launcher3.widget.WidgetManagerHelper;
-import com.android.launcher3.widget.custom.CustomWidgetManager;
+import com.android.launcher3.widget.WidgetInflater;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -113,6 +103,7 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.CancellationException;
 
@@ -123,6 +114,7 @@
  *   - all apps icons
  *   - deep shortcuts within apps
  */
+@SuppressWarnings("NewApi")
 public class LoaderTask implements Runnable {
     private static final String TAG = "LoaderTask";
     public static final String SMARTSPACE_ON_HOME_SCREEN = "pref_smartspace_home_screen";
@@ -134,6 +126,7 @@
     private final AllAppsList mBgAllAppsList;
     protected final BgDataModel mBgDataModel;
     private final ModelDelegate mModelDelegate;
+    private boolean mIsRestoreFromBackup;
 
     private FirstScreenBroadcast mFirstScreenBroadcast;
 
@@ -148,7 +141,6 @@
     private final IconCache mIconCache;
 
     private final UserManagerState mUserManagerState;
-
     protected final Map<ComponentKey, AppWidgetProviderInfo> mWidgetProvidersMap = new ArrayMap<>();
     private Map<ShortcutKey, ShortcutInfo> mShortcutKeyToPinnedShortcuts;
 
@@ -172,10 +164,9 @@
         mBgDataModel = bgModel;
         mModelDelegate = modelDelegate;
         mLauncherBinder = launcherBinder;
-
         mLauncherApps = mApp.getContext().getSystemService(LauncherApps.class);
         mUserManager = mApp.getContext().getSystemService(UserManager.class);
-        mUserCache = UserCache.getInstance(mApp.getContext());
+        mUserCache = UserCache.INSTANCE.get(mApp.getContext());
         mSessionHelper = InstallSessionHelper.INSTANCE.get(mApp.getContext());
         mIconCache = mApp.getIconCache();
         mUserManagerState = userManagerState;
@@ -221,15 +212,23 @@
 
         TraceHelper.INSTANCE.beginSection(TAG);
         LoaderMemoryLogger memoryLogger = new LoaderMemoryLogger();
+        mIsRestoreFromBackup =
+                (Boolean) LauncherPrefs.get(mApp.getContext()).get(IS_FIRST_LOAD_AFTER_RESTORE);
+        LauncherRestoreEventLogger restoreEventLogger = null;
+        if (enableLauncherBrMetricsFixed()) {
+            restoreEventLogger = LauncherRestoreEventLogger.Companion
+                    .newInstance(mApp.getContext());
+        }
         try (LauncherModel.LoaderTransaction transaction = mApp.getModel().beginLoader(this)) {
+
             List<ShortcutInfo> allShortcuts = new ArrayList<>();
-            loadWorkspace(allShortcuts, "", memoryLogger);
+            loadWorkspace(allShortcuts, "", memoryLogger, restoreEventLogger);
 
             // Sanitize data re-syncs widgets/shortcuts based on the workspace loaded from db.
             // sanitizeData should not be invoked if the workspace is loaded from a db different
             // from the main db as defined in the invariant device profile.
             // (e.g. both grid preview and minimal device mode uses a different db)
-            if (mApp.getInvariantDeviceProfile().dbFile.equals(mDbName)) {
+            if (Objects.equals(mApp.getInvariantDeviceProfile().dbFile, mDbName)) {
                 verifyNotStopped();
                 sanitizeFolders(mItemsDeleted);
                 sanitizeWidgetsShortcutsAndPackages();
@@ -314,8 +313,8 @@
             mLauncherBinder.bindWidgets();
             logASplit("bindWidgets");
             verifyNotStopped();
-
             LauncherPrefs prefs = LauncherPrefs.get(mApp.getContext());
+
             if (SMARTSPACE_AS_A_WIDGET.get() && prefs.get(SHOULD_SHOW_SMARTSPACE)) {
                 mLauncherBinder.bindSmartspaceWidget();
                 // Turn off pref.
@@ -349,6 +348,13 @@
             mModelDelegate.modelLoadComplete();
             transaction.commit();
             memoryLogger.clearLogs();
+            if (mIsRestoreFromBackup) {
+                mIsRestoreFromBackup = false;
+                LauncherPrefs.get(mApp.getContext()).putSync(IS_FIRST_LOAD_AFTER_RESTORE.to(false));
+                if (restoreEventLogger != null) {
+                    restoreEventLogger.reportLauncherRestoreResults();
+                }
+            }
         } catch (CancellationException e) {
             // Loader stopped, ignore
             logASplit("Cancelled");
@@ -367,10 +373,12 @@
     protected void loadWorkspace(
             List<ShortcutInfo> allDeepShortcuts,
             String selection,
-            LoaderMemoryLogger memoryLogger) {
+            LoaderMemoryLogger memoryLogger,
+            @Nullable LauncherRestoreEventLogger restoreEventLogger
+    ) {
         Trace.beginSection("LoadWorkspace");
         try {
-            loadWorkspaceImpl(allDeepShortcuts, selection, memoryLogger);
+            loadWorkspaceImpl(allDeepShortcuts, selection, memoryLogger, restoreEventLogger);
         } finally {
             Trace.endSection();
         }
@@ -391,15 +399,15 @@
     private void loadWorkspaceImpl(
             List<ShortcutInfo> allDeepShortcuts,
             String selection,
-            @Nullable LoaderMemoryLogger memoryLogger) {
+            @Nullable LoaderMemoryLogger memoryLogger,
+            @Nullable LauncherRestoreEventLogger restoreEventLogger) {
         final Context context = mApp.getContext();
         final PackageManagerHelper pmHelper = new PackageManagerHelper(context);
-        final boolean isSafeMode = pmHelper.isSafeMode();
         final boolean isSdCardReady = Utilities.isBootCompleted();
-        final WidgetManagerHelper widgetHelper = new WidgetManagerHelper(context);
+        final WidgetInflater widgetInflater = new WidgetInflater(context);
 
         ModelDbController dbController = mApp.getModel().getModelDbController();
-        dbController.tryMigrateDB();
+        dbController.tryMigrateDB(restoreEventLogger);
         Log.d(TAG, "loadWorkspace: loading default favorites");
         dbController.loadDefaultFavoritesIfNecessary();
 
@@ -411,51 +419,31 @@
                     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);
 
             mShortcutKeyToPinnedShortcuts = new HashMap<>();
             final LoaderCursor c = new LoaderCursor(
                     dbController.query(TABLE_NAME, null, selection, null, null),
-                    mApp, mUserManagerState);
+                    mApp, mUserManagerState, mIsRestoreFromBackup ? restoreEventLogger : null);
             final Bundle extras = c.getExtras();
             mDbName = extras == null ? null : extras.getString(ModelDbController.EXTRA_DB_NAME);
             try {
                 final LongSparseArray<Boolean> unlockedUsers = new LongSparseArray<>();
-
-                mUserManagerState.init(mUserCache, mUserManager);
-
-                for (UserHandle user : mUserCache.getUserProfiles()) {
-                    long serialNo = mUserCache.getSerialNumberForUser(user);
-                    boolean userUnlocked = mUserManager.isUserUnlocked(user);
-
-                    // We can only query for shortcuts when the user is unlocked.
-                    if (userUnlocked) {
-                        QueryResult pinnedShortcuts = new ShortcutRequest(context, user)
-                                .query(ShortcutRequest.PINNED);
-                        if (pinnedShortcuts.wasSuccess()) {
-                            for (ShortcutInfo shortcut : pinnedShortcuts) {
-                                mShortcutKeyToPinnedShortcuts.put(ShortcutKey.fromInfo(shortcut),
-                                        shortcut);
-                            }
-                        } 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.
-                            userUnlocked = false;
-                        }
-                    }
-                    unlockedUsers.put(serialNo, userUnlocked);
-                }
+                queryPinnedShortcutsForUnlockedUsers(context, unlockedUsers);
 
                 List<IconRequestInfo<WorkspaceItemInfo>> iconRequestInfos = new ArrayList<>();
 
+                WorkspaceItemProcessor itemProcessor = new WorkspaceItemProcessor(c, memoryLogger,
+                        mUserManagerState, mLauncherApps, mPendingPackages,
+                        mShortcutKeyToPinnedShortcuts, mApp, mBgDataModel,
+                        mWidgetProvidersMap, installingPkgs, isSdCardReady,
+                        widgetInflater, pmHelper, iconRequestInfos, unlockedUsers,
+                        allDeepShortcuts);
+
                 while (!mStopped && c.moveToNext()) {
-                    processWorkspaceItem(c, memoryLogger, installingPkgs, isSdCardReady,
-                            tempPackageKey, widgetHelper, pmHelper,
-                            iconRequestInfos, unlockedUsers, isSafeMode, allDeepShortcuts);
+                    itemProcessor.processItem();
                 }
                 tryLoadWorkspaceIconsInBulk(iconRequestInfos);
             } finally {
@@ -480,433 +468,85 @@
             // Remove dead items
             mItemsDeleted = c.commitDeleted();
 
-            // Sort the folder items, update ranks, and make sure all preview items are high res.
-            List<FolderGridOrganizer> verifiers =
-                    mApp.getInvariantDeviceProfile().supportedProfiles.stream().map(
-                            FolderGridOrganizer::new).toList();
-            for (FolderInfo folder : mBgDataModel.folders) {
-                Collections.sort(folder.contents, Folder.ITEM_POS_COMPARATOR);
-                verifiers.forEach(verifier -> verifier.setFolderInfo(folder));
-                int size = folder.contents.size();
-
-                // Update ranks here to ensure there are no gaps caused by removed folder items.
-                // Ranks are the source of truth for folder items, so cellX and cellY can be
-                // ignored for now. Database will be updated once user manually modifies folder.
-                for (int rank = 0; rank < size; ++rank) {
-                    WorkspaceItemInfo info = folder.contents.get(rank);
-                    // rank is used differently in app pairs, so don't reset
-                    if (folder.itemType != ITEM_TYPE_APP_PAIR) {
-                        info.rank = rank;
-                    }
-
-                    if (info.usingLowResIcon() && info.itemType == Favorites.ITEM_TYPE_APPLICATION
-                            && verifiers.stream().anyMatch(
-                                verifier -> verifier.isItemInPreview(info.rank))) {
-                        mIconCache.getTitleAndIcon(info, false);
-                    }
-                }
-            }
+            processFolderItems();
 
             c.commitRestoredItems();
         }
     }
 
-    private void processWorkspaceItem(LoaderCursor c,
-            LoaderMemoryLogger memoryLogger,
-            HashMap<PackageUserKey, SessionInfo> installingPkgs,
-            boolean isSdCardReady,
-            PackageUserKey tempPackageKey,
-            WidgetManagerHelper widgetHelper,
-            PackageManagerHelper pmHelper,
-            List<IconRequestInfo<WorkspaceItemInfo>> iconRequestInfos,
-            LongSparseArray<Boolean> unlockedUsers,
-            boolean isSafeMode,
-            List<ShortcutInfo> allDeepShortcuts) {
+    /**
+     * Initialized the UserManagerState, and determines which users are unlocked. Additionally, if
+     * the user is unlocked, it queries LauncherAppsService for pinned shortcuts and stores the
+     * result in a class variable to be used in other methods while processing workspace items.
+     *
+     * @param context used to query LauncherAppsService
+     * @param unlockedUsers this param is changed, and the updated value is used outside this method
+     */
+    @WorkerThread
+    private void queryPinnedShortcutsForUnlockedUsers(Context context,
+            LongSparseArray<Boolean> unlockedUsers) {
+        mUserManagerState.init(mUserCache, mUserManager);
 
-        try {
-            if (c.user == null) {
-                // User has been deleted, remove the item.
-                c.markDeleted("User has been deleted");
-                return;
+        for (UserHandle user : mUserCache.getUserProfiles()) {
+            long serialNo = mUserCache.getSerialNumberForUser(user);
+            boolean userUnlocked = mUserManager.isUserUnlocked(user);
+
+            // We can only query for shortcuts when the user is unlocked.
+            if (userUnlocked) {
+                QueryResult pinnedShortcuts = new ShortcutRequest(context, user)
+                        .query(ShortcutRequest.PINNED);
+                if (pinnedShortcuts.wasSuccess()) {
+                    for (ShortcutInfo shortcut : pinnedShortcuts) {
+                        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;
+                }
             }
+            unlockedUsers.put(serialNo, userUnlocked);
+        }
 
-            boolean allowMissingTarget = false;
-            switch (c.itemType) {
-                case Favorites.ITEM_TYPE_APPLICATION:
-                case Favorites.ITEM_TYPE_DEEP_SHORTCUT:
-                    Intent intent = c.parseIntent();
-                    if (intent == null) {
-                        c.markDeleted("Invalid or null intent");
-                        return;
-                    }
+    }
 
-                    int disabledState = mUserManagerState.isUserQuiet(c.serialNumber)
-                            ? WorkspaceItemInfo.FLAG_DISABLED_QUIET_USER : 0;
-                    ComponentName cn = intent.getComponent();
-                    String targetPkg = cn == null ? intent.getPackage() : cn.getPackageName();
+    /**
+     * After all items have been processed and added to the BgDataModel, this method can correctly
+     * rank items inside folders and load the correct miniature preview icons to be shown when the
+     * folder is collapsed.
+     */
+    @WorkerThread
+    private void processFolderItems() {
+        // Sort the folder items, update ranks, and make sure all preview items are high res.
+        List<FolderGridOrganizer> verifiers = mApp.getInvariantDeviceProfile().supportedProfiles
+                .stream().map(FolderGridOrganizer::new).toList();
+        for (FolderInfo folder : mBgDataModel.folders) {
+            Collections.sort(folder.contents, Folder.ITEM_POS_COMPARATOR);
+            verifiers.forEach(verifier -> verifier.setFolderInfo(folder));
+            int size = folder.contents.size();
 
-                    if (TextUtils.isEmpty(targetPkg)) {
-                        c.markDeleted("Shortcuts can't have null package");
-                        return;
-                    }
+            // Update ranks here to ensure there are no gaps caused by removed folder items.
+            // Ranks are the source of truth for folder items, so cellX and cellY can be
+            // ignored for now. Database will be updated once user manually modifies folder.
+            for (int rank = 0; rank < size; ++rank) {
+                WorkspaceItemInfo info = folder.contents.get(rank);
+                // rank is used differently in app pairs, so don't reset
+                if (folder.itemType != ITEM_TYPE_APP_PAIR) {
+                    info.rank = rank;
+                }
 
-                    // If there is no target package, it's an implicit intent
-                    // (legacy shortcut) which is always valid
-                    boolean validTarget = TextUtils.isEmpty(targetPkg)
-                            || mLauncherApps.isPackageEnabled(targetPkg, c.user);
-
-                    // If it's a deep shortcut, we'll use pinned shortcuts to restore it
-                    if (cn != null && validTarget && c.itemType
-                            != Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
-                        // If the apk is present and the shortcut points to a specific component.
-
-                        // If the component is already present
-                        if (mLauncherApps.isActivityEnabled(cn, c.user)) {
-                            // no special handling necessary for this item
-                            c.markRestored();
-                        } else {
-                            // Gracefully try to find a fallback activity.
-                            intent = pmHelper.getAppLaunchIntent(targetPkg, c.user);
-                            if (intent != null) {
-                                c.restoreFlag = 0;
-                                c.updater().put(
-                                        Favorites.INTENT,
-                                        intent.toUri(0)).commit();
-                                cn = intent.getComponent();
-                            } else {
-                                c.markDeleted("Unable to find a launch target");
-                                return;
-                            }
-                        }
-                    }
-                    // else if cn == null => can't infer much, leave it
-                    // else if !validPkg => could be restored icon or missing sd-card
-
-                    if (!TextUtils.isEmpty(targetPkg) && !validTarget) {
-                        // Points to a valid app (superset of cn != null) but the apk
-                        // is not available.
-
-                        if (c.restoreFlag != 0) {
-                            // 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();
-                            } else {
-                                c.markDeleted("Unrestored app removed: " + targetPkg);
-                                return;
-                            }
-                        } else if (pmHelper.isAppOnSdcard(targetPkg, c.user)) {
-                            // Package is present but not available.
-                            disabledState |= WorkspaceItemInfo.FLAG_DISABLED_NOT_AVAILABLE;
-                            // Add the icon on the workspace anyway.
-                            allowMissingTarget = true;
-                        } 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);
-                            mPendingPackages.add(new PackageUserKey(targetPkg, c.user));
-                            // Add the icon on the workspace anyway.
-                            allowMissingTarget = true;
-                        } else {
-                            // Do not wait for external media load anymore.
-                            c.markDeleted("Invalid package removed: " + targetPkg);
-                            return;
-                        }
-                    }
-
-                    if ((c.restoreFlag & WorkspaceItemInfo.FLAG_SUPPORTS_WEB_UI) != 0) {
-                        validTarget = false;
-                    }
-
-                    if (validTarget) {
-                        // The shortcut points to a valid target (either no target
-                        // or something which is ready to be used)
-                        c.markRestored();
-                    }
-
-                    boolean useLowResIcon = !c.isOnWorkspaceOrHotseat();
-
-                    WorkspaceItemInfo info;
-                    if (c.restoreFlag != 0) {
-                        // Already verified above that user is same as default user
-                        info = c.getRestoredItemInfo(intent);
-                    } else if (c.itemType == Favorites.ITEM_TYPE_APPLICATION) {
-                        info = c.getAppShortcutInfo(
-                                intent, allowMissingTarget, useLowResIcon, false);
-                    } else if (c.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
-                        ShortcutKey key = ShortcutKey.fromIntent(intent, c.user);
-                        if (unlockedUsers.get(c.serialNumber)) {
-                            ShortcutInfo pinnedShortcut = mShortcutKeyToPinnedShortcuts.get(key);
-                            if (pinnedShortcut == null) {
-                                // The shortcut is no longer valid.
-                                c.markDeleted("Pinned shortcut not found");
-                                return;
-                            }
-                            info = new WorkspaceItemInfo(pinnedShortcut, mApp.getContext());
-                            // If the pinned deep shortcut is no longer published,
-                            // use the last saved icon instead of the default.
-                            mIconCache.getShortcutIcon(info, pinnedShortcut, c::loadIcon);
-
-                            if (pmHelper.isAppSuspended(
-                                    pinnedShortcut.getPackage(), info.user)) {
-                                info.runtimeStatusFlags |= FLAG_DISABLED_SUSPENDED;
-                            }
-                            intent = info.getIntent();
-                            allDeepShortcuts.add(pinnedShortcut);
-                        } else {
-                            // Create a shortcut info in disabled mode for now.
-                            info = c.loadSimpleWorkspaceItem();
-                            info.runtimeStatusFlags |= FLAG_DISABLED_LOCKED_USER;
-                        }
-                    } else { // item type == ITEM_TYPE_SHORTCUT
-                        info = c.loadSimpleWorkspaceItem();
-
-                        // Shortcuts are only available on the primary profile
-                        if (!TextUtils.isEmpty(targetPkg)
-                                && pmHelper.isAppSuspended(targetPkg, c.user)) {
-                            disabledState |= FLAG_DISABLED_SUSPENDED;
-                        }
-                        info.options = c.getOptions();
-
-                        // App shortcuts that used to be automatically added to Launcher
-                        // didn't always have the correct intent flags set, so do that here
-                        if (intent.getAction() != null
-                                && intent.getCategories() != null
-                                && intent.getAction().equals(Intent.ACTION_MAIN)
-                                && intent.getCategories().contains(Intent.CATEGORY_LAUNCHER)) {
-                            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
-                                    | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
-                        }
-                    }
-
-                    if (info != null) {
-                        if (info.itemType != Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
-                            // Skip deep shortcuts; their title and icons have already been
-                            // loaded above.
-                            iconRequestInfos.add(c.createIconRequestInfo(info, useLowResIcon));
-                        }
-
-                        c.applyCommonProperties(info);
-
-                        info.intent = intent;
-                        info.rank = c.getRank();
-                        info.spanX = 1;
-                        info.spanY = 1;
-                        info.runtimeStatusFlags |= disabledState;
-                        if (isSafeMode && !isSystemApp(mApp.getContext(), intent)) {
-                            info.runtimeStatusFlags |= FLAG_DISABLED_SAFEMODE;
-                        }
-                        LauncherActivityInfo activityInfo = c.getLauncherActivityInfo();
-                        if (activityInfo != null) {
-                            info.setProgressLevel(
-                                    PackageManagerHelper.getLoadingProgress(activityInfo),
-                                    PackageInstallInfo.STATUS_INSTALLED_DOWNLOADING);
-                        }
-
-                        if (c.restoreFlag != 0 && !TextUtils.isEmpty(targetPkg)) {
-                            tempPackageKey.update(targetPkg, c.user);
-                            SessionInfo si = installingPkgs.get(tempPackageKey);
-                            if (si == null) {
-                                info.runtimeStatusFlags
-                                        &= ~ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE;
-                            } else if (activityInfo == null) {
-                                int installProgress = (int) (si.getProgress() * 100);
-
-                                info.setProgressLevel(installProgress,
-                                        PackageInstallInfo.STATUS_INSTALLING);
-                            }
-                        }
-
-                        c.checkAndAddItem(info, mBgDataModel, memoryLogger);
-                    } else {
-                        throw new RuntimeException("Unexpected null WorkspaceItemInfo");
-                    }
-                    break;
-
-                case Favorites.ITEM_TYPE_FOLDER:
-                case Favorites.ITEM_TYPE_APP_PAIR:
-                    FolderInfo folderInfo = mBgDataModel.findOrMakeFolder(c.id);
-                    c.applyCommonProperties(folderInfo);
-
-                    folderInfo.itemType = c.itemType;
-                    // Do not trim the folder label, as is was set by the user.
-                    folderInfo.title = c.getString(c.mTitleIndex);
-                    folderInfo.spanX = 1;
-                    folderInfo.spanY = 1;
-                    folderInfo.options = c.getOptions();
-
-                    // no special handling required for restored folders
-                    c.markRestored();
-
-                    c.checkAndAddItem(folderInfo, mBgDataModel, memoryLogger);
-                    break;
-
-                case Favorites.ITEM_TYPE_APPWIDGET:
-                    if (WidgetsModel.GO_DISABLE_WIDGETS) {
-                        c.markDeleted("Only legacy shortcuts can have null package");
-                        return;
-                    }
-                    // Follow through
-                case Favorites.ITEM_TYPE_CUSTOM_APPWIDGET:
-                    // Read all Launcher-specific widget details
-                    boolean customWidget = c.itemType
-                            == Favorites.ITEM_TYPE_CUSTOM_APPWIDGET;
-
-                    int appWidgetId = c.getAppWidgetId();
-                    String savedProvider = c.getAppWidgetProvider();
-                    final ComponentName component;
-
-                    if ((c.getOptions() & LauncherAppWidgetInfo.OPTION_SEARCH_WIDGET) != 0) {
-                        component  = QsbContainerView.getSearchComponentName(mApp.getContext());
-                        if (component == null) {
-                            c.markDeleted("Discarding SearchWidget without packagename ");
-                            return;
-                        }
-                    } else {
-                        component = ComponentName.unflattenFromString(savedProvider);
-                    }
-                    final boolean isIdValid =
-                            !c.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_ID_NOT_VALID);
-                    final boolean wasProviderReady =
-                            !c.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY);
-
-                    ComponentKey providerKey = new ComponentKey(component, c.user);
-                    if (!mWidgetProvidersMap.containsKey(providerKey)) {
-                        if (customWidget) {
-                            mWidgetProvidersMap.put(providerKey, CustomWidgetManager.INSTANCE
-                                    .get(mApp.getContext()).getWidgetProvider(component));
-                        } else {
-                            mWidgetProvidersMap.put(providerKey,
-                                    widgetHelper.findProvider(component, c.user));
-                        }
-                    }
-                    final AppWidgetProviderInfo provider = mWidgetProvidersMap.get(providerKey);
-
-                    final boolean isProviderReady = isValidProvider(provider);
-                    if (!isSafeMode && !customWidget && wasProviderReady && !isProviderReady) {
-                        c.markDeleted("Deleting widget that isn't installed anymore: " + provider);
-                    } else {
-                        LauncherAppWidgetInfo appWidgetInfo;
-                        if (isProviderReady) {
-                            appWidgetInfo =
-                                    new LauncherAppWidgetInfo(appWidgetId, provider.provider);
-
-                            // The provider is available. So the widget is either
-                            // available or not available. We do not need to track
-                            // any future restore updates.
-                            int status = c.restoreFlag
-                                    & ~LauncherAppWidgetInfo.FLAG_RESTORE_STARTED
-                                    & ~LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY;
-                            if (!wasProviderReady) {
-                                // If provider was not previously ready, update status and UI flag.
-
-                                // Id would be valid only if the widget restore broadcast received.
-                                if (isIdValid) {
-                                    status |= LauncherAppWidgetInfo.FLAG_UI_NOT_READY;
-                                }
-                            }
-                            appWidgetInfo.restoreStatus = status;
-                        } else {
-                            Log.v(TAG, "Widget restore pending id=" + c.id
-                                    + " appWidgetId=" + appWidgetId
-                                    + " status =" + c.restoreFlag);
-                            appWidgetInfo = new LauncherAppWidgetInfo(appWidgetId, component);
-                            appWidgetInfo.restoreStatus = c.restoreFlag;
-
-                            tempPackageKey.update(component.getPackageName(), c.user);
-                            SessionInfo si = installingPkgs.get(tempPackageKey);
-                            Integer installProgress = si == null
-                                    ? null
-                                    : (int) (si.getProgress() * 100);
-
-                            if (c.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_RESTORE_STARTED)) {
-                                // Restore has started once.
-                            } else if (installProgress != null) {
-                                // App restore has started. Update the flag
-                                appWidgetInfo.restoreStatus
-                                        |= LauncherAppWidgetInfo.FLAG_RESTORE_STARTED;
-                            } else if (!isSafeMode) {
-                                c.markDeleted("Unrestored widget removed: " + component);
-                                return;
-                            }
-
-                            appWidgetInfo.installProgress =
-                                    installProgress == null ? 0 : installProgress;
-                        }
-                        if (appWidgetInfo.hasRestoreFlag(
-                                LauncherAppWidgetInfo.FLAG_DIRECT_CONFIG)) {
-                            appWidgetInfo.bindOptions = c.parseIntent();
-                        }
-
-                        c.applyCommonProperties(appWidgetInfo);
-                        appWidgetInfo.spanX = c.getSpanX();
-                        appWidgetInfo.spanY = c.getSpanY();
-                        appWidgetInfo.options = c.getOptions();
-                        appWidgetInfo.user = c.user;
-                        appWidgetInfo.sourceContainer = c.getAppWidgetSource();
-
-                        if (appWidgetInfo.spanX <= 0 || appWidgetInfo.spanY <= 0) {
-                            c.markDeleted("Widget has invalid size: "
-                                    + appWidgetInfo.spanX + "x" + appWidgetInfo.spanY);
-                            return;
-                        }
-                        LauncherAppWidgetProviderInfo widgetProviderInfo =
-                                widgetHelper.getLauncherAppWidgetInfo(appWidgetId,
-                                        appWidgetInfo.getTargetComponent());
-                        if (widgetProviderInfo != null
-                                && (appWidgetInfo.spanX < widgetProviderInfo.minSpanX
-                                || appWidgetInfo.spanY < widgetProviderInfo.minSpanY)) {
-                            FileLog.d(TAG, "Widget " + widgetProviderInfo.getComponent()
-                                    + " minSizes not meet: span=" + appWidgetInfo.spanX
-                                    + "x" + appWidgetInfo.spanY + " minSpan="
-                                    + widgetProviderInfo.minSpanX + "x"
-                                    + widgetProviderInfo.minSpanY);
-                            logWidgetInfo(mApp.getInvariantDeviceProfile(),
-                                    widgetProviderInfo);
-                        }
-                        if (!c.isOnWorkspaceOrHotseat()) {
-                            c.markDeleted("Widget found where container != CONTAINER_DESKTOP"
-                                    + "nor CONTAINER_HOTSEAT - ignoring!");
-                            return;
-                        }
-
-                        if (!customWidget) {
-                            String providerName = appWidgetInfo.providerName.flattenToString();
-                            if (!providerName.equals(savedProvider)
-                                    || (appWidgetInfo.restoreStatus != c.restoreFlag)) {
-                                c.updater()
-                                        .put(Favorites.APPWIDGET_PROVIDER,
-                                                providerName)
-                                        .put(Favorites.RESTORED,
-                                                appWidgetInfo.restoreStatus)
-                                        .commit();
-                            }
-                        }
-
-                        if (appWidgetInfo.restoreStatus
-                                != LauncherAppWidgetInfo.RESTORE_COMPLETED) {
-                            appWidgetInfo.pendingItemInfo = WidgetsModel.newPendingItemInfo(
-                                    mApp.getContext(),
-                                    appWidgetInfo.providerName,
-                                    appWidgetInfo.user);
-                            mIconCache.getTitleAndIconForApp(
-                                    appWidgetInfo.pendingItemInfo, false);
-                        }
-
-                        c.checkAndAddItem(appWidgetInfo, mBgDataModel);
-                    }
-                    break;
+                if (info.usingLowResIcon() && info.itemType == Favorites.ITEM_TYPE_APPLICATION
+                        && verifiers.stream().anyMatch(it -> it.isItemInPreview(info.rank))) {
+                    mIconCache.getTitleAndIcon(info, false);
+                }
             }
-        } catch (Exception e) {
-            Log.e(TAG, "Desktop items loading interrupted", e);
         }
     }
 
@@ -1009,6 +649,8 @@
             for (int i = 0; i < apps.size(); i++) {
                 LauncherActivityInfo app = apps.get(i);
                 AppInfo appInfo = new AppInfo(app, user, quietMode);
+                // TODO(b/302115555): Handle the case when archived apps with active sessions are
+                //  loaded.
 
                 iconRequestInfos.add(new IconRequestInfo<>(
                         appInfo, app, /* useLowResIcon= */ false));
@@ -1102,52 +744,6 @@
                 && (provider.provider.getPackageName() != null);
     }
 
-    @SuppressLint("NewApi") // Already added API check.
-    private static void logWidgetInfo(InvariantDeviceProfile idp,
-            LauncherAppWidgetProviderInfo widgetProviderInfo) {
-        Point cellSize = new Point();
-        for (DeviceProfile deviceProfile : idp.supportedProfiles) {
-            deviceProfile.getCellSize(cellSize);
-            FileLog.d(TAG, "DeviceProfile available width: " + deviceProfile.availableWidthPx
-                    + ", available height: " + deviceProfile.availableHeightPx
-                    + ", cellLayoutBorderSpacePx Horizontal: "
-                    + deviceProfile.cellLayoutBorderSpacePx.x
-                    + ", cellLayoutBorderSpacePx Vertical: "
-                    + deviceProfile.cellLayoutBorderSpacePx.y
-                    + ", cellSize: " + cellSize);
-        }
-
-        StringBuilder widgetDimension = new StringBuilder();
-        widgetDimension.append("Widget dimensions:\n")
-                .append("minResizeWidth: ")
-                .append(widgetProviderInfo.minResizeWidth)
-                .append("\n")
-                .append("minResizeHeight: ")
-                .append(widgetProviderInfo.minResizeHeight)
-                .append("\n")
-                .append("defaultWidth: ")
-                .append(widgetProviderInfo.minWidth)
-                .append("\n")
-                .append("defaultHeight: ")
-                .append(widgetProviderInfo.minHeight)
-                .append("\n");
-        if (Utilities.ATLEAST_S) {
-            widgetDimension.append("targetCellWidth: ")
-                    .append(widgetProviderInfo.targetCellWidth)
-                    .append("\n")
-                    .append("targetCellHeight: ")
-                    .append(widgetProviderInfo.targetCellHeight)
-                    .append("\n")
-                    .append("maxResizeWidth: ")
-                    .append(widgetProviderInfo.maxResizeWidth)
-                    .append("\n")
-                    .append("maxResizeHeight: ")
-                    .append(widgetProviderInfo.maxResizeHeight)
-                    .append("\n");
-        }
-        FileLog.d(TAG, widgetDimension.toString());
-    }
-
     private static void logASplit(String label) {
         if (DEBUG) {
             Log.d(TAG, label);
diff --git a/src/com/android/launcher3/model/ModelDbController.java b/src/com/android/launcher3/model/ModelDbController.java
index 139efc3..6c64713 100644
--- a/src/com/android/launcher3/model/ModelDbController.java
+++ b/src/com/android/launcher3/model/ModelDbController.java
@@ -19,6 +19,7 @@
 import static android.util.Base64.NO_WRAP;
 
 import static com.android.launcher3.DefaultLayoutParser.RES_PARTNER_DEFAULT_LAYOUT;
+import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE;
 import static com.android.launcher3.LauncherSettings.Favorites.addTableToDb;
 import static com.android.launcher3.LauncherSettings.Settings.LAYOUT_DIGEST_KEY;
 import static com.android.launcher3.LauncherSettings.Settings.LAYOUT_DIGEST_LABEL;
@@ -48,6 +49,7 @@
 import android.util.Log;
 import android.util.Xml;
 
+import androidx.annotation.Nullable;
 import androidx.annotation.WorkerThread;
 
 import com.android.launcher3.AutoInstallsLayout;
@@ -62,6 +64,9 @@
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.backuprestore.LauncherRestoreEventLogger;
+import com.android.launcher3.backuprestore.LauncherRestoreEventLogger.RestoreError;
+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;
@@ -260,9 +265,13 @@
     /**
      * Migrates the DB if needed. If the migration failed, it clears the DB.
      */
-    public void tryMigrateDB() {
+    public void tryMigrateDB(@Nullable LauncherRestoreEventLogger restoreEventLogger) {
+
         if (!migrateGridIfNeeded()) {
-            Log.d(TAG, "Migration failed: resetting launcher database");
+            if (restoreEventLogger != null) {
+                sendMetricsForFailedMigration(restoreEventLogger, getDb());
+            }
+            FileLog.d(TAG, "Migration failed: resetting launcher database");
             createEmptyDB();
             LauncherPrefs.get(mContext).putSync(
                     getEmptyDbCreatedKey(mOpenHelper.getDatabaseName()).to(true));
@@ -282,15 +291,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 +310,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();
@@ -307,6 +321,30 @@
     }
 
     /**
+     * In case of migration failure, report metrics for the count of each itemType in the DB.
+     * @param restoreEventLogger logger used to report Launcher restore metrics
+     */
+    private void sendMetricsForFailedMigration(LauncherRestoreEventLogger restoreEventLogger,
+            SQLiteDatabase db) {
+        try (Cursor cursor = db.rawQuery(
+                "SELECT itemType, COUNT(*) AS count FROM favorites GROUP BY itemType",
+                null
+        )) {
+            if (cursor.moveToFirst()) {
+                do {
+                    restoreEventLogger.logFavoritesItemsRestoreFailed(
+                            cursor.getInt(cursor.getColumnIndexOrThrow(ITEM_TYPE)),
+                            cursor.getInt(cursor.getColumnIndexOrThrow("count")),
+                            RestoreError.GRID_MIGRATION_FAILURE
+                    );
+                } while (cursor.moveToNext());
+            }
+        } catch (Exception e) {
+            FileLog.e(TAG, "sendMetricsForFailedDb: Error reading from database", e);
+        }
+    }
+
+    /**
      * Returns the underlying model database
      */
     public SQLiteDatabase getDb() {
diff --git a/src/com/android/launcher3/model/ModelLauncherCallbacks.kt b/src/com/android/launcher3/model/ModelLauncherCallbacks.kt
new file mode 100644
index 0000000..b12b2bc
--- /dev/null
+++ b/src/com/android/launcher3/model/ModelLauncherCallbacks.kt
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2024 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.model
+
+import android.content.pm.LauncherApps
+import android.content.pm.ShortcutInfo
+import android.os.UserHandle
+import android.text.TextUtils
+import com.android.launcher3.LauncherModel.ModelUpdateTask
+import com.android.launcher3.logging.FileLog
+import com.android.launcher3.model.PackageUpdatedTask.OP_ADD
+import com.android.launcher3.model.PackageUpdatedTask.OP_REMOVE
+import com.android.launcher3.model.PackageUpdatedTask.OP_SUSPEND
+import com.android.launcher3.model.PackageUpdatedTask.OP_UNAVAILABLE
+import com.android.launcher3.model.PackageUpdatedTask.OP_UNSUSPEND
+import com.android.launcher3.model.PackageUpdatedTask.OP_UPDATE
+import java.util.function.Consumer
+
+/**
+ * Implementation of {@link LauncherApps#Callbacks} which converts various events to corresponding
+ * model tasks
+ */
+class ModelLauncherCallbacks(private var taskExecutor: Consumer<ModelUpdateTask>) :
+    LauncherApps.Callback() {
+
+    override fun onPackageAdded(packageName: String, user: UserHandle) {
+        taskExecutor.accept(PackageUpdatedTask(OP_ADD, user, packageName))
+    }
+
+    override fun onPackageChanged(packageName: String, user: UserHandle) {
+        taskExecutor.accept(PackageUpdatedTask(OP_UPDATE, user, packageName))
+    }
+
+    override fun onPackageLoadingProgressChanged(
+        packageName: String,
+        user: UserHandle,
+        progress: Float
+    ) {
+        taskExecutor.accept(PackageIncrementalDownloadUpdatedTask(packageName, user, progress))
+    }
+
+    override fun onPackageRemoved(packageName: String, user: UserHandle) {
+        FileLog.d(TAG, "package removed received $packageName")
+        taskExecutor.accept(PackageUpdatedTask(OP_REMOVE, user, packageName))
+    }
+
+    override fun onPackagesAvailable(
+        vararg packageNames: String,
+        user: UserHandle,
+        replacing: Boolean
+    ) {
+        taskExecutor.accept(PackageUpdatedTask(OP_UPDATE, user, *packageNames))
+    }
+
+    override fun onPackagesSuspended(vararg packageNames: String, user: UserHandle) {
+        taskExecutor.accept(PackageUpdatedTask(OP_SUSPEND, user, *packageNames))
+    }
+
+    override fun onPackagesUnavailable(
+        packageNames: Array<String>,
+        user: UserHandle,
+        replacing: Boolean
+    ) {
+        if (!replacing) {
+            taskExecutor.accept(PackageUpdatedTask(OP_UNAVAILABLE, user, *packageNames))
+        }
+    }
+
+    override fun onPackagesUnsuspended(vararg packageNames: String, user: UserHandle) {
+        taskExecutor.accept(PackageUpdatedTask(OP_UNSUSPEND, user, *packageNames))
+    }
+
+    override fun onShortcutsChanged(
+        packageName: String,
+        shortcuts: MutableList<ShortcutInfo>,
+        user: UserHandle
+    ) {
+        taskExecutor.accept(ShortcutsChangedTask(packageName, shortcuts, user, true))
+    }
+
+    fun onPackagesRemoved(user: UserHandle, packages: List<String>) {
+        FileLog.d(TAG, "package removed received " + TextUtils.join(",", packages))
+        taskExecutor.accept(PackageUpdatedTask(OP_REMOVE, user, *packages.toTypedArray()))
+    }
+
+    companion object {
+        private const val TAG = "LauncherAppsCallbackImpl"
+    }
+}
diff --git a/src/com/android/launcher3/model/PackageInstallStateChangedTask.java b/src/com/android/launcher3/model/PackageInstallStateChangedTask.java
index 76a87ed..2457a42 100644
--- a/src/com/android/launcher3/model/PackageInstallStateChangedTask.java
+++ b/src/com/android/launcher3/model/PackageInstallStateChangedTask.java
@@ -52,7 +52,8 @@
                 ApplicationInfo ai = app.getContext()
                         .getPackageManager().getApplicationInfo(mInstallInfo.packageName, 0);
                 if (InstantAppResolver.newInstance(app.getContext()).isInstantApp(ai)) {
-                    app.getModel().onPackageAdded(ai.packageName, mInstallInfo.user);
+                    app.getModel().newModelCallbacks()
+                            .onPackageAdded(ai.packageName, mInstallInfo.user);
                 }
             } catch (PackageManager.NameNotFoundException e) {
                 // Ignore
diff --git a/src/com/android/launcher3/model/PackageUpdatedTask.java b/src/com/android/launcher3/model/PackageUpdatedTask.java
index 4f2d398..069e96b 100644
--- a/src/com/android/launcher3/model/PackageUpdatedTask.java
+++ b/src/com/android/launcher3/model/PackageUpdatedTask.java
@@ -15,9 +15,11 @@
  */
 package com.android.launcher3.model;
 
+import static com.android.launcher3.Flags.enableSupportForArchiving;
 import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_PRIVATE_PROFILE_QUIET_MODE_ENABLED;
 import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_QUIET_MODE_ENABLED;
 import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_WORK_PROFILE_QUIET_MODE_ENABLED;
+import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_ARCHIVED;
 import static com.android.launcher3.model.data.WorkspaceItemInfo.FLAG_AUTOINSTALL_ICON;
 import static com.android.launcher3.model.data.WorkspaceItemInfo.FLAG_RESTORED_ICON;
 
@@ -67,6 +69,7 @@
  * Handles updates due to changes in package manager (app installed/updated/removed)
  * or when a user availability changes.
  */
+@SuppressWarnings("NewApi")
 public class PackageUpdatedTask extends BaseModelUpdateTask {
 
     // TODO(b/290090023): Set to false after root causing is done.
@@ -269,6 +272,16 @@
                                             : PackageManagerHelper.getLoadingProgress(
                                                     activities.get(0)),
                                     PackageInstallInfo.STATUS_INSTALLED_DOWNLOADING);
+                            // In case an app is archived, we need to make sure that archived state
+                            // in WorkspaceItemInfo is refreshed.
+                            if (enableSupportForArchiving() && !activities.isEmpty()) {
+                                boolean newArchivalState = activities.get(
+                                        0).getActivityInfo().isArchived;
+                                if (newArchivalState != si.isArchived()) {
+                                    si.runtimeStatusFlags ^= FLAG_ARCHIVED;
+                                    infoUpdated = true;
+                                }
+                            }
                             if (si.itemType == Favorites.ITEM_TYPE_APPLICATION) {
                                 iconCache.getTitleAndIcon(si, si.usingLowResIcon());
                                 infoUpdated = true;
diff --git a/src/com/android/launcher3/model/SdCardAvailableReceiver.java b/src/com/android/launcher3/model/SdCardAvailableReceiver.java
index 3798575..8cfa3aa 100644
--- a/src/com/android/launcher3/model/SdCardAvailableReceiver.java
+++ b/src/com/android/launcher3/model/SdCardAvailableReceiver.java
@@ -67,11 +67,10 @@
                 }
             }
             if (!packagesRemoved.isEmpty()) {
-                mModel.onPackagesRemoved(user,
-                        packagesRemoved.toArray(new String[packagesRemoved.size()]));
+                mModel.newModelCallbacks().onPackagesRemoved(user, packagesRemoved);
             }
             if (!packagesUnavailable.isEmpty()) {
-                mModel.onPackagesUnavailable(
+                mModel.newModelCallbacks().onPackagesUnavailable(
                         packagesUnavailable.toArray(new String[packagesUnavailable.size()]),
                         user, false);
             }
diff --git a/src/com/android/launcher3/model/WorkspaceItemProcessor.kt b/src/com/android/launcher3/model/WorkspaceItemProcessor.kt
new file mode 100644
index 0000000..f98cab6
--- /dev/null
+++ b/src/com/android/launcher3/model/WorkspaceItemProcessor.kt
@@ -0,0 +1,548 @@
+/*
+ * 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.model
+
+import android.annotation.SuppressLint
+import android.appwidget.AppWidgetProviderInfo
+import android.content.ComponentName
+import android.content.Intent
+import android.content.pm.LauncherApps
+import android.content.pm.PackageInstaller
+import android.content.pm.ShortcutInfo
+import android.graphics.Point
+import android.text.TextUtils
+import android.util.Log
+import android.util.LongSparseArray
+import androidx.annotation.VisibleForTesting
+import com.android.launcher3.Flags
+import com.android.launcher3.InvariantDeviceProfile
+import com.android.launcher3.LauncherAppState
+import com.android.launcher3.LauncherSettings.Favorites
+import com.android.launcher3.Utilities
+import com.android.launcher3.backuprestore.LauncherRestoreEventLogger.RestoreError
+import com.android.launcher3.logging.FileLog
+import com.android.launcher3.model.data.IconRequestInfo
+import com.android.launcher3.model.data.ItemInfoWithIcon
+import com.android.launcher3.model.data.LauncherAppWidgetInfo
+import com.android.launcher3.model.data.WorkspaceItemInfo
+import com.android.launcher3.pm.PackageInstallInfo
+import com.android.launcher3.shortcuts.ShortcutKey
+import com.android.launcher3.util.ComponentKey
+import com.android.launcher3.util.PackageManagerHelper
+import com.android.launcher3.util.PackageUserKey
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo
+import com.android.launcher3.widget.WidgetInflater
+import com.android.launcher3.widget.util.WidgetSizes
+
+/**
+ * This items is used by LoaderTask to process items that have been loaded from the Launcher's DB.
+ * This data, stored in the Favorites table, needs to be processed in order to be shown on the Home
+ * Page.
+ *
+ * This class processes each of those items: App Shortcuts, Widgets, Folders, etc., one at a time.
+ */
+class WorkspaceItemProcessor(
+    private val c: LoaderCursor,
+    private val memoryLogger: LoaderMemoryLogger?,
+    private val userManagerState: UserManagerState,
+    private val launcherApps: LauncherApps,
+    private val pendingPackages: MutableSet<PackageUserKey>,
+    private val shortcutKeyToPinnedShortcuts: Map<ShortcutKey, ShortcutInfo>,
+    private val app: LauncherAppState,
+    private val bgDataModel: BgDataModel,
+    private val widgetProvidersMap: MutableMap<ComponentKey, AppWidgetProviderInfo?>,
+    private val installingPkgs: HashMap<PackageUserKey, PackageInstaller.SessionInfo>,
+    private val isSdCardReady: Boolean,
+    private val widgetInflater: WidgetInflater,
+    private val pmHelper: PackageManagerHelper,
+    private val iconRequestInfos: MutableList<IconRequestInfo<WorkspaceItemInfo>>,
+    private val unlockedUsers: LongSparseArray<Boolean>,
+    private val allDeepShortcuts: MutableList<ShortcutInfo>
+) {
+
+    private val isSafeMode = app.isSafeModeEnabled
+    private val tempPackageKey = PackageUserKey(null, null)
+    private val iconCache = app.iconCache
+
+    /**
+     * This is the entry point for processing 1 workspace item. This method is like the midfielder
+     * that delegates the actual processing to either processAppShortcut, processFolder, or
+     * processWidget depending on what type of item is being processed.
+     *
+     * All the parameters are expected to be shared between many repeated calls of this method, one
+     * for each workspace item.
+     */
+    fun processItem() {
+        try {
+            if (c.user == null) {
+                // User has been deleted, remove the item.
+                c.markDeleted("User has been deleted", RestoreError.PROFILE_DELETED)
+                return
+            }
+            when (c.itemType) {
+                Favorites.ITEM_TYPE_APPLICATION,
+                Favorites.ITEM_TYPE_DEEP_SHORTCUT -> processAppOrDeepShortcut()
+                Favorites.ITEM_TYPE_FOLDER,
+                Favorites.ITEM_TYPE_APP_PAIR -> processFolderOrAppPair()
+                Favorites.ITEM_TYPE_APPWIDGET,
+                Favorites.ITEM_TYPE_CUSTOM_APPWIDGET -> processWidget()
+            }
+        } catch (e: Exception) {
+            Log.e(TAG, "Desktop items loading interrupted", e)
+        }
+    }
+
+    /**
+     * This method verifies that an app shortcut should be shown on the home screen, updates the
+     * database accordingly, formats the data in such a way that it is ready to be added to the data
+     * model, and then adds it to the launcher’s data model.
+     *
+     * In this method, verification means that an an app shortcut database entry is required to:
+     * Have a Launch Intent. This is how the app component symbolized by the shortcut is launched.
+     * Have a Package Name. Not be in a funky “Restoring, but never actually restored” state. Not
+     * have null or missing ShortcutInfos or ItemInfos in other data models.
+     *
+     * If any of the above are found to be true, the database entry is deleted, and not shown on the
+     * user’s home screen. When an app is verified, it is marked as restored, meaning that the app
+     * is viable to show on the home screen.
+     *
+     * In order to accommodate different types and versions of App Shortcuts, different properties
+     * and flags are set on the ItemInfo objects that are added to the data model. For example,
+     * icons that are not a part of the workspace or hotseat are marked as using low resolution icon
+     * bitmaps. Currently suspended app icons are marked as such. Installing packages are also
+     * marked as such. Lastly, after applying common properties to the ItemInfo, it is added to the
+     * data model to be bound to the launcher’s data model.
+     */
+    @SuppressLint("NewApi")
+    @VisibleForTesting
+    fun processAppOrDeepShortcut() {
+        var allowMissingTarget = false
+        var intent = c.parseIntent()
+        if (intent == null) {
+            c.markDeleted("Invalid or null intent", RestoreError.MISSING_INFO)
+            return
+        }
+        var disabledState =
+            if (userManagerState.isUserQuiet(c.serialNumber))
+                WorkspaceItemInfo.FLAG_DISABLED_QUIET_USER
+            else 0
+        var cn = intent.component
+        val targetPkg = if (cn == null) intent.getPackage() else cn.packageName
+        if (TextUtils.isEmpty(targetPkg)) {
+            c.markDeleted("Shortcuts can't have null package", RestoreError.MISSING_INFO)
+            return
+        }
+
+        // If there is no target package, it's an implicit intent
+        // (legacy shortcut) which is always valid
+        var validTarget =
+            (TextUtils.isEmpty(targetPkg) || launcherApps.isPackageEnabled(targetPkg, c.user))
+
+        // If it's a deep shortcut, we'll use pinned shortcuts to restore it
+        if (cn != null && validTarget && (c.itemType != Favorites.ITEM_TYPE_DEEP_SHORTCUT)) {
+            // If the apk is present and the shortcut points to a specific component.
+
+            // If the component is already present
+            if (launcherApps.isActivityEnabled(cn, c.user)) {
+                // no special handling necessary for this item
+                c.markRestored()
+            } else {
+                // Gracefully try to find a fallback activity.
+                intent = pmHelper.getAppLaunchIntent(targetPkg, c.user)
+                if (intent != null) {
+                    c.restoreFlag = 0
+                    c.updater().put(Favorites.INTENT, intent.toUri(0)).commit()
+                } else {
+                    c.markDeleted(
+                        "Intent null, unable to find a launch target",
+                        RestoreError.MISSING_INFO
+                    )
+                    return
+                }
+            }
+        }
+        // else if cn == null => can't infer much, leave it
+        // else if !validPkg => could be restored icon or missing sd-card
+        when {
+            !TextUtils.isEmpty(targetPkg) && !validTarget -> {
+                // Points to a valid app (superset of cn != null) but the apk
+                // is not available.
+                when {
+                    c.restoreFlag != 0 -> {
+                        // Package is not yet available but might be
+                        // installed later.
+                        FileLog.d(TAG, "package not yet restored: $targetPkg")
+                        tempPackageKey.update(targetPkg, c.user)
+                        when {
+                            c.hasRestoreFlag(WorkspaceItemInfo.FLAG_RESTORE_STARTED) -> {
+                                // Restore has started once.
+                            }
+                            installingPkgs.containsKey(tempPackageKey) -> {
+                                // App restore has started. Update the flag
+                                c.restoreFlag =
+                                    c.restoreFlag or WorkspaceItemInfo.FLAG_RESTORE_STARTED
+                                FileLog.d(TAG, "restore started for installing app: $targetPkg")
+                                c.updater().put(Favorites.RESTORED, c.restoreFlag).commit()
+                            }
+                            else -> {
+                                c.markDeleted(
+                                    "removing app that is not restored and not installing. package: $targetPkg",
+                                    RestoreError.APP_NOT_INSTALLED
+                                )
+                                return
+                            }
+                        }
+                    }
+                    pmHelper.isAppOnSdcard(targetPkg!!, c.user) -> {
+                        // Package is present but not available.
+                        disabledState =
+                            disabledState or WorkspaceItemInfo.FLAG_DISABLED_NOT_AVAILABLE
+                        // Add the icon on the workspace anyway.
+                        allowMissingTarget = true
+                    }
+                    !isSdCardReady -> {
+                        // SdCard is not ready yet. Package might get available,
+                        // once it is ready.
+                        Log.d(TAG, "Missing package, will check later: $targetPkg")
+                        pendingPackages.add(PackageUserKey(targetPkg, c.user))
+                        // Add the icon on the workspace anyway.
+                        allowMissingTarget = true
+                    }
+                    else -> {
+                        // Do not wait for external media load anymore.
+                        c.markDeleted(
+                            "Invalid package removed: $targetPkg",
+                            RestoreError.APP_NOT_INSTALLED
+                        )
+                        return
+                    }
+                }
+            }
+        }
+        if (c.restoreFlag and WorkspaceItemInfo.FLAG_SUPPORTS_WEB_UI != 0) {
+            validTarget = false
+        }
+        if (validTarget) {
+            // The shortcut points to a valid target (either no target
+            // or something which is ready to be used)
+            c.markRestored()
+        }
+        val useLowResIcon = !c.isOnWorkspaceOrHotseat
+        val info: WorkspaceItemInfo?
+        when {
+            c.restoreFlag != 0 -> {
+                // Already verified above that user is same as default user
+                info = c.getRestoredItemInfo(intent)
+            }
+            c.itemType == Favorites.ITEM_TYPE_APPLICATION ->
+                info = c.getAppShortcutInfo(intent, allowMissingTarget, useLowResIcon, false)
+            c.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT -> {
+                val key = ShortcutKey.fromIntent(intent, c.user)
+                if (unlockedUsers[c.serialNumber]) {
+                    val pinnedShortcut = shortcutKeyToPinnedShortcuts[key]
+                    if (pinnedShortcut == null) {
+                        // The shortcut is no longer valid.
+                        c.markDeleted(
+                            "Pinned shortcut not found from request. package=${key.packageName}, user=${c.user}",
+                            RestoreError.SHORTCUT_NOT_FOUND
+                        )
+                        return
+                    }
+                    info = WorkspaceItemInfo(pinnedShortcut, app.context)
+                    // If the pinned deep shortcut is no longer published,
+                    // use the last saved icon instead of the default.
+                    iconCache.getShortcutIcon(info, pinnedShortcut, c::loadIcon)
+                    if (pmHelper.isAppSuspended(pinnedShortcut.getPackage(), info.user)) {
+                        info.runtimeStatusFlags =
+                            info.runtimeStatusFlags or ItemInfoWithIcon.FLAG_DISABLED_SUSPENDED
+                    }
+                    intent = info.getIntent()
+                    allDeepShortcuts.add(pinnedShortcut)
+                } else {
+                    // Create a shortcut info in disabled mode for now.
+                    info = c.loadSimpleWorkspaceItem()
+                    info.runtimeStatusFlags =
+                        info.runtimeStatusFlags or ItemInfoWithIcon.FLAG_DISABLED_LOCKED_USER
+                }
+            }
+            else -> { // item type == ITEM_TYPE_SHORTCUT
+                info = c.loadSimpleWorkspaceItem()
+
+                // Shortcuts are only available on the primary profile
+                if (!TextUtils.isEmpty(targetPkg) && pmHelper.isAppSuspended(targetPkg!!, c.user)) {
+                    disabledState = disabledState or ItemInfoWithIcon.FLAG_DISABLED_SUSPENDED
+                }
+                info.options = c.options
+
+                // App shortcuts that used to be automatically added to Launcher
+                // didn't always have the correct intent flags set, so do that here
+                if (
+                    intent.action != null &&
+                        intent.categories != null &&
+                        intent.action == Intent.ACTION_MAIN &&
+                        intent.categories.contains(Intent.CATEGORY_LAUNCHER)
+                ) {
+                    intent.addFlags(
+                        Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
+                    )
+                }
+            }
+        }
+        if (info != null) {
+            if (info.itemType != Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
+                // Skip deep shortcuts; their title and icons have already been
+                // loaded above.
+                iconRequestInfos.add(c.createIconRequestInfo(info, useLowResIcon))
+            }
+            c.applyCommonProperties(info)
+            info.intent = intent
+            info.rank = c.rank
+            info.spanX = 1
+            info.spanY = 1
+            info.runtimeStatusFlags = info.runtimeStatusFlags or disabledState
+            if (isSafeMode && !PackageManagerHelper.isSystemApp(app.context, intent)) {
+                info.runtimeStatusFlags =
+                    info.runtimeStatusFlags or ItemInfoWithIcon.FLAG_DISABLED_SAFEMODE
+            }
+            val activityInfo = c.launcherActivityInfo
+            if (activityInfo != null) {
+                info.setProgressLevel(
+                    PackageManagerHelper.getLoadingProgress(activityInfo),
+                    PackageInstallInfo.STATUS_INSTALLED_DOWNLOADING
+                )
+            }
+            if (
+                (c.restoreFlag != 0 ||
+                    Flags.enableSupportForArchiving() &&
+                        activityInfo != null &&
+                        activityInfo.applicationInfo.isArchived) && !TextUtils.isEmpty(targetPkg)
+            ) {
+                tempPackageKey.update(targetPkg, c.user)
+                val si = installingPkgs[tempPackageKey]
+                if (si == null) {
+                    info.runtimeStatusFlags =
+                        info.runtimeStatusFlags and
+                            ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE.inv()
+                } else if (
+                    activityInfo ==
+                        null // For archived apps, include progress info in case there is
+                    // a pending install session post restart of device.
+                    ||
+                        (Flags.enableSupportForArchiving() &&
+                            activityInfo.applicationInfo.isArchived)
+                ) {
+                    val installProgress = (si.getProgress() * 100).toInt()
+                    info.setProgressLevel(installProgress, PackageInstallInfo.STATUS_INSTALLING)
+                }
+            }
+            c.checkAndAddItem(info, bgDataModel, memoryLogger)
+        } else {
+            throw RuntimeException("Unexpected null WorkspaceItemInfo")
+        }
+    }
+
+    /**
+     * Loads the folder information from the database and formats it into a FolderInfo. Some of the
+     * processing for folder content items is done in LoaderTask after all the items in the
+     * workspace have been loaded. The loaded FolderInfos are stored in the BgDataModel.
+     */
+    @VisibleForTesting
+    fun processFolderOrAppPair() {
+        val folderInfo =
+            bgDataModel.findOrMakeFolder(c.id).apply {
+                c.applyCommonProperties(this)
+                itemType = c.itemType
+                // Do not trim the folder label, as is was set by the user.
+                title = c.getString(c.mTitleIndex)
+                spanX = 1
+                spanY = 1
+                options = c.options
+            }
+
+        // no special handling required for restored folders
+        c.markRestored()
+        c.checkAndAddItem(folderInfo, bgDataModel, memoryLogger)
+    }
+
+    /**
+     * This method, similar to processAppShortcut above, verifies that a widget should be shown on
+     * the home screen, updates the database accordingly, formats the data in such a way that it is
+     * ready to be added to the data model, and then adds it to the launcher’s data model.
+     *
+     * It verifies that: Widgets are not disabled due to the Launcher variety being of the `Go`
+     * type. Search Widgets have a package name. The app behind the widget is still installed on the
+     * device. The app behind the widget is not in a funky “Restoring, but never actually restored”
+     * state. The widget has a valid size. The widget is in the workspace or the hotseat. If any of
+     * the above are found to be true, the database entry is deleted, and the widget is not shown on
+     * the user’s home screen. When a widget is verified, it is marked as restored, meaning that the
+     * widget is viable to show on the home screen.
+     *
+     * Common properties are applied to the Widget’s Info object, and other information as well
+     * depending on the type of widget. Custom widgets are treated differently than non-custom
+     * widgets, installing / restoring widgets are treated differently, etc.
+     */
+    @VisibleForTesting
+    fun processWidget() {
+        val component = ComponentName.unflattenFromString(c.appWidgetProvider)!!
+        val appWidgetInfo = LauncherAppWidgetInfo(c.appWidgetId, component)
+        c.applyCommonProperties(appWidgetInfo)
+        appWidgetInfo.spanX = c.spanX
+        appWidgetInfo.spanY = c.spanY
+        appWidgetInfo.options = c.options
+        appWidgetInfo.user = c.user
+        appWidgetInfo.sourceContainer = c.appWidgetSource
+        appWidgetInfo.restoreStatus = c.restoreFlag
+        if (appWidgetInfo.spanX <= 0 || appWidgetInfo.spanY <= 0) {
+            c.markDeleted(
+                "Widget has invalid size: ${appWidgetInfo.spanX}x${appWidgetInfo.spanY}",
+                RestoreError.INVALID_LOCATION
+            )
+            return
+        }
+        if (!c.isOnWorkspaceOrHotseat) {
+            c.markDeleted(
+                "Widget found where container != CONTAINER_DESKTOP nor CONTAINER_HOTSEAT - ignoring!",
+                RestoreError.INVALID_LOCATION
+            )
+            return
+        }
+        if (appWidgetInfo.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_DIRECT_CONFIG)) {
+            appWidgetInfo.bindOptions = c.parseIntent()
+        }
+        val inflationResult = widgetInflater.inflateAppWidget(appWidgetInfo)
+        var shouldUpdate = inflationResult.isUpdate
+        val lapi = inflationResult.widgetInfo
+
+        when (inflationResult.type) {
+            WidgetInflater.TYPE_DELETE -> {
+                c.markDeleted(inflationResult.reason, inflationResult.restoreErrorType)
+                return
+            }
+            WidgetInflater.TYPE_PENDING -> {
+                tempPackageKey.update(component.packageName, c.user)
+                val si = installingPkgs[tempPackageKey]
+
+                if (
+                    !c.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_RESTORE_STARTED) &&
+                        !isSafeMode &&
+                        (si == null) &&
+                        (lapi == null)
+                ) {
+                    // Restore never started
+                    c.markDeleted(
+                        "Unrestored widget removed: $component",
+                        RestoreError.APP_NOT_INSTALLED
+                    )
+                    return
+                } else if (
+                    !c.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_RESTORE_STARTED) && si != null
+                ) {
+                    shouldUpdate = true
+                    appWidgetInfo.restoreStatus =
+                        appWidgetInfo.restoreStatus or LauncherAppWidgetInfo.FLAG_RESTORE_STARTED
+                }
+                appWidgetInfo.installProgress =
+                    if (si == null) 0 else (si.getProgress() * 100).toInt()
+                appWidgetInfo.pendingItemInfo =
+                    WidgetsModel.newPendingItemInfo(
+                        app.context,
+                        appWidgetInfo.providerName,
+                        appWidgetInfo.user
+                    )
+                iconCache.getTitleAndIconForApp(appWidgetInfo.pendingItemInfo, false)
+            }
+            WidgetInflater.TYPE_REAL ->
+                WidgetSizes.updateWidgetSizeRangesAsync(
+                    appWidgetInfo.appWidgetId,
+                    lapi,
+                    app.context,
+                    appWidgetInfo.spanX,
+                    appWidgetInfo.spanY
+                )
+        }
+
+        if (shouldUpdate) {
+            c.updater()
+                .put(Favorites.APPWIDGET_PROVIDER, component.flattenToString())
+                .put(Favorites.APPWIDGET_ID, appWidgetInfo.appWidgetId)
+                .put(Favorites.RESTORED, appWidgetInfo.restoreStatus)
+                .commit()
+        }
+        if (lapi != null) {
+            widgetProvidersMap[ComponentKey(lapi.provider, lapi.user)] = inflationResult.widgetInfo
+            if (appWidgetInfo.spanX < lapi.minSpanX || appWidgetInfo.spanY < lapi.minSpanY) {
+                FileLog.d(
+                    TAG,
+                    "Widget ${lapi.component} minSizes not meet: span=${appWidgetInfo.spanX}x${appWidgetInfo.spanY} minSpan=${lapi.minSpanX}x${lapi.minSpanY}"
+                )
+                logWidgetInfo(app.invariantDeviceProfile, lapi)
+            }
+        }
+        c.checkAndAddItem(appWidgetInfo, bgDataModel)
+    }
+
+    companion object {
+        private const val TAG = "WorkspaceItemProcessor"
+        private fun logWidgetInfo(
+            idp: InvariantDeviceProfile,
+            widgetProviderInfo: LauncherAppWidgetProviderInfo
+        ) {
+            val cellSize = Point()
+            for (deviceProfile in idp.supportedProfiles) {
+                deviceProfile.getCellSize(cellSize)
+                FileLog.d(
+                    TAG,
+                    "DeviceProfile available width: ${deviceProfile.availableWidthPx}," +
+                        " available height: ${deviceProfile.availableHeightPx}," +
+                        " cellLayoutBorderSpacePx Horizontal: ${deviceProfile.cellLayoutBorderSpacePx.x}," +
+                        " cellLayoutBorderSpacePx Vertical: ${deviceProfile.cellLayoutBorderSpacePx.y}," +
+                        " cellSize: $cellSize"
+                )
+            }
+            val widgetDimension = StringBuilder()
+            widgetDimension
+                .append("Widget dimensions:\n")
+                .append("minResizeWidth: ")
+                .append(widgetProviderInfo.minResizeWidth)
+                .append("\n")
+                .append("minResizeHeight: ")
+                .append(widgetProviderInfo.minResizeHeight)
+                .append("\n")
+                .append("defaultWidth: ")
+                .append(widgetProviderInfo.minWidth)
+                .append("\n")
+                .append("defaultHeight: ")
+                .append(widgetProviderInfo.minHeight)
+                .append("\n")
+            if (Utilities.ATLEAST_S) {
+                widgetDimension
+                    .append("targetCellWidth: ")
+                    .append(widgetProviderInfo.targetCellWidth)
+                    .append("\n")
+                    .append("targetCellHeight: ")
+                    .append(widgetProviderInfo.targetCellHeight)
+                    .append("\n")
+                    .append("maxResizeWidth: ")
+                    .append(widgetProviderInfo.maxResizeWidth)
+                    .append("\n")
+                    .append("maxResizeHeight: ")
+                    .append(widgetProviderInfo.maxResizeHeight)
+                    .append("\n")
+            }
+            FileLog.d(TAG, widgetDimension.toString())
+        }
+    }
+}
diff --git a/src/com/android/launcher3/model/data/AppInfo.java b/src/com/android/launcher3/model/data/AppInfo.java
index 6c2f589..ea8a7a1 100644
--- a/src/com/android/launcher3/model/data/AppInfo.java
+++ b/src/com/android/launcher3/model/data/AppInfo.java
@@ -16,6 +16,7 @@
 
 package com.android.launcher3.model.data;
 
+import static com.android.launcher3.Flags.enableSupportForArchiving;
 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_ALL_APPS;
 
 import android.content.ComponentName;
@@ -40,6 +41,7 @@
 /**
  * Represents an app in AllAppsView.
  */
+@SuppressWarnings("NewApi")
 public class AppInfo extends ItemInfoWithIcon implements WorkspaceItemFactory {
 
     public static final AppInfo[] EMPTY_ARRAY = new AppInfo[0];
@@ -53,7 +55,8 @@
      */
     public Intent intent;
 
-    @NonNull
+    // componentName for the Private Space Install App button can be null
+    @Nullable
     public ComponentName componentName;
 
     // Section name used for indexing.
@@ -172,6 +175,9 @@
         if (PackageManagerHelper.isAppSuspended(appInfo)) {
             info.runtimeStatusFlags |= FLAG_DISABLED_SUSPENDED;
         }
+        if (enableSupportForArchiving() && lai.getActivityInfo().isArchived) {
+            info.runtimeStatusFlags |= FLAG_ARCHIVED;
+        }
         info.runtimeStatusFlags |= (appInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0
                 ? FLAG_SYSTEM_NO : FLAG_SYSTEM_YES;
 
diff --git a/src/com/android/launcher3/model/data/ItemInfoWithIcon.java b/src/com/android/launcher3/model/data/ItemInfoWithIcon.java
index dc180d8..58b12b1 100644
--- a/src/com/android/launcher3/model/data/ItemInfoWithIcon.java
+++ b/src/com/android/launcher3/model/data/ItemInfoWithIcon.java
@@ -16,8 +16,11 @@
 
 package com.android.launcher3.model.data;
 
+import static com.android.launcher3.Flags.enableSupportForArchiving;
+
 import android.content.Context;
 import android.content.Intent;
+import android.os.Process;
 
 import androidx.annotation.Nullable;
 
@@ -26,7 +29,7 @@
 import com.android.launcher3.icons.FastBitmapDrawable;
 import com.android.launcher3.logging.FileLog;
 import com.android.launcher3.pm.PackageInstallInfo;
-import com.android.launcher3.util.PackageManagerHelper;
+import com.android.launcher3.uioverrides.ApiWrapper;
 
 /**
  * Represents an ItemInfo which also holds an icon.
@@ -113,6 +116,12 @@
     public static final int FLAG_NOT_PINNABLE = 1 << 13;
 
     /**
+     * Flag indicating whether the package related to the item & user corresponds to that of
+     * archived app.
+     */
+    public static final int FLAG_ARCHIVED = 1 << 14;
+
+    /**
      * Status associated with the system state of the underlying item. This is calculated every
      * time a new info is created and not persisted on the disk.
      */
@@ -142,6 +151,15 @@
     }
 
     /**
+     * Returns true if the app corresponding to the item is archived. */
+    public boolean isArchived() {
+        if (!enableSupportForArchiving()) {
+            return false;
+        }
+        return (runtimeStatusFlags & FLAG_ARCHIVED) != 0;
+    }
+
+    /**
      * Indicates whether we're using a low res icon
      */
     public boolean usingLowResIcon() {
@@ -157,7 +175,7 @@
     public boolean isAppStartable() {
         return ((runtimeStatusFlags & FLAG_INSTALL_SESSION_ACTIVE) == 0)
                 && (((runtimeStatusFlags & FLAG_INCREMENTAL_DOWNLOAD_ACTIVE) != 0)
-                    || mProgressLevel == 100);
+                    || mProgressLevel == 100 || isArchived());
     }
 
     /**
@@ -166,7 +184,10 @@
      * progress.
      */
     public int getProgressLevel() {
-        if ((runtimeStatusFlags & FLAG_SHOW_DOWNLOAD_PROGRESS_MASK) != 0) {
+        if (((runtimeStatusFlags & FLAG_SHOW_DOWNLOAD_PROGRESS_MASK) != 0)
+                // This condition for archived apps is so that in case unarchival/update of
+                // archived app is cancelled, the state transitions back to 0% installed state.
+                || isArchived()) {
             return mProgressLevel;
         }
         return 100;
@@ -216,7 +237,8 @@
         String targetPackage = getTargetPackage();
 
         return targetPackage != null
-                ? new PackageManagerHelper(context).getMarketIntent(targetPackage)
+                ? ApiWrapper.getAppMarketActivityIntent(
+                        context, targetPackage, Process.myUserHandle())
                 : null;
     }
 
diff --git a/src/com/android/launcher3/model/data/LauncherAppWidgetInfo.java b/src/com/android/launcher3/model/data/LauncherAppWidgetInfo.java
index 1fbe04f..6fa8c54 100644
--- a/src/com/android/launcher3/model/data/LauncherAppWidgetInfo.java
+++ b/src/com/android/launcher3/model/data/LauncherAppWidgetInfo.java
@@ -32,13 +32,11 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
-import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.logger.LauncherAtom;
 import com.android.launcher3.util.ContentWriter;
 import com.android.launcher3.widget.LauncherAppWidgetHostView;
 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
-import com.android.launcher3.widget.util.WidgetSizes;
 
 /**
  * Represents a widget (either instantiated or about to be) in the Launcher.
@@ -143,8 +141,6 @@
      */
     private int widgetFeatures;
 
-    private boolean mHasNotifiedInitialWidgetSizeChanged;
-
     /**
      * The container from which this widget was added (e.g. widgets tray, pin widget, search)
      */
@@ -202,17 +198,6 @@
                 .put(LauncherSettings.Favorites.APPWIDGET_SOURCE, sourceContainer);
     }
 
-    /**
-     * When we bind the widget, we should notify the widget that the size has changed if we have not
-     * done so already (only really for default workspace widgets).
-     */
-    public void onBindAppWidget(Launcher launcher, AppWidgetHostView hostView) {
-        if (!mHasNotifiedInitialWidgetSizeChanged) {
-            WidgetSizes.updateWidgetSizeRanges(hostView, launcher, spanX, spanY);
-            mHasNotifiedInitialWidgetSizeChanged = true;
-        }
-    }
-
     @Override
     protected String dumpProperties() {
         return super.dumpProperties()
diff --git a/src/com/android/launcher3/model/data/WorkspaceItemInfo.java b/src/com/android/launcher3/model/data/WorkspaceItemInfo.java
index 3ce194d..c67ec5a 100644
--- a/src/com/android/launcher3/model/data/WorkspaceItemInfo.java
+++ b/src/com/android/launcher3/model/data/WorkspaceItemInfo.java
@@ -148,9 +148,19 @@
 
 
     public final boolean isPromise() {
-        return hasStatusFlag(FLAG_RESTORED_ICON | FLAG_AUTOINSTALL_ICON);
+        return hasStatusFlag(FLAG_RESTORED_ICON | FLAG_AUTOINSTALL_ICON)
+                // For archived apps, promise icons are always ready to be displayed.
+                || isArchived();
     }
 
+    /**
+     * Returns true if the workspace item supports promise icon UI. There are a few cases where they
+     * are supported:
+     * 1. Icons to be restored via backup/restore.
+     * 2. Icons added as an auto-install app.
+     * 3. Icons added due to it being an active install session created by the user.
+     * 4. Icons for archived apps.
+     */
     public boolean hasPromiseIconUi() {
         return isPromise() && !hasStatusFlag(FLAG_SUPPORTS_WEB_UI);
     }
diff --git a/src/com/android/launcher3/pm/InstallSessionHelper.java b/src/com/android/launcher3/pm/InstallSessionHelper.java
index cb3c16c..ca27eb2 100644
--- a/src/com/android/launcher3/pm/InstallSessionHelper.java
+++ b/src/com/android/launcher3/pm/InstallSessionHelper.java
@@ -16,6 +16,8 @@
 
 package com.android.launcher3.pm;
 
+import static com.android.launcher3.Flags.enableSupportForArchiving;
+
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.LauncherApps;
@@ -51,6 +53,7 @@
 /**
  * Utility class to tracking install sessions
  */
+@SuppressWarnings("NewApi")
 public class InstallSessionHelper {
 
     @NonNull
@@ -227,6 +230,11 @@
     }
 
     public boolean verifySessionInfo(@Nullable final PackageInstaller.SessionInfo sessionInfo) {
+        // For archived apps we always want to show promise icons and the checks below don't apply.
+        if (enableSupportForArchiving() && sessionInfo != null && sessionInfo.isUnarchival()) {
+            return true;
+        }
+
         return verify(sessionInfo) != null
                 && sessionInfo.getInstallReason() == PackageManager.INSTALL_REASON_USER
                 && sessionInfo.getAppIcon() != null
diff --git a/src/com/android/launcher3/pm/InstallSessionTracker.java b/src/com/android/launcher3/pm/InstallSessionTracker.java
index 41908d3..e4a2045 100644
--- a/src/com/android/launcher3/pm/InstallSessionTracker.java
+++ b/src/com/android/launcher3/pm/InstallSessionTracker.java
@@ -15,6 +15,7 @@
  */
 package com.android.launcher3.pm;
 
+import static com.android.launcher3.Flags.enableSupportForArchiving;
 import static com.android.launcher3.pm.InstallSessionHelper.getUserHandle;
 import static com.android.launcher3.pm.PackageInstallInfo.STATUS_FAILED;
 import static com.android.launcher3.pm.PackageInstallInfo.STATUS_INSTALLED;
@@ -36,6 +37,7 @@
 import java.lang.ref.WeakReference;
 import java.util.Objects;
 
+@SuppressWarnings("NewApi")
 @WorkerThread
 public class InstallSessionTracker extends PackageInstaller.SessionCallback {
 
@@ -77,6 +79,12 @@
         }
 
         helper.tryQueuePromiseAppIcon(sessionInfo);
+
+        if (enableSupportForArchiving() && sessionInfo != null && sessionInfo.isUnarchival()) {
+            // For archived apps, icon could already be present on the workspace. To make sure
+            // the icon state is updated, we send a change event.
+            callback.onPackageStateChanged(PackageInstallInfo.fromInstallingState(sessionInfo));
+        }
     }
 
     @Override
diff --git a/src/com/android/launcher3/pm/UserCache.java b/src/com/android/launcher3/pm/UserCache.java
index 4661fd4..8708d5a 100644
--- a/src/com/android/launcher3/pm/UserCache.java
+++ b/src/com/android/launcher3/pm/UserCache.java
@@ -28,6 +28,7 @@
 
 import androidx.annotation.AnyThread;
 import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
 import androidx.annotation.WorkerThread;
 
 import com.android.launcher3.util.MainThreadInitializedObject;
@@ -154,6 +155,11 @@
                 .orElse(Process.myUserHandle());
     }
 
+    @VisibleForTesting
+    public void putToCache(UserHandle userHandle, UserIconInfo info) {
+        mUserToSerialMap.put(userHandle, info);
+    }
+
     /**
      * @see UserManager#getUserProfiles()
      */
diff --git a/src/com/android/launcher3/popup/ArrowPopup.java b/src/com/android/launcher3/popup/ArrowPopup.java
index e3314d4..4d4a8f7 100644
--- a/src/com/android/launcher3/popup/ArrowPopup.java
+++ b/src/com/android/launcher3/popup/ArrowPopup.java
@@ -636,10 +636,10 @@
         return getResources().getDimensionPixelSize(R.dimen.popup_vertical_padding);
     }
 
-    protected AnimatorSet getOpenCloseAnimator(boolean isOpening, int scaleDuration,
-            int fadeStartDelay, int fadeDuration, int childFadeStartDelay, int childFadeDuration,
-            Interpolator interpolator) {
-
+    /**
+     * Sets X and Y pivots for the view animation considering arrow position.
+     */
+    protected void setPivotForOpenCloseAnimation() {
         int arrowCenter = mArrowOffsetHorizontal + mArrowWidth / 2;
         if (mIsArrowRotated) {
             setPivotX(mIsLeftAligned ? 0f : getMeasuredWidth());
@@ -648,6 +648,14 @@
             setPivotX(mIsLeftAligned ? arrowCenter : getMeasuredWidth() - arrowCenter);
             setPivotY(mIsAboveIcon ? getMeasuredHeight() : 0f);
         }
+    }
+
+
+    protected AnimatorSet getOpenCloseAnimator(boolean isOpening, int scaleDuration,
+            int fadeStartDelay, int fadeDuration, int childFadeStartDelay, int childFadeDuration,
+            Interpolator interpolator) {
+
+        setPivotForOpenCloseAnimation();
 
         float[] alphaValues = isOpening ? new float[] {0, 1} : new float[] {1, 0};
         float[] scaleValues = isOpening ? new float[] {0.5f, 1.02f} : new float[] {1f, 0.5f};
diff --git a/src/com/android/launcher3/popup/SystemShortcut.java b/src/com/android/launcher3/popup/SystemShortcut.java
index 69bba69..fa7700b 100644
--- a/src/com/android/launcher3/popup/SystemShortcut.java
+++ b/src/com/android/launcher3/popup/SystemShortcut.java
@@ -1,12 +1,17 @@
 package com.android.launcher3.popup;
 
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_PRIVATE_SPACE_INSTALL_SYSTEM_SHORTCUT_TAP;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_APP_INFO_TAP;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_DONT_SUGGEST_APP_TAP;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_WIDGETS_TAP;
 
 import android.app.ActivityOptions;
+import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.graphics.Rect;
+import android.os.Process;
+import android.os.UserHandle;
 import android.view.View;
 import android.view.accessibility.AccessibilityNodeInfo;
 import android.widget.ImageView;
@@ -19,15 +24,19 @@
 import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.allapps.PrivateProfileManager;
 import com.android.launcher3.model.WidgetItem;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.uioverrides.ApiWrapper;
+import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.InstantAppResolver;
 import com.android.launcher3.util.PackageManagerHelper;
 import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.views.ActivityContext;
 import com.android.launcher3.widget.WidgetsBottomSheet;
 
+import java.util.Arrays;
 import java.util.List;
 
 /**
@@ -210,6 +219,77 @@
         }
     }
 
+    public static final Factory<Launcher> PRIVATE_PROFILE_INSTALL =
+            (launcher, itemInfo, originalView) -> {
+                if (itemInfo.getTargetComponent() == null
+                        || !(itemInfo instanceof com.android.launcher3.model.data.AppInfo)
+                        || !itemInfo.getContainerInfo().hasAllAppsContainer()
+                        || !Process.myUserHandle().equals(itemInfo.user)) {
+                    return null;
+                }
+
+                PrivateProfileManager privateProfileManager =
+                        launcher.getAppsView().getPrivateProfileManager();
+                if (privateProfileManager == null || !privateProfileManager.isEnabled()) {
+                    return null;
+                }
+
+                UserHandle privateProfileUser = privateProfileManager.getProfileUser();
+                if (privateProfileUser == null) {
+                    return null;
+                }
+                // Do not show shortcut if an app is already installed to the space
+                ComponentName targetComponent = itemInfo.getTargetComponent();
+                if (launcher.getAppsView()
+                                .getAppsStore()
+                                .getApp(new ComponentKey(targetComponent, privateProfileUser))
+                        != null) {
+                    return null;
+                }
+
+                // Do not show shortcut for settings
+                String[] packagesToSkip =
+                        launcher.getResources()
+                                .getStringArray(R.array.skip_private_profile_shortcut_packages);
+                if (Arrays.asList(packagesToSkip).contains(targetComponent.getPackageName())) {
+                    return null;
+                }
+
+                return new InstallToPrivateProfile(
+                        launcher, itemInfo, originalView, privateProfileUser);
+            };
+
+    static class InstallToPrivateProfile extends SystemShortcut<Launcher> {
+        UserHandle mSpaceUser;
+
+        InstallToPrivateProfile(
+                Launcher target, ItemInfo itemInfo, View originalView, UserHandle spaceUser) {
+            // TODO(b/302666597): update icon once available
+            super(
+                    R.drawable.ic_install_to_private,
+                    R.string.install_private_system_shortcut_label,
+                    target,
+                    itemInfo,
+                    originalView);
+            mSpaceUser = spaceUser;
+        }
+
+        @Override
+        public void onClick(View view) {
+            Intent intent =
+                    ApiWrapper.getAppMarketActivityIntent(
+                            view.getContext(),
+                            mItemInfo.getTargetComponent().getPackageName(),
+                            mSpaceUser);
+            mTarget.startActivitySafely(view, intent, mItemInfo);
+            AbstractFloatingView.closeAllOpenViews(mTarget);
+            mTarget.getStatsLogManager()
+                    .logger()
+                    .withItemInfo(mItemInfo)
+                    .log(LAUNCHER_PRIVATE_SPACE_INSTALL_SYSTEM_SHORTCUT_TAP);
+        }
+    }
+
     public static final Factory<BaseDraggingActivity> INSTALL =
             (activity, itemInfo, originalView) -> {
                 boolean supportsWebUI = (itemInfo instanceof WorkspaceItemInfo)
@@ -237,13 +317,42 @@
 
         @Override
         public void onClick(View view) {
-            Intent intent = new PackageManagerHelper(view.getContext()).getMarketIntent(
-                    mItemInfo.getTargetComponent().getPackageName());
+            Intent intent = ApiWrapper.getAppMarketActivityIntent(view.getContext(),
+                    mItemInfo.getTargetComponent().getPackageName(),
+                    Process.myUserHandle());
             mTarget.startActivitySafely(view, intent, mItemInfo);
             AbstractFloatingView.closeAllOpenViews(mTarget);
         }
     }
 
+    public static final Factory<Launcher> DONT_SUGGEST_APP = new Factory<Launcher>() {
+        @Nullable
+        @Override
+        public SystemShortcut<Launcher> getShortcut(Launcher activity, ItemInfo itemInfo,
+                View originalView) {
+            if (!itemInfo.isPredictedItem()) {
+                return null;
+            }
+            return new DontSuggestApp(activity, itemInfo, originalView);
+        }
+    };
+
+    private static class DontSuggestApp extends SystemShortcut<Launcher> {
+        DontSuggestApp(Launcher target, ItemInfo itemInfo,
+                View originalView) {
+            super(R.drawable.ic_block_no_shadow, R.string.dismiss_prediction_label, target,
+                    itemInfo, originalView);
+        }
+
+        @Override
+        public void onClick(View view) {
+            dismissTaskMenuView(mTarget);
+            mTarget.getStatsLogManager().logger()
+                    .withItemInfo(mItemInfo)
+                    .log(LAUNCHER_SYSTEM_SHORTCUT_DONT_SUGGEST_APP_TAP);
+        }
+    }
+
     public static <T extends Context & ActivityContext> void dismissTaskMenuView(T activity) {
         AbstractFloatingView.closeOpenViews(activity, true,
             AbstractFloatingView.TYPE_ALL & ~AbstractFloatingView.TYPE_REBIND_SAFE);
diff --git a/src/com/android/launcher3/provider/LauncherDbUtils.java b/src/com/android/launcher3/provider/LauncherDbUtils.java
index 30958d9..1f15947 100644
--- a/src/com/android/launcher3/provider/LauncherDbUtils.java
+++ b/src/com/android/launcher3/provider/LauncherDbUtils.java
@@ -109,7 +109,7 @@
         UserManagerState ums = new UserManagerState();
         ums.init(UserCache.INSTANCE.get(context),
                 context.getSystemService(UserManager.class));
-        LoaderCursor lc = new LoaderCursor(c, LauncherAppState.getInstance(context), ums);
+        LoaderCursor lc = new LoaderCursor(c, LauncherAppState.getInstance(context), ums, null);
         IntSet deletedShortcuts = new IntSet();
 
         while (lc.moveToNext()) {
diff --git a/src/com/android/launcher3/provider/RestoreDbTask.java b/src/com/android/launcher3/provider/RestoreDbTask.java
index dbd13b3..1c53855 100644
--- a/src/com/android/launcher3/provider/RestoreDbTask.java
+++ b/src/com/android/launcher3/provider/RestoreDbTask.java
@@ -18,11 +18,15 @@
 
 import static android.os.Process.myUserHandle;
 
+import static com.android.launcher3.Flags.enableLauncherBrMetricsFixed;
 import static com.android.launcher3.InvariantDeviceProfile.TYPE_MULTI_DISPLAY;
 import static com.android.launcher3.LauncherPrefs.APP_WIDGET_IDS;
+import static com.android.launcher3.LauncherPrefs.IS_FIRST_LOAD_AFTER_RESTORE;
 import static com.android.launcher3.LauncherPrefs.OLD_APP_WIDGET_IDS;
 import static com.android.launcher3.LauncherPrefs.RESTORE_DEVICE;
+import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE;
 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
+import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET;
 import static com.android.launcher3.provider.LauncherDbUtils.dropTable;
 import static com.android.launcher3.widget.LauncherWidgetHolder.APPWIDGET_HOST_ID;
 
@@ -52,6 +56,8 @@
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.backuprestore.LauncherRestoreEventLogger;
+import com.android.launcher3.backuprestore.LauncherRestoreEventLogger.RestoreError;
 import com.android.launcher3.logging.FileLog;
 import com.android.launcher3.model.DeviceGridState;
 import com.android.launcher3.model.LoaderTask;
@@ -83,15 +89,17 @@
 
     private static final String TAG = "RestoreDbTask";
     public static final String RESTORED_DEVICE_TYPE = "restored_task_pending";
+    public static final String FIRST_LOAD_AFTER_RESTORE_KEY = "first_load_after_restore";
 
     private static final String INFO_COLUMN_NAME = "name";
     private static final String INFO_COLUMN_DEFAULT_VALUE = "dflt_value";
 
     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"};
+    @VisibleForTesting
+    public static final String[] DB_COLUMNS_TO_LOG = {"profileId", "title", "itemType", "screen",
+            "container", "cellX", "cellY", "spanX", "spanY", "intent", "appWidgetProvider",
+            "appWidgetId", "restored"};
 
     /**
      * Tries to restore the backup DB if needed
@@ -121,8 +129,11 @@
         FileLog.d(TAG, "performRestore: starting restore from db");
         try (SQLiteTransaction t = new SQLiteTransaction(db)) {
             RestoreDbTask task = new RestoreDbTask();
-            task.sanitizeDB(context, controller, db, new BackupManager(context));
-            task.restoreAppWidgetIdsIfExists(context, controller);
+            BackupManager backupManager = new BackupManager(context);
+            LauncherRestoreEventLogger restoreEventLogger =
+                    LauncherRestoreEventLogger.Companion.newInstance(context);
+            task.sanitizeDB(context, controller, db, backupManager, restoreEventLogger);
+            task.restoreAppWidgetIdsIfExists(context, controller, restoreEventLogger);
             t.commit();
             return true;
         } catch (Exception e) {
@@ -141,16 +152,18 @@
      *   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:");
+            BackupManager backupManager, LauncherRestoreEventLogger restoreEventLogger)
+            throws Exception {
+        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 +195,10 @@
         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);
+        if (enableLauncherBrMetricsFixed()) {
+            reportUnrestoredProfiles(db, where, profileIds, restoreEventLogger);
+        }
         int itemsDeletedCount = db.delete(Favorites.TABLE_NAME, where, profileIds);
         FileLog.d(TAG, itemsDeletedCount + " total items from unrestored user(s) were deleted");
 
@@ -242,47 +258,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]
@@ -380,23 +355,27 @@
      * Marks the DB state as pending restoration
      */
     public static void setPending(Context context) {
-        FileLog.d(TAG, "Restore data received through full backup");
-        LauncherPrefs.get(context)
-                .putSync(RESTORE_DEVICE.to(new DeviceGridState(context).getDeviceType()));
+        DeviceGridState deviceGridState = new DeviceGridState(context);
+        FileLog.d(TAG, "restore initiated from backup: DeviceGridState=" + deviceGridState);
+        LauncherPrefs.get(context).putSync(RESTORE_DEVICE.to(deviceGridState.getDeviceType()));
+        if (enableLauncherBrMetricsFixed()) {
+            LauncherPrefs.get(context).putSync(IS_FIRST_LOAD_AFTER_RESTORE.to(true));
+        }
     }
 
     @WorkerThread
     @VisibleForTesting
-    void restoreAppWidgetIdsIfExists(Context context, ModelDbController controller) {
+    void restoreAppWidgetIdsIfExists(Context context, ModelDbController controller,
+            LauncherRestoreEventLogger restoreEventLogger) {
         LauncherPrefs lp = LauncherPrefs.get(context);
         if (lp.has(APP_WIDGET_IDS, OLD_APP_WIDGET_IDS)) {
             AppWidgetHost host = new AppWidgetHost(context, APPWIDGET_HOST_ID);
-            restoreAppWidgetIds(context, controller,
+            restoreAppWidgetIds(context, controller, restoreEventLogger,
                     IntArray.fromConcatString(lp.get(OLD_APP_WIDGET_IDS)).toArray(),
                     IntArray.fromConcatString(lp.get(APP_WIDGET_IDS)).toArray(),
                     host);
         } else {
-            FileLog.d(TAG, "No app widget ids to restore.");
+            FileLog.d(TAG, "Did not receive new app widget id map during Launcher restore");
         }
 
         lp.remove(APP_WIDGET_IDS, OLD_APP_WIDGET_IDS);
@@ -407,18 +386,21 @@
      */
     @WorkerThread
     private void restoreAppWidgetIds(Context context, ModelDbController controller,
-            int[] oldWidgetIds, int[] newWidgetIds, @NonNull AppWidgetHost host) {
+            LauncherRestoreEventLogger launcherRestoreEventLogger, 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();
+            launcherRestoreEventLogger.logFavoritesItemsRestoreFailed(Favorites.ITEM_TYPE_APPWIDGET,
+                    oldWidgetIds.length, RestoreError.WIDGETS_DISABLED);
             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 +408,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 +416,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 +436,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 +445,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,14 +453,19 @@
                         "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]);
+                        launcherRestoreEventLogger.logSingleFavoritesItemRestoreFailed(
+                                ITEM_TYPE_APPWIDGET,
+                                RestoreError.WIDGET_REMOVED
+                        );
                     }
                 }
             }
         }
 
+        logFavoritesTable(controller.getDb(), "launcher db after remap widget ids", null, null);
         LauncherAppState app = LauncherAppState.getInstanceNoCreate();
         if (app != null) {
             app.getModel().forceReload();
@@ -513,17 +500,16 @@
             StringBuilder builder = new StringBuilder();
             builder.append("[");
             for (int i = 0; i < widgetIdList.size(); i++) {
-                builder.append("[")
+                builder.append("[appWidgetId=")
                         .append(widgetIdList.get(i))
-                        .append(", ")
+                        .append(", restoreFlag=")
                         .append(widgetRestoreList.get(i))
-                        .append(", ")
+                        .append(", profileId=")
                         .append(widgetProfileIdList.get(i))
                         .append("]");
             }
             builder.append("]");
-            Log.d(TAG, "restoreAppWidgetIds: all widget ids in database: "
-                    + builder.toString());
+            Log.d(TAG, "restoreAppWidgetIds: all widget ids in database: " + builder);
         } catch (Exception ex) {
             Log.e(TAG, "Getting widget ids from the database failed", ex);
         }
@@ -572,4 +558,72 @@
                 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 cursor = database.query(
+                /* table */ Favorites.TABLE_NAME,
+                /* columns */ DB_COLUMNS_TO_LOG,
+                /* selection */ where,
+                /* selection args */ profileIds,
+                /* groupBy */ null,
+                /* having */ null,
+                /* orderBy */ null
+        )) {
+            if (cursor.moveToFirst()) {
+                String[] columnNames = cursor.getColumnNames();
+                StringBuilder stringBuilder = new StringBuilder(logHeader + "\n");
+                do {
+                    for (String columnName : columnNames) {
+                        stringBuilder.append(columnName)
+                                .append("=")
+                                .append(cursor.getString(
+                                        cursor.getColumnIndex(columnName)))
+                                .append(" ");
+                    }
+                    stringBuilder.append("\n");
+                } while (cursor.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);
+        }
+    }
+
+
+    /**
+     * Queries and reports the count of each itemType to be removed due to unrestored profiles.
+     * @param database The Launcher db to query from.
+     * @param where Query being used for to find unrestored profiles
+     * @param profileIds profile ids that were not restored
+     * @param restoreEventLogger Backup/Restore Logger to report metrics
+     */
+    private void reportUnrestoredProfiles(SQLiteDatabase database, String where,
+            String[] profileIds, LauncherRestoreEventLogger restoreEventLogger) {
+        final String query = "SELECT itemType, COUNT(*) AS count FROM favorites WHERE "
+                + where + " GROUP BY itemType";
+        try (Cursor cursor = database.rawQuery(query, profileIds)) {
+            if (cursor.moveToFirst()) {
+                do {
+                    restoreEventLogger.logFavoritesItemsRestoreFailed(
+                            cursor.getInt(cursor.getColumnIndexOrThrow(ITEM_TYPE)),
+                            cursor.getInt(cursor.getColumnIndexOrThrow("count")),
+                            RestoreError.PROFILE_NOT_RESTORED
+                    );
+                } while (cursor.moveToNext());
+            }
+        } catch (Exception e) {
+            FileLog.e(TAG, "reportUnrestoredProfiles: Error reading from database", e);
+        }
+    }
 }
diff --git a/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt b/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt
index 45174a7..71957e1 100644
--- a/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt
+++ b/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt
@@ -23,10 +23,10 @@
 import com.android.launcher3.BubbleTextView
 import com.android.launcher3.allapps.BaseAllAppsAdapter
 import com.android.launcher3.config.FeatureFlags
+import com.android.launcher3.util.ExecutorRunnable
 import com.android.launcher3.util.Executors.MAIN_EXECUTOR
 import com.android.launcher3.util.Executors.VIEW_PREINFLATION_EXECUTOR
 import com.android.launcher3.views.ActivityContext
-import java.util.concurrent.Future
 
 const val PREINFLATE_ICONS_ROW_COUNT = 4
 const val EXTRA_ICONS_COUNT = 2
@@ -38,9 +38,8 @@
  */
 class AllAppsRecyclerViewPool<T> : RecycledViewPool() {
 
-    private var future: Future<Void>? = null
-
     var hasWorkProfile = false
+    var executorRunnable: ExecutorRunnable<List<ViewHolder>>? = null
 
     /**
      * Preinflate app icons. If all apps RV cannot be scrolled down, we don't need to preinflate.
@@ -63,21 +62,38 @@
                 override fun getLayoutManager(): RecyclerView.LayoutManager? = null
             }
 
-        // Inflate view holders on background thread, and added to view pool on main thread.
-        future?.cancel(true)
-        future =
-            VIEW_PREINFLATION_EXECUTOR.submit<Void> {
-                val viewHolders =
-                    Array(preInflateCount) {
-                        adapter.createViewHolder(activeRv, BaseAllAppsAdapter.VIEW_TYPE_ICON)
+        executorRunnable?.cancel(/* interrupt= */ false)
+        executorRunnable =
+            ExecutorRunnable.createAndExecute(
+                VIEW_PREINFLATION_EXECUTOR,
+                {
+                    val list: ArrayList<ViewHolder> = ArrayList()
+                    for (i in 0 until preInflateCount) {
+                        if (Thread.interrupted()) {
+                            break
+                        }
+                        list.add(
+                            adapter.createViewHolder(activeRv, BaseAllAppsAdapter.VIEW_TYPE_ICON)
+                        )
                     }
-                MAIN_EXECUTOR.execute {
+                    list
+                },
+                MAIN_EXECUTOR,
+                { viewHolders ->
                     for (i in 0 until minOf(viewHolders.size, getPreinflateCount(context))) {
                         putRecycledView(viewHolders[i])
                     }
                 }
-                null
-            }
+            )
+    }
+
+    /**
+     * When clearing [RecycledViewPool], we should also abort pre-inflation tasks. This will make
+     * sure we don't inflate app icons after DeviceProfile has changed.
+     */
+    override fun clear() {
+        super.clear()
+        executorRunnable?.cancel(/* interrupt= */ true)
     }
 
     /**
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/ResponsiveSpec.kt b/src/com/android/launcher3/responsive/ResponsiveSpec.kt
index b0e1b27..65e0b32 100644
--- a/src/com/android/launcher3/responsive/ResponsiveSpec.kt
+++ b/src/com/android/launcher3/responsive/ResponsiveSpec.kt
@@ -59,7 +59,7 @@
     val cellSize: SizeSpec,
 ) : IResponsiveSpec {
     init {
-        check(isValid()) { "Invalid ResponsiveSpec found." }
+        check(isValid()) { "Invalid ResponsiveSpec found. $this" }
     }
 
     constructor(
@@ -106,7 +106,7 @@
         }
 
         if (!isValidRemainderSpace()) {
-            logError("The total Remainder Space used must be lower or equal to 100%.")
+            logError("The total Remainder Space used must be equal to 0 or 1.")
             return false
         }
 
@@ -131,11 +131,12 @@
     }
 
     private fun isValidRemainderSpace(): Boolean {
-        // TODO(b/313621277): This validation must be update do accept only 0 or 1 instead of <= 1f.
-        return startPadding.ofRemainderSpace +
-            endPadding.ofRemainderSpace +
-            gutter.ofRemainderSpace +
-            cellSize.ofRemainderSpace <= 1f
+        val remainderSpaceUsed =
+            startPadding.ofRemainderSpace +
+                endPadding.ofRemainderSpace +
+                gutter.ofRemainderSpace +
+                cellSize.ofRemainderSpace
+        return remainderSpaceUsed == 0f || remainderSpaceUsed == 1f
     }
 
     private fun isValidAvailableSpace(): Boolean {
@@ -254,8 +255,8 @@
 
         startPaddingPx = spec.startPadding.getRemainderSpaceValue(remainderSpace, startPaddingPx)
         endPaddingPx = spec.endPadding.getRemainderSpaceValue(remainderSpace, endPaddingPx)
-        gutterPx = spec.gutter.getRemainderSpaceValue(remainderSpace, gutterPx)
-        cellSizePx = spec.cellSize.getRemainderSpaceValue(remainderSpace, cellSizePx)
+        gutterPx = spec.gutter.getRemainderSpaceValue(remainderSpace, gutterPx, gutters)
+        cellSizePx = spec.cellSize.getRemainderSpaceValue(remainderSpace, cellSizePx, cells)
     }
 
     override fun hashCode(): Int {
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/responsive/SizeSpec.kt b/src/com/android/launcher3/responsive/SizeSpec.kt
index d146898..41dcd5e 100644
--- a/src/com/android/launcher3/responsive/SizeSpec.kt
+++ b/src/com/android/launcher3/responsive/SizeSpec.kt
@@ -57,11 +57,16 @@
     /**
      * Calculates the [SizeSpec] value when remainder space value is defined. If no remainderSpace
      * is 0, returns a default value.
+     *
+     * @param remainderSpace The remainder space to be used for the calculation
+     * @param defaultValue The default value to be returned when no ofRemainderSpace is defined
+     * @param factor A number to divide the remainder space. The default value is 1. This property
+     *   is used to split equally the remainder space by the number of cells and gutters.
      */
-    fun getRemainderSpaceValue(remainderSpace: Int, defaultValue: Int): Int {
+    fun getRemainderSpaceValue(remainderSpace: Int, defaultValue: Int, factor: Int = 1): Int {
         val remainderSpaceValue =
             if (ofRemainderSpace > 0) {
-                (ofRemainderSpace * remainderSpace).roundToInt()
+                (ofRemainderSpace * remainderSpace / factor).roundToInt()
             } else {
                 defaultValue
             }
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/statemanager/StateManager.java b/src/com/android/launcher3/statemanager/StateManager.java
index 360ff7e..e8b3066 100644
--- a/src/com/android/launcher3/statemanager/StateManager.java
+++ b/src/com/android/launcher3/statemanager/StateManager.java
@@ -19,6 +19,7 @@
 import static android.animation.ValueAnimator.areAnimatorsEnabled;
 
 import static com.android.launcher3.anim.AnimatorPlaybackController.callListenerCommandRecursively;
+import static com.android.launcher3.states.StateAnimationConfig.HANDLE_STATE_APPLY;
 import static com.android.launcher3.states.StateAnimationConfig.SKIP_ALL_ANIMATIONS;
 
 import android.animation.Animator;
@@ -36,10 +37,13 @@
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.states.StateAnimationConfig;
 import com.android.launcher3.states.StateAnimationConfig.AnimationFlags;
+import com.android.launcher3.states.StateAnimationConfig.AnimationPropertyFlags;
 import com.android.launcher3.testing.shared.TestProtocol;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.stream.Collectors;
 
 /**
  * Class to manage transitions between different states for a StatefulActivity based on different
@@ -189,7 +193,7 @@
 
     public void reapplyState(boolean cancelCurrentAnimation) {
         boolean wasInAnimation = mConfig.currentAnimation != null;
-        if (cancelCurrentAnimation) {
+        if (cancelCurrentAnimation && (mConfig.animProps & HANDLE_STATE_APPLY) == 0) {
             // Animation canceling can trigger a cleanup routine, causing problems when we are in a
             // launcher state that relies on member variable data. So if we are in one of those
             // states, accelerate the current animation to its end point rather than canceling it
@@ -227,7 +231,15 @@
 
     private void goToState(
             STATE_TYPE state, boolean animated, long delay, AnimatorListener listener) {
-        Log.d(TestProtocol.OVERVIEW_OVER_HOME, "go to state " + state);
+        String stackTrace = Log.getStackTraceString(new Exception("tracing state transition"));
+        String truncatedTrace =
+                Arrays.stream(stackTrace.split("\\n"))
+                    .limit(5)
+                    .skip(1) // Removes the line "java.lang.Exception: tracing state transition"
+                    .filter(traceLine -> !traceLine.contains("StateManager.goToState"))
+                    .collect(Collectors.joining("\n"));
+        Log.d(TestProtocol.OVERVIEW_OVER_HOME,
+                "go to state " + state + " partial trace:\n" + truncatedTrace);
 
         animated &= areAnimatorsEnabled();
         if (mActivity.isInState(state)) {
@@ -237,7 +249,7 @@
                     listener.onAnimationEnd(null);
                 }
                 return;
-            } else if ((!mConfig.userControlled && animated && mConfig.targetState == state)
+            } else if ((!mConfig.isUserControlled() && animated && mConfig.targetState == state)
                     || mState.shouldPreserveDataStateOnReapply()) {
                 // We are running the same animation as requested, and/or target state should not be
                 // reset -- allow the current animation to complete instead of canceling it.
@@ -343,7 +355,7 @@
 
     public AnimatorPlaybackController createAnimationToNewWorkspace(STATE_TYPE state,
             StateAnimationConfig config) {
-        config.userControlled = true;
+        config.animProps |= StateAnimationConfig.USER_CONTROLLED;
         cancelAnimation();
         config.copyTo(mConfig);
         mConfig.playbackController = createAnimationToNewWorkspaceInternal(state)
@@ -418,7 +430,7 @@
     }
 
     public void moveToRestState(boolean isAnimated) {
-        if (mConfig.currentAnimation != null && mConfig.userControlled) {
+        if (mConfig.currentAnimation != null && mConfig.isUserControlled()) {
             // The user is doing something. Lets not mess it up
             return;
         }
@@ -450,10 +462,18 @@
         }
     }
 
+    /**
+     * Sets the provided controller as the current user controlled state animation
+     */
     public void setCurrentUserControlledAnimation(AnimatorPlaybackController controller) {
+        setCurrentAnimation(controller, StateAnimationConfig.USER_CONTROLLED);
+    }
+
+    public void setCurrentAnimation(AnimatorPlaybackController controller,
+            @AnimationPropertyFlags int animationProps) {
         clearCurrentAnimation();
         setCurrentAnimation(controller.getTarget());
-        mConfig.userControlled = true;
+        mConfig.animProps = animationProps;
         mConfig.playbackController = controller;
     }
 
diff --git a/src/com/android/launcher3/statemanager/StatefulActivity.java b/src/com/android/launcher3/statemanager/StatefulActivity.java
index 520f33c..30ba703 100644
--- a/src/com/android/launcher3/statemanager/StatefulActivity.java
+++ b/src/com/android/launcher3/statemanager/StatefulActivity.java
@@ -126,16 +126,8 @@
 
     @Override
     public void reapplyUi() {
-        reapplyUi(true /* cancelCurrentAnimation */);
-    }
-
-    /**
-     * Re-applies if any state transition is not running, optionally cancelling
-     * the transition if requested.
-     */
-    public void reapplyUi(boolean cancelCurrentAnimation) {
         getRootView().dispatchInsets();
-        getStateManager().reapplyState(cancelCurrentAnimation);
+        getStateManager().reapplyState(true /* cancelCurrentAnimation */);
     }
 
     @Override
diff --git a/src/com/android/launcher3/states/StateAnimationConfig.java b/src/com/android/launcher3/states/StateAnimationConfig.java
index 0d9e010..0ca5afd 100644
--- a/src/com/android/launcher3/states/StateAnimationConfig.java
+++ b/src/com/android/launcher3/states/StateAnimationConfig.java
@@ -40,8 +40,19 @@
     public static final int SKIP_DEPTH_CONTROLLER = 1 << 2;
     public static final int SKIP_SCRIM = 1 << 3;
 
+    @IntDef(flag = true, value = {
+            USER_CONTROLLED,
+            HANDLE_STATE_APPLY
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface AnimationPropertyFlags {}
+    // Indicates that the animation is controlled by the user
+    public static final int USER_CONTROLLED = 1 << 0;
+    // Indicates that he animation can survive state UI resets due to inset or config changes
+    public static final int HANDLE_STATE_APPLY = 1 << 1;
+
     public long duration;
-    public boolean userControlled;
+    public @AnimationPropertyFlags int animProps = 0;
     public @AnimationFlags int animFlags = 0;
 
 
@@ -105,12 +116,16 @@
     public void copyTo(StateAnimationConfig target) {
         target.duration = duration;
         target.animFlags = animFlags;
-        target.userControlled = userControlled;
+        target.animProps = animProps;
         for (int i = 0; i < ANIM_TYPES_COUNT; i++) {
             target.mInterpolators[i] = mInterpolators[i];
         }
     }
 
+    public boolean isUserControlled() {
+        return (animProps & USER_CONTROLLED) != 0;
+    }
+
     /**
      * Returns the interpolator set for animId or fallback if nothing is set
      *
diff --git a/src/com/android/launcher3/testing/TestInformationHandler.java b/src/com/android/launcher3/testing/TestInformationHandler.java
index 2c834bd..ccff095 100644
--- a/src/com/android/launcher3/testing/TestInformationHandler.java
+++ b/src/com/android/launcher3/testing/TestInformationHandler.java
@@ -153,11 +153,13 @@
                 }, this::getCurrentActivity);
             }
 
-            case TestProtocol.REQUEST_IME_INSETS: {
+            case TestProtocol.REQUEST_SYSTEM_GESTURE_REGION: {
                 return getUIProperty(Bundle::putParcelable, activity -> {
                     WindowInsetsCompat insets = WindowInsetsCompat.toWindowInsetsCompat(
                             activity.getWindow().getDecorView().getRootWindowInsets());
-                    return insets.getInsets(WindowInsetsCompat.Type.ime()).toPlatformInsets();
+                    return insets.getInsets(WindowInsetsCompat.Type.ime()
+                            | WindowInsetsCompat.Type.systemGestures())
+                            .toPlatformInsets();
                 }, this::getCurrentActivity);
             }
 
@@ -330,6 +332,7 @@
                 return null;
             }
             T value = provider.apply(target);
+
             Bundle response = new Bundle();
             bundleSetter.set(response, TestProtocol.TEST_INFO_RESPONSE_FIELD, value);
             return response;
diff --git a/src/com/android/launcher3/touch/AllAppsSwipeController.java b/src/com/android/launcher3/touch/AllAppsSwipeController.java
index 8b9bc19..fe4a83b 100644
--- a/src/com/android/launcher3/touch/AllAppsSwipeController.java
+++ b/src/com/android/launcher3/touch/AllAppsSwipeController.java
@@ -192,7 +192,7 @@
     protected StateAnimationConfig getConfigForStates(LauncherState fromState,
             LauncherState toState) {
         StateAnimationConfig config = super.getConfigForStates(fromState, toState);
-        config.userControlled = true;
+        config.animProps |= StateAnimationConfig.USER_CONTROLLED;
         if (fromState == NORMAL && toState == ALL_APPS) {
             applyNormalToAllAppsAnimConfig(mLauncher, config);
         } else if (fromState == ALL_APPS && toState == NORMAL) {
@@ -209,13 +209,13 @@
             config.setInterpolator(ANIM_SCRIM_FADE,
                     Interpolators.reverse(ALL_APPS_SCRIM_RESPONDER));
             config.setInterpolator(ANIM_ALL_APPS_FADE, FINAL_FRAME);
-            if (!config.userControlled) {
+            if (!config.isUserControlled()) {
                 config.setInterpolator(ANIM_VERTICAL_PROGRESS, EMPHASIZED);
             }
             config.setInterpolator(ANIM_WORKSPACE_SCALE, DECELERATED_EASE);
             config.setInterpolator(ANIM_DEPTH, DECELERATED_EASE);
         } else {
-            if (config.userControlled) {
+            if (config.isUserControlled()) {
                 config.setInterpolator(ANIM_DEPTH, Interpolators.reverse(BLUR_MANUAL));
                 config.setInterpolator(ANIM_WORKSPACE_FADE,
                         Interpolators.reverse(WORKSPACE_FADE_MANUAL));
@@ -250,29 +250,32 @@
         if (launcher.getDeviceProfile().isTablet) {
             config.setInterpolator(ANIM_ALL_APPS_FADE, INSTANT);
             config.setInterpolator(ANIM_SCRIM_FADE, ALL_APPS_SCRIM_RESPONDER);
-            if (!config.userControlled) {
+            if (!config.isUserControlled()) {
                 config.setInterpolator(ANIM_VERTICAL_PROGRESS, EMPHASIZED);
             }
             config.setInterpolator(ANIM_WORKSPACE_SCALE, DECELERATED_EASE);
             config.setInterpolator(ANIM_DEPTH, DECELERATED_EASE);
         } else {
-            config.setInterpolator(ANIM_DEPTH, config.userControlled ? BLUR_MANUAL : BLUR_ATOMIC);
+            config.setInterpolator(ANIM_DEPTH,
+                    config.isUserControlled() ? BLUR_MANUAL : BLUR_ATOMIC);
             config.setInterpolator(ANIM_WORKSPACE_FADE,
-                    config.userControlled ? WORKSPACE_FADE_MANUAL : WORKSPACE_FADE_ATOMIC);
+                    config.isUserControlled() ? WORKSPACE_FADE_MANUAL : WORKSPACE_FADE_ATOMIC);
             config.setInterpolator(ANIM_WORKSPACE_SCALE,
-                    config.userControlled ? WORKSPACE_SCALE_MANUAL : WORKSPACE_SCALE_ATOMIC);
+                    config.isUserControlled() ? WORKSPACE_SCALE_MANUAL : WORKSPACE_SCALE_ATOMIC);
             config.setInterpolator(ANIM_HOTSEAT_FADE,
-                    config.userControlled ? HOTSEAT_FADE_MANUAL : HOTSEAT_FADE_ATOMIC);
+                    config.isUserControlled() ? HOTSEAT_FADE_MANUAL : HOTSEAT_FADE_ATOMIC);
             config.setInterpolator(ANIM_HOTSEAT_SCALE,
-                    config.userControlled ? HOTSEAT_SCALE_MANUAL : HOTSEAT_SCALE_ATOMIC);
+                    config.isUserControlled() ? HOTSEAT_SCALE_MANUAL : HOTSEAT_SCALE_ATOMIC);
             config.setInterpolator(ANIM_HOTSEAT_TRANSLATE,
-                    config.userControlled ? HOTSEAT_TRANSLATE_MANUAL : HOTSEAT_TRANSLATE_ATOMIC);
+                    config.isUserControlled()
+                            ? HOTSEAT_TRANSLATE_MANUAL
+                            : HOTSEAT_TRANSLATE_ATOMIC);
             config.setInterpolator(ANIM_SCRIM_FADE,
-                    config.userControlled ? SCRIM_FADE_MANUAL : SCRIM_FADE_ATOMIC);
+                    config.isUserControlled() ? SCRIM_FADE_MANUAL : SCRIM_FADE_ATOMIC);
             config.setInterpolator(ANIM_ALL_APPS_FADE,
-                    config.userControlled ? ALL_APPS_FADE_MANUAL : ALL_APPS_FADE_ATOMIC);
+                    config.isUserControlled() ? ALL_APPS_FADE_MANUAL : ALL_APPS_FADE_ATOMIC);
             config.setInterpolator(ANIM_VERTICAL_PROGRESS,
-                    config.userControlled
+                    config.isUserControlled()
                             ? ALL_APPS_VERTICAL_PROGRESS_MANUAL
                             : ALL_APPS_VERTICAL_PROGRESS_ATOMIC);
         }
@@ -285,7 +288,7 @@
      */
     public static void applyOverviewToAllAppsAnimConfig(
             DeviceProfile deviceProfile, StateAnimationConfig config, float threshold) {
-        config.userControlled = true;
+        config.animProps |= StateAnimationConfig.USER_CONTROLLED;
         config.animFlags = SKIP_OVERVIEW;
         if (deviceProfile.isTablet) {
             config.setInterpolator(ANIM_ALL_APPS_FADE, INSTANT);
diff --git a/src/com/android/launcher3/touch/ItemClickHandler.java b/src/com/android/launcher3/touch/ItemClickHandler.java
index 3bce377..ff8b381 100644
--- a/src/com/android/launcher3/touch/ItemClickHandler.java
+++ b/src/com/android/launcher3/touch/ItemClickHandler.java
@@ -15,6 +15,7 @@
  */
 package com.android.launcher3.touch;
 
+import static com.android.launcher3.Flags.enableSupportForArchiving;
 import static com.android.launcher3.LauncherConstants.ActivityCodes.REQUEST_BIND_PENDING_APPWIDGET;
 import static com.android.launcher3.LauncherConstants.ActivityCodes.REQUEST_RECONFIGURE_APPWIDGET;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_FOLDER_OPEN;
@@ -31,6 +32,7 @@
 import android.content.Intent;
 import android.content.pm.LauncherApps;
 import android.content.pm.PackageInstaller.SessionInfo;
+import android.os.Process;
 import android.text.TextUtils;
 import android.util.Log;
 import android.view.View;
@@ -58,8 +60,8 @@
 import com.android.launcher3.shortcuts.ShortcutKey;
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.shared.TestProtocol;
+import com.android.launcher3.uioverrides.ApiWrapper;
 import com.android.launcher3.util.ItemInfoMatcher;
-import com.android.launcher3.util.PackageManagerHelper;
 import com.android.launcher3.views.FloatingIconView;
 import com.android.launcher3.views.Snackbar;
 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
@@ -145,8 +147,8 @@
      */
     private static void onClickAppPairIcon(View v) {
         Launcher launcher = Launcher.getLauncher(v.getContext());
-        FolderInfo folderInfo = ((AppPairIcon) v).getInfo();
-        launcher.launchAppPair(folderInfo.contents.get(0), folderInfo.contents.get(1));
+        AppPairIcon appPairIcon = (AppPairIcon) v;
+        launcher.launchAppPair(appPairIcon);
     }
 
     /**
@@ -207,7 +209,8 @@
                 }
             }
             // Fallback to using custom market intent.
-            Intent intent = new PackageManagerHelper(launcher).getMarketIntent(packageName);
+            Intent intent = ApiWrapper.getAppMarketActivityIntent(launcher,
+                    packageName, Process.myUserHandle());
             launcher.startActivitySafely(v, intent, item);
         };
 
@@ -317,7 +320,8 @@
         }
 
         // Check for abandoned promise
-        if ((v instanceof BubbleTextView) && shortcut.hasPromiseIconUi()) {
+        if ((v instanceof BubbleTextView) && shortcut.hasPromiseIconUi()
+                && (!enableSupportForArchiving() || !shortcut.isArchived())) {
             String packageName = shortcut.getIntent().getComponent() != null
                     ? shortcut.getIntent().getComponent().getPackageName()
                     : shortcut.getIntent().getPackage();
@@ -344,8 +348,8 @@
                 && (((ItemInfoWithIcon) item).runtimeStatusFlags
                 & ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE) != 0) {
             ItemInfoWithIcon appInfo = (ItemInfoWithIcon) item;
-            intent = new PackageManagerHelper(launcher)
-                    .getMarketIntent(appInfo.getTargetComponent().getPackageName());
+            intent = ApiWrapper.getAppMarketActivityIntent(launcher,
+                    appInfo.getTargetComponent().getPackageName(), Process.myUserHandle());
         } else {
             intent = item.getIntent();
         }
diff --git a/src/com/android/launcher3/touch/LandscapePagedViewHandler.java b/src/com/android/launcher3/touch/LandscapePagedViewHandler.java
index 51c047c..f7afcb9 100644
--- a/src/com/android/launcher3/touch/LandscapePagedViewHandler.java
+++ b/src/com/android/launcher3/touch/LandscapePagedViewHandler.java
@@ -543,15 +543,19 @@
 
         secondarySnapshotWidth = parentWidth;
         secondarySnapshotHeight = totalThumbnailHeight - primarySnapshotHeight - dividerBar;
-        secondarySnapshot.setTranslationY(0);
-        primarySnapshot.setTranslationY(secondarySnapshotHeight + spaceAboveSnapshot + dividerBar);
+
+        int translationY = primarySnapshotHeight + spaceAboveSnapshot + dividerBar;
+        primarySnapshot.setTranslationY(spaceAboveSnapshot);
+        secondarySnapshot.setTranslationY(translationY - spaceAboveSnapshot);
+
         primarySnapshot.measure(
                 View.MeasureSpec.makeMeasureSpec(primarySnapshotWidth, View.MeasureSpec.EXACTLY),
-                View.MeasureSpec.makeMeasureSpec(primarySnapshotHeight, View.MeasureSpec.EXACTLY));
+                View.MeasureSpec.makeMeasureSpec(primarySnapshotHeight, View.MeasureSpec.EXACTLY)
+        );
         secondarySnapshot.measure(
                 View.MeasureSpec.makeMeasureSpec(secondarySnapshotWidth, View.MeasureSpec.EXACTLY),
-                View.MeasureSpec.makeMeasureSpec(secondarySnapshotHeight,
-                        View.MeasureSpec.EXACTLY));
+                View.MeasureSpec.makeMeasureSpec(secondarySnapshotHeight, View.MeasureSpec.EXACTLY)
+        );
     }
 
     @Override
diff --git a/src/com/android/launcher3/touch/PortraitPagedViewHandler.java b/src/com/android/launcher3/touch/PortraitPagedViewHandler.java
index 04b6710..8301981 100644
--- a/src/com/android/launcher3/touch/PortraitPagedViewHandler.java
+++ b/src/com/android/launcher3/touch/PortraitPagedViewHandler.java
@@ -644,11 +644,12 @@
 
             secondarySnapshotHeight = totalThumbnailHeight;
             secondarySnapshotWidth = parentWidth - primarySnapshotWidth - scaledDividerBar;
-            int translationX = primarySnapshotWidth + scaledDividerBar;
             if (isRtl) {
+                int translationX = secondarySnapshotWidth + scaledDividerBar;
                 primarySnapshot.setTranslationX(-translationX);
                 secondarySnapshot.setTranslationX(0);
             } else {
+                int translationX = primarySnapshotWidth + scaledDividerBar;
                 secondarySnapshot.setTranslationX(translationX);
                 primarySnapshot.setTranslationX(0);
             }
@@ -744,7 +745,8 @@
             secondaryIconParams.setMarginStart(primaryIconParams.getMarginStart());
             if (deviceProfile.isLeftRightSplit) {
                 if (isRtl) {
-                    primaryIconView.setTranslationX(-primarySnapshotWidth);
+                    int secondarySnapshotWidth = groupedTaskViewWidth - primarySnapshotWidth;
+                    primaryIconView.setTranslationX(-secondarySnapshotWidth);
                 } else {
                     secondaryIconView.setTranslationX(primarySnapshotWidth);
                 }
diff --git a/src/com/android/launcher3/touch/SeascapePagedViewHandler.java b/src/com/android/launcher3/touch/SeascapePagedViewHandler.java
index 06526a8..7077ad9 100644
--- a/src/com/android/launcher3/touch/SeascapePagedViewHandler.java
+++ b/src/com/android/launcher3/touch/SeascapePagedViewHandler.java
@@ -285,7 +285,7 @@
         primaryIconView.setTranslationX(0);
         secondaryIconView.setTranslationX(0);
         if (enableOverviewIconMenu()) {
-            if (primaryIconView.getLayoutDirection() == LAYOUT_DIRECTION_RTL) {
+            if (isRtl) {
                 primaryIconView.setTranslationY(groupedTaskViewHeight - primarySnapshotHeight);
                 secondaryIconView.setTranslationY(0);
             } else {
@@ -295,14 +295,25 @@
         } else if (splitConfig.initiatedFromSeascape) {
             // if the split was initiated from seascape,
             // the task on the right (secondary) is slightly larger
-            primaryIconView.setTranslationY(-bottomToMidpointOffset - insetOffset
-                    + taskIconHeight);
-            secondaryIconView.setTranslationY(-bottomToMidpointOffset - insetOffset);
+            if (isRtl) {
+                primaryIconView.setTranslationY(bottomToMidpointOffset - insetOffset
+                        + taskIconHeight);
+                secondaryIconView.setTranslationY(bottomToMidpointOffset - insetOffset);
+            } else {
+                primaryIconView.setTranslationY(-bottomToMidpointOffset - insetOffset
+                        + taskIconHeight);
+                secondaryIconView.setTranslationY(-bottomToMidpointOffset - insetOffset);
+            }
         } else {
             // if not,
             // the task on the left (primary) is slightly larger
-            primaryIconView.setTranslationY(-bottomToMidpointOffset + taskIconHeight);
-            secondaryIconView.setTranslationY(-bottomToMidpointOffset);
+            if (isRtl) {
+                primaryIconView.setTranslationY(bottomToMidpointOffset + taskIconHeight);
+                secondaryIconView.setTranslationY(bottomToMidpointOffset);
+            } else {
+                primaryIconView.setTranslationY(-bottomToMidpointOffset + taskIconHeight);
+                secondaryIconView.setTranslationY(-bottomToMidpointOffset);
+            }
         }
 
         primaryIconView.setLayoutParams(primaryIconParams);
@@ -343,13 +354,15 @@
         secondarySnapshotHeight = totalThumbnailHeight - primarySnapshotHeight - dividerBar;
         secondarySnapshot.setTranslationY(0);
         primarySnapshot.setTranslationY(secondarySnapshotHeight + spaceAboveSnapshot + dividerBar);
+
         primarySnapshot.measure(
                 View.MeasureSpec.makeMeasureSpec(primarySnapshotWidth, View.MeasureSpec.EXACTLY),
-                View.MeasureSpec.makeMeasureSpec(primarySnapshotHeight, View.MeasureSpec.EXACTLY));
+                View.MeasureSpec.makeMeasureSpec(primarySnapshotHeight, View.MeasureSpec.EXACTLY)
+        );
         secondarySnapshot.measure(
                 View.MeasureSpec.makeMeasureSpec(secondarySnapshotWidth, View.MeasureSpec.EXACTLY),
-                View.MeasureSpec.makeMeasureSpec(secondarySnapshotHeight,
-                        View.MeasureSpec.EXACTLY));
+                View.MeasureSpec.makeMeasureSpec(secondarySnapshotHeight, View.MeasureSpec.EXACTLY)
+        );
     }
 
     /* ---------- The following are only used by TaskViewTouchHandler. ---------- */
diff --git a/src/com/android/launcher3/util/CannedAnimationCoordinator.kt b/src/com/android/launcher3/util/CannedAnimationCoordinator.kt
index 18f8339..85f81f5 100644
--- a/src/com/android/launcher3/util/CannedAnimationCoordinator.kt
+++ b/src/com/android/launcher3/util/CannedAnimationCoordinator.kt
@@ -26,6 +26,8 @@
 import com.android.launcher3.anim.AnimatorPlaybackController
 import com.android.launcher3.anim.PendingAnimation
 import com.android.launcher3.statemanager.StatefulActivity
+import com.android.launcher3.states.StateAnimationConfig.HANDLE_STATE_APPLY
+import com.android.launcher3.states.StateAnimationConfig.USER_CONTROLLED
 import java.util.function.Consumer
 
 private const val TAG = "CannedAnimCoordinator"
@@ -107,8 +109,15 @@
         }
         // Link this to the state manager so that it auto-cancels when state changes
         recreatePending = false
+        // Animator coordinator takes care of reapplying the animation due to state reset. Set the
+        // flags accordingly
         animationController =
-            controller.apply { activity.stateManager.setCurrentUserControlledAnimation(this) }
+            controller.apply {
+                activity.stateManager.setCurrentAnimation(
+                    this,
+                    USER_CONTROLLED or HANDLE_STATE_APPLY
+                )
+            }
         recreateAnimation(provider)
     }
 
diff --git a/src/com/android/launcher3/util/CellContentDimensions.kt b/src/com/android/launcher3/util/CellContentDimensions.kt
index 3c8e0c4..5059c2f 100644
--- a/src/com/android/launcher3/util/CellContentDimensions.kt
+++ b/src/com/android/launcher3/util/CellContentDimensions.kt
@@ -48,7 +48,10 @@
             cellContentHeight = getCellContentHeight()
 
             // Step 3. Decrease label size
-            if (cellContentHeight > cellHeightPx) {
+            if (
+                cellContentHeight > cellHeightPx &&
+                    iconTextSizePx > iconSizeSteps.minimumIconLabelSize
+            ) {
                 iconTextSizePx =
                     max(
                         iconSizeSteps.minimumIconLabelSize,
@@ -58,6 +61,17 @@
             }
         }
 
+        // For some cases, depending on the display size, the content might not fit inside the
+        // cell height after considering the minimum icon and label size allowed.
+        // For these extreme cases, we will allow the icon size to be smaller than
+        // [IconSizeSteps.minimumIconSize] to fit inside the cell height without cropping.
+        while (
+            cellContentHeight > cellHeightPx && iconSizePx > IconSizeSteps.ICON_SIZE_STEP_EXTRA
+        ) {
+            iconSizePx -= IconSizeSteps.ICON_SIZE_STEP_EXTRA
+            cellContentHeight = getCellContentHeight()
+        }
+
         return cellContentHeight
     }
 
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/DisplayController.java b/src/com/android/launcher3/util/DisplayController.java
index 18f583d..1419dc4 100644
--- a/src/com/android/launcher3/util/DisplayController.java
+++ b/src/com/android/launcher3/util/DisplayController.java
@@ -175,6 +175,13 @@
         sTransientTaskbarStatusForTests = enable;
     }
 
+    /**
+     * Returns whether the taskbar is pinned in gesture navigation mode.
+     */
+    public static boolean isPinnedTaskbar(Context context) {
+        return INSTANCE.get(context).getInfo().isPinnedTaskbar();
+    }
+
     @Override
     public void close() {
         mDestroyed = true;
@@ -423,6 +430,12 @@
             }
             return true;
         }
+        /**
+         * Returns whether the taskbar is pinned in gesture navigation mode.
+         */
+        public boolean isPinnedTaskbar() {
+            return navigationMode == NavigationMode.NO_BUTTON && !isTransientTaskbar();
+        }
 
         /**
          * Returns {@code true} if the bounds represent a tablet.
diff --git a/src/com/android/launcher3/util/ExecutorRunnable.kt b/src/com/android/launcher3/util/ExecutorRunnable.kt
new file mode 100644
index 0000000..49cf592
--- /dev/null
+++ b/src/com/android/launcher3/util/ExecutorRunnable.kt
@@ -0,0 +1,78 @@
+/*
+ * 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.util
+
+import java.util.concurrent.Executor
+import java.util.concurrent.ExecutorService
+import java.util.concurrent.Future
+import java.util.function.Consumer
+import java.util.function.Supplier
+
+/** A [Runnable] that can be posted to a [Executor] that can be cancelled. */
+class ExecutorRunnable<T>
+private constructor(
+    private val task: Supplier<T>,
+    // Executor where consumer needs to be executed on. Typically UI executor.
+    private val callbackExecutor: Executor,
+    // Consumer that needs to be accepted upon completion of the task. Typically work that needs to
+    // be done in UI thread after task completes.
+    private val callback: Consumer<T>
+) : Runnable {
+
+    // future of this runnable that will used for cancellation.
+    lateinit var future: Future<*>
+
+    // flag to cancel the callback
+    var canceled = false
+
+    override fun run() {
+        val value: T = task.get()
+        callbackExecutor.execute {
+            if (!canceled) {
+                callback.accept(value)
+            }
+        }
+    }
+
+    /**
+     * Cancel the [ExecutorRunnable] if not scheduled. If [ExecutorRunnable] has started execution
+     * at this time, we will try to cancel the callback if not executed yet.
+     */
+    fun cancel(interrupt: Boolean) {
+        future.cancel(interrupt)
+        canceled = true
+    }
+
+    companion object {
+        /**
+         * Create [ExecutorRunnable] and execute it on task [Executor]. It will also save the
+         * [Future] into this [ExecutorRunnable] to be used for cancellation.
+         */
+        fun <T> createAndExecute(
+            // Executor where task will be executed, typically an Executor running on background
+            // thread.
+            taskExecutor: ExecutorService,
+            task: Supplier<T>,
+            callbackExecutor: Executor,
+            callback: Consumer<T>
+        ): ExecutorRunnable<T> {
+            val executorRunnable = ExecutorRunnable(task, callbackExecutor, callback)
+            executorRunnable.future = taskExecutor.submit(executorRunnable)
+            return executorRunnable
+        }
+    }
+}
diff --git a/src/com/android/launcher3/util/IconSizeSteps.kt b/src/com/android/launcher3/util/IconSizeSteps.kt
index 6128eb4..a207d5c 100644
--- a/src/com/android/launcher3/util/IconSizeSteps.kt
+++ b/src/com/android/launcher3/util/IconSizeSteps.kt
@@ -49,5 +49,9 @@
 
     companion object {
         internal const val TEXT_STEP = 1
+
+        // This icon extra step is used for stepping down logic in extreme cases when it's
+        // necessary to reduce the icon size below minimum size available in [icon_size_steps].
+        internal const val ICON_SIZE_STEP_EXTRA = 2
     }
 }
diff --git a/src/com/android/launcher3/util/KeyboardShortcutsDelegate.java b/src/com/android/launcher3/util/KeyboardShortcutsDelegate.java
index c9db83d..e4e0bae 100644
--- a/src/com/android/launcher3/util/KeyboardShortcutsDelegate.java
+++ b/src/com/android/launcher3/util/KeyboardShortcutsDelegate.java
@@ -17,6 +17,7 @@
 
 import static com.android.launcher3.LauncherState.ALL_APPS;
 import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.LauncherState.OVERVIEW;
 import static com.android.launcher3.accessibility.LauncherAccessibilityDelegate.getSupportedActions;
 
 import android.util.Log;
@@ -27,6 +28,7 @@
 
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherState;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.accessibility.BaseAccessibilityDelegate;
@@ -118,17 +120,22 @@
                 return true;
             } else if (mLauncher.getAppsView().isInAllApps()) {
                 // Close all apps if there are no open floating views.
-                closeAllApps();
+                mLauncher.getStateManager().goToState(NORMAL, true);
+                return true;
+            } else if (mLauncher.isInState(LauncherState.OVERVIEW)
+                    || mLauncher.isInState(LauncherState.OVERVIEW_SPLIT_SELECT)) {
+                // Close Overview and return to home.
+                mLauncher.getStateManager().goToState(NORMAL, true);
+                return true;
+            } else if (mLauncher.isInState(LauncherState.OVERVIEW_MODAL_TASK)) {
+                // Return to the previous state (Overview) when the modal task is open.
+                mLauncher.getStateManager().goToState(OVERVIEW, true);
                 return true;
             }
         }
         return null;
     }
 
-    private void closeAllApps() {
-        mLauncher.getStateManager().goToState(NORMAL, true);
-    }
-
     /**
      * Handle key up event.
      * @param keyCode code of the key being pressed.
diff --git a/src/com/android/launcher3/util/PackageManagerHelper.java b/src/com/android/launcher3/util/PackageManagerHelper.java
index 91203a7..2b5aaf5 100644
--- a/src/com/android/launcher3/util/PackageManagerHelper.java
+++ b/src/com/android/launcher3/util/PackageManagerHelper.java
@@ -28,8 +28,8 @@
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.pm.ResolveInfo;
 import android.graphics.Rect;
-import android.net.Uri;
 import android.os.Bundle;
+import android.os.Process;
 import android.os.UserHandle;
 import android.text.TextUtils;
 import android.util.Log;
@@ -46,6 +46,7 @@
 import com.android.launcher3.model.data.ItemInfoWithIcon;
 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.uioverrides.ApiWrapper;
 
 import java.net.URISyntaxException;
 import java.util.List;
@@ -118,10 +119,6 @@
         }
     }
 
-    public boolean isSafeMode() {
-        return mPm.isSafeMode();
-    }
-
     @Nullable
     public Intent getAppLaunchIntent(@Nullable final String pkg, @NonNull final UserHandle user) {
         List<LauncherActivityInfo> activities = mLauncherApps.getActivityList(pkg, user);
@@ -137,17 +134,6 @@
         return (info.flags & ApplicationInfo.FLAG_SUSPENDED) != 0;
     }
 
-    public Intent getMarketIntent(String packageName) {
-        return new Intent(Intent.ACTION_VIEW)
-                .setData(new Uri.Builder()
-                        .scheme("market")
-                        .authority("details")
-                        .appendQueryParameter("id", packageName)
-                        .build())
-                .putExtra(Intent.EXTRA_REFERRER, new Uri.Builder().scheme("android-app")
-                        .authority(mContext.getPackageName()).build());
-    }
-
     /**
      * Creates a new market search intent.
      */
@@ -172,8 +158,8 @@
                 && (((ItemInfoWithIcon) info).runtimeStatusFlags
                     & ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE) != 0) {
             ItemInfoWithIcon appInfo = (ItemInfoWithIcon) info;
-            mContext.startActivity(new PackageManagerHelper(mContext)
-                    .getMarketIntent(appInfo.getTargetComponent().getPackageName()));
+            mContext.startActivity(ApiWrapper.getAppMarketActivityIntent(mContext,
+                    appInfo.getTargetComponent().getPackageName(), Process.myUserHandle()));
             return;
         }
         ComponentName componentName = null;
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/views/ClipIconView.java b/src/com/android/launcher3/views/ClipIconView.java
index 87e496e..7737adb 100644
--- a/src/com/android/launcher3/views/ClipIconView.java
+++ b/src/com/android/launcher3/views/ClipIconView.java
@@ -112,7 +112,7 @@
         float scaleY = rect.height() / minSize;
         float scale = Math.max(1f, Math.min(scaleX, scaleY));
 
-        if (Float.isNaN(scale)) {
+        if (Float.isNaN(scale) || Float.isInfinite(scale)) {
             // Views are no longer laid out, do not update.
             return;
         }
diff --git a/src/com/android/launcher3/widget/DeferredAppWidgetHostView.java b/src/com/android/launcher3/widget/DeferredAppWidgetHostView.java
deleted file mode 100644
index f42142e..0000000
--- a/src/com/android/launcher3/widget/DeferredAppWidgetHostView.java
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.widget;
-
-import android.appwidget.AppWidgetProviderInfo;
-import android.content.Context;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.text.Layout;
-import android.text.StaticLayout;
-import android.text.TextPaint;
-import android.text.TextUtils;
-import android.util.TypedValue;
-import android.view.View;
-import android.widget.RemoteViews;
-
-import com.android.launcher3.R;
-
-/**
- * A widget host views created while the host has not bind to the system service.
- */
-public class DeferredAppWidgetHostView extends LauncherAppWidgetHostView {
-
-    private final TextPaint mPaint;
-    private Layout mSetupTextLayout;
-
-    public DeferredAppWidgetHostView(Context context) {
-        super(context);
-        setWillNotDraw(false);
-
-        mPaint = new TextPaint();
-        mPaint.setColor(Color.WHITE);
-        mPaint.setTextSize(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX,
-                mLauncher.getDeviceProfile().iconTextSizePx,
-                getResources().getDisplayMetrics()));
-        setBackgroundResource(R.drawable.bg_deferred_app_widget);
-    }
-
-    @Override
-    public void updateAppWidget(RemoteViews remoteViews) {
-        // Not allowed
-    }
-
-    @Override
-    public void addView(View child) {
-        // Not allowed
-    }
-
-    @Override
-    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
-        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
-
-        AppWidgetProviderInfo info = getAppWidgetInfo();
-        if (info == null || TextUtils.isEmpty(info.label)) {
-            return;
-        }
-
-        // Use double padding so that there is extra space between background and text if possible.
-        int availableWidth = getMeasuredWidth() - 2 * (getPaddingLeft() + getPaddingRight());
-        if (availableWidth <= 0) {
-            availableWidth = getMeasuredWidth() - (getPaddingLeft() + getPaddingRight());
-        }
-        if (mSetupTextLayout != null && mSetupTextLayout.getText().equals(info.label)
-                && mSetupTextLayout.getWidth() == availableWidth) {
-            return;
-        }
-        mSetupTextLayout = new StaticLayout(info.label, mPaint, availableWidth,
-                Layout.Alignment.ALIGN_CENTER, 1, 0, true);
-    }
-
-    @Override
-    protected void onDraw(Canvas canvas) {
-        if (mSetupTextLayout != null) {
-            canvas.translate((getWidth() - mSetupTextLayout.getWidth()) / 2,
-                    (getHeight() - mSetupTextLayout.getHeight()) / 2);
-            mSetupTextLayout.draw(canvas);
-        }
-    }
-}
diff --git a/src/com/android/launcher3/widget/LauncherAppWidgetHost.java b/src/com/android/launcher3/widget/LauncherAppWidgetHost.java
index 9c21ea2..b1c477c 100644
--- a/src/com/android/launcher3/widget/LauncherAppWidgetHost.java
+++ b/src/com/android/launcher3/widget/LauncherAppWidgetHost.java
@@ -21,13 +21,22 @@
 import android.appwidget.AppWidgetHost;
 import android.appwidget.AppWidgetProviderInfo;
 import android.content.Context;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.widget.RemoteViews;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.util.Executors;
+import com.android.launcher3.util.SafeCloseable;
+import com.android.launcher3.widget.LauncherWidgetHolder.ProviderChangedListener;
 
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.WeakHashMap;
 import java.util.function.IntConsumer;
 
 /**
@@ -37,8 +46,7 @@
  */
 class LauncherAppWidgetHost extends AppWidgetHost {
     @NonNull
-    private final ArrayList<LauncherWidgetHolder.ProviderChangedListener>
-            mProviderChangeListeners = new ArrayList<>();
+    private final List<ProviderChangedListener> mProviderChangeListeners;
 
     @NonNull
     private final Context mContext;
@@ -46,33 +54,13 @@
     @Nullable
     private final IntConsumer mAppWidgetRemovedCallback;
 
-    @NonNull
-    private final LauncherWidgetHolder mHolder;
-
     public LauncherAppWidgetHost(@NonNull Context context,
-            @Nullable IntConsumer appWidgetRemovedCallback, @NonNull LauncherWidgetHolder holder) {
+            @Nullable IntConsumer appWidgetRemovedCallback,
+            List<ProviderChangedListener> providerChangeListeners) {
         super(context, APPWIDGET_HOST_ID);
         mContext = context;
         mAppWidgetRemovedCallback = appWidgetRemovedCallback;
-        mHolder = holder;
-    }
-
-    /**
-     * Add a listener that is triggered when the providers of the widgets are changed
-     * @param listener The listener that notifies when the providers changed
-     */
-    public void addProviderChangeListener(
-            @NonNull LauncherWidgetHolder.ProviderChangedListener listener) {
-        mProviderChangeListeners.add(listener);
-    }
-
-    /**
-     * Remove the specified listener from the host
-     * @param listener The listener that is to be removed from the host
-     */
-    public void removeProviderChangeListener(
-            LauncherWidgetHolder.ProviderChangedListener listener) {
-        mProviderChangeListeners.remove(listener);
+        mProviderChangeListeners = providerChangeListeners;
     }
 
     @Override
@@ -89,7 +77,7 @@
     @NonNull
     public LauncherAppWidgetHostView onCreateView(Context context, int appWidgetId,
             AppWidgetProviderInfo appWidget) {
-        return mHolder.onCreateView(context, appWidgetId, appWidget);
+        return new ListenableHostView(context);
     }
 
     /**
@@ -115,7 +103,10 @@
         if (mAppWidgetRemovedCallback == null) {
             return;
         }
-        mAppWidgetRemovedCallback.accept(appWidgetId);
+        // Route the call via model thread, in case it comes while a loader-bind is in progress
+        Executors.MODEL_EXECUTOR.execute(
+                () -> Executors.MAIN_EXECUTOR.execute(
+                        () -> mAppWidgetRemovedCallback.accept(appWidgetId)));
     }
 
     /**
@@ -126,4 +117,36 @@
         super.clearViews();
     }
 
+    public static class ListenableHostView extends LauncherAppWidgetHostView {
+
+        private Set<Runnable> mUpdateListeners = Collections.EMPTY_SET;
+
+        ListenableHostView(Context context) {
+            super(context);
+        }
+
+        @Override
+        public void updateAppWidget(RemoteViews remoteViews) {
+            super.updateAppWidget(remoteViews);
+            mUpdateListeners.forEach(Runnable::run);
+        }
+
+        @Override
+        public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+            super.onInitializeAccessibilityNodeInfo(info);
+            info.setClassName(LauncherAppWidgetHostView.class.getName());
+        }
+
+        /**
+         * Adds a callback to be run everytime the provided app widget updates.
+         * @return a closable to remove this callback
+         */
+        public SafeCloseable addUpdateListener(Runnable callback) {
+            if (mUpdateListeners == Collections.EMPTY_SET) {
+                mUpdateListeners = Collections.newSetFromMap(new WeakHashMap<>());
+            }
+            mUpdateListeners.add(callback);
+            return () -> mUpdateListeners.remove(callback);
+        }
+    }
 }
diff --git a/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java b/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
index 1aa49c7..e0de269 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;
@@ -43,10 +41,10 @@
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.CheckLongPressHelper;
+import com.android.launcher3.Flags;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
-import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.dragndrop.DragLayer;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
@@ -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;
 
@@ -100,14 +95,15 @@
 
     private boolean mTrackingWidgetUpdate = false;
 
-    private boolean mIsWidgetCachingDisabled = false;
-
     public LauncherAppWidgetHostView(Context context) {
         super(context);
         mLauncher = Launcher.getLauncher(context);
         mLongPressHelper = new CheckLongPressHelper(this, this);
         setAccessibilityDelegate(mLauncher.getAccessibilityDelegate());
         setBackgroundResource(R.drawable.widget_internal_focus_bg);
+        if (Flags.enableFocusOutline()) {
+            setDefaultFocusHighlightEnabled(false);
+        }
 
         if (Utilities.ATLEAST_Q && Themes.getAttrBoolean(mLauncher, R.attr.isWorkspaceDarkText)) {
             setOnLightBackground(true);
@@ -145,10 +141,6 @@
         }
     }
 
-    public void setIsWidgetCachingDisabled(boolean isWidgetCachingDisabled) {
-        mIsWidgetCachingDisabled = isWidgetCachingDisabled;
-    }
-
     @Override
     @TargetApi(Build.VERSION_CODES.Q)
     public void updateAppWidget(RemoteViews remoteViews) {
@@ -158,35 +150,16 @@
                     TRACE_METHOD_NAME + getAppWidgetInfo().provider, getAppWidgetId());
             mTrackingWidgetUpdate = false;
         }
-        if (FeatureFlags.ENABLE_CACHED_WIDGET.get()
-                && !mIsWidgetCachingDisabled) {
+        if (isDeferringUpdates()) {
             mLastRemoteViews = remoteViews;
-            if (isDeferringUpdates()) {
-                return;
-            }
-        } else {
-            if (isDeferringUpdates()) {
-                mLastRemoteViews = remoteViews;
-                return;
-            }
-            mLastRemoteViews = null;
+            return;
         }
+        mLastRemoteViews = null;
 
         super.updateAppWidget(remoteViews);
 
         // 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) {
@@ -451,33 +424,6 @@
     }
 
     @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;
-        }
-        LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) getTag();
-        if (info == null) {
-            // This occurs when LauncherAppWidgetHostView is used to render a preview layout.
-            return;
-        }
-        // Remove and rebind the current widget (which was inflated in the wrong
-        // orientation), but don't delete it from the database
-        mLauncher.removeItem(this, info, false  /* deleteFromDb */,
-                "widget removed because of configuration change");
-        mLauncher.bindAppWidget(info);
-    }
-
-    @Override
     protected boolean shouldAllowDirectClick() {
         if (getTag() instanceof ItemInfo) {
             ItemInfo item = (ItemInfo) getTag();
diff --git a/src/com/android/launcher3/widget/LauncherWidgetHolder.java b/src/com/android/launcher3/widget/LauncherWidgetHolder.java
index 6acc83d..23127b3 100644
--- a/src/com/android/launcher3/widget/LauncherWidgetHolder.java
+++ b/src/com/android/launcher3/widget/LauncherWidgetHolder.java
@@ -28,7 +28,6 @@
 import android.content.Intent;
 import android.os.Bundle;
 import android.util.SparseArray;
-import android.widget.RemoteViews;
 import android.widget.Toast;
 
 import androidx.annotation.NonNull;
@@ -36,17 +35,19 @@
 
 import com.android.launcher3.BaseActivity;
 import com.android.launcher3.BaseDraggingActivity;
-import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
-import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.model.WidgetsModel;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.shared.TestProtocol;
 import com.android.launcher3.util.ResourceBasedOverride;
+import com.android.launcher3.util.SafeCloseable;
+import com.android.launcher3.widget.LauncherAppWidgetHost.ListenableHostView;
 import com.android.launcher3.widget.custom.CustomWidgetManager;
 
+import java.util.ArrayList;
+import java.util.List;
 import java.util.function.IntConsumer;
 
 /**
@@ -64,17 +65,14 @@
             FLAG_STATE_IS_NORMAL | FLAG_ACTIVITY_STARTED | FLAG_ACTIVITY_RESUMED;
 
     @NonNull
-    private final Context mContext;
+    protected final Context mContext;
 
     @NonNull
     private final AppWidgetHost mWidgetHost;
 
     @NonNull
-    private final SparseArray<LauncherAppWidgetHostView> mViews = new SparseArray<>();
-    @NonNull
-    private final SparseArray<PendingAppWidgetHostView> mPendingViews = new SparseArray<>();
-    @NonNull
-    private final SparseArray<LauncherAppWidgetHostView> mDeferredViews = new SparseArray<>();
+    protected final SparseArray<LauncherAppWidgetHostView> mViews = new SparseArray<>();
+    protected final List<ProviderChangedListener> mProviderChangedListeners = new ArrayList<>();
 
     protected int mFlags = FLAG_STATE_IS_NORMAL;
 
@@ -91,7 +89,8 @@
 
     protected AppWidgetHost createHost(
             Context context, @Nullable IntConsumer appWidgetRemovedCallback) {
-        return new LauncherAppWidgetHost(context, appWidgetRemovedCallback, this);
+        return new LauncherAppWidgetHost(
+                context, appWidgetRemovedCallback, mProviderChangedListeners);
     }
 
     /**
@@ -121,25 +120,12 @@
      * Update any views which have been deferred because the host was not listening.
      */
     protected void updateDeferredView() {
+        // Update any views which have been deferred because the host was not listening.
         // We go in reverse order and inflate any deferred or cached widget
         for (int i = mViews.size() - 1; i >= 0; i--) {
             LauncherAppWidgetHostView view = mViews.valueAt(i);
-            if (view instanceof DeferredAppWidgetHostView) {
-                view.reInflate();
-            }
-            if (FeatureFlags.ENABLE_CACHED_WIDGET.get()) {
-                final int appWidgetId = mViews.keyAt(i);
-                if (view == mDeferredViews.get(appWidgetId)) {
-                    // If the widget view was deferred, we'll need to call super.createView here
-                    // to make the binder call to system process to fetch cumulative updates to this
-                    // widget, as well as setting up this view for future updates.
-                    mWidgetHost.createView(view.mLauncher, appWidgetId,
-                            view.getAppWidgetInfo());
-                    // At this point #onCreateView should have been called, which in turn returned
-                    // the deferred view. There's no reason to keep the reference anymore, so we
-                    // removed it here.
-                    mDeferredViews.remove(appWidgetId);
-                }
+            if (view instanceof PendingAppWidgetHostView pv) {
+                pv.reInflate();
             }
         }
     }
@@ -173,34 +159,6 @@
     public void deleteAppWidgetId(int appWidgetId) {
         mWidgetHost.deleteAppWidgetId(appWidgetId);
         mViews.remove(appWidgetId);
-        if (FeatureFlags.ENABLE_CACHED_WIDGET.get()) {
-            final LauncherAppState state = LauncherAppState.getInstance(mContext);
-            synchronized (state.mCachedRemoteViews) {
-                state.mCachedRemoteViews.delete(appWidgetId);
-            }
-        }
-    }
-
-    /**
-     * Add the pending view to the host for complete configuration in further steps
-     * @param appWidgetId The ID of the specified app widget
-     * @param view The {@link PendingAppWidgetHostView} of the app widget
-     */
-    public void addPendingView(int appWidgetId, @NonNull PendingAppWidgetHostView view) {
-        mPendingViews.put(appWidgetId, view);
-    }
-
-    /**
-     * @param appWidgetId The app widget id of the specified widget
-     * @return The {@link PendingAppWidgetHostView} of the widget if it exists, null otherwise
-     */
-    @Nullable
-    protected PendingAppWidgetHostView getPendingView(int appWidgetId) {
-        return mPendingViews.get(appWidgetId);
-    }
-
-    protected void removePendingView(int appWidgetId) {
-        mPendingViews.remove(appWidgetId);
     }
 
     /**
@@ -225,18 +183,18 @@
      * Add a listener that is triggered when the providers of the widgets are changed
      * @param listener The listener that notifies when the providers changed
      */
-    public void addProviderChangeListener(@NonNull ProviderChangedListener listener) {
-        LauncherAppWidgetHost tempHost = (LauncherAppWidgetHost) mWidgetHost;
-        tempHost.addProviderChangeListener(listener);
+    public void addProviderChangeListener(
+            @NonNull LauncherWidgetHolder.ProviderChangedListener listener) {
+        MAIN_EXECUTOR.execute(() -> mProviderChangedListeners.add(listener));
     }
 
     /**
      * Remove the specified listener from the host
      * @param listener The listener that is to be removed from the host
      */
-    public void removeProviderChangeListener(ProviderChangedListener listener) {
-        LauncherAppWidgetHost tempHost = (LauncherAppWidgetHost) mWidgetHost;
-        tempHost.removeProviderChangeListener(listener);
+    public void removeProviderChangeListener(
+            LauncherWidgetHolder.ProviderChangedListener listener) {
+        MAIN_EXECUTOR.execute(() -> mProviderChangedListeners.remove(listener));
     }
 
     /**
@@ -319,17 +277,6 @@
         if (WidgetsModel.GO_DISABLE_WIDGETS) {
             return;
         }
-        if (FeatureFlags.ENABLE_CACHED_WIDGET.get()) {
-            // Cache the content from the widgets when Launcher stops listening to widget updates
-            final LauncherAppState state = LauncherAppState.getInstance(mContext);
-            synchronized (state.mCachedRemoteViews) {
-                for (int i = 0; i < mViews.size(); i++) {
-                    final int appWidgetId = mViews.keyAt(i);
-                    final LauncherAppWidgetHostView view = mViews.get(appWidgetId);
-                    state.mCachedRemoteViews.put(appWidgetId, view.mLastRemoteViews);
-                }
-            }
-        }
         mWidgetHost.stopListening();
         setListeningFlag(false);
     }
@@ -351,47 +298,51 @@
     }
 
     /**
+     * Adds a callback to be run everytime the provided app widget updates.
+     * @return a closable to remove this callback
+     */
+    public SafeCloseable addOnUpdateListener(
+            int appWidgetId, LauncherAppWidgetProviderInfo appWidget, Runnable callback) {
+        if (createView(appWidgetId, appWidget) instanceof ListenableHostView lhv) {
+            return lhv.addUpdateListener(callback);
+        }
+        return () -> { };
+    }
+
+    /**
      * Create a view for the specified app widget
-     * @param context The activity context for which the view is created
+     *
      * @param appWidgetId The ID of the widget
-     * @param appWidget The {@link LauncherAppWidgetProviderInfo} of the widget
+     * @param appWidget   The {@link LauncherAppWidgetProviderInfo} of the widget
      * @return A view for the widget
      */
     @NonNull
-    public AppWidgetHostView createView(@NonNull Context context, int appWidgetId,
-            @NonNull LauncherAppWidgetProviderInfo appWidget) {
+    public AppWidgetHostView createView(
+            int appWidgetId, @NonNull LauncherAppWidgetProviderInfo appWidget) {
         if (appWidget.isCustomWidget()) {
-            LauncherAppWidgetHostView lahv = new LauncherAppWidgetHostView(context);
+            LauncherAppWidgetHostView lahv = new LauncherAppWidgetHostView(mContext);
             lahv.setAppWidget(0, appWidget);
-            CustomWidgetManager.INSTANCE.get(context).onViewCreated(lahv);
+            CustomWidgetManager.INSTANCE.get(mContext).onViewCreated(lahv);
             return lahv;
-        } else if ((mFlags & FLAG_LISTENING) == 0) {
+        }
+
+        LauncherAppWidgetHostView view = createViewInternal(appWidgetId, appWidget);
+        mViews.put(appWidgetId, view);
+        return view;
+    }
+
+    @NonNull
+    protected LauncherAppWidgetHostView createViewInternal(
+            int appWidgetId, @NonNull LauncherAppWidgetProviderInfo appWidget) {
+        if ((mFlags & FLAG_LISTENING) == 0) {
             // Since the launcher hasn't started listening to widget updates, we can't simply call
-            // super.createView here because the later will make a binder call to retrieve
+            // host.createView here because the later will make a binder call to retrieve
             // RemoteViews from system process.
-            // TODO: have launcher always listens to widget updates in background so that this
-            //  check can be removed altogether.
-            if (FeatureFlags.ENABLE_CACHED_WIDGET.get()) {
-                final RemoteViews cachedRemoteViews = getCachedRemoteViews(appWidgetId);
-                if (cachedRemoteViews != null) {
-                    // We've found RemoteViews from cache for this widget, so we will instantiate a
-                    // widget host view and populate it with the cached RemoteViews.
-                    final LauncherAppWidgetHostView view = new LauncherAppWidgetHostView(context);
-                    view.setAppWidget(appWidgetId, appWidget);
-                    view.updateAppWidget(cachedRemoteViews);
-                    mDeferredViews.put(appWidgetId, view);
-                    mViews.put(appWidgetId, view);
-                    return view;
-                }
-            }
-            // If cache misses or not enabled, a placeholder for the widget will be returned.
-            DeferredAppWidgetHostView view = new DeferredAppWidgetHostView(context);
-            view.setAppWidget(appWidgetId, appWidget);
-            mViews.put(appWidgetId, view);
-            return view;
+            return new PendingAppWidgetHostView(mContext, appWidgetId, appWidget);
         } else {
             try {
-                return mWidgetHost.createView(context, appWidgetId, appWidget);
+                return (LauncherAppWidgetHostView) mWidgetHost.createView(
+                        mContext, appWidgetId, appWidget);
             } catch (Exception e) {
                 if (!Utilities.isBinderSizeError(e)) {
                     throw new RuntimeException(e);
@@ -402,7 +353,7 @@
                 // will update.
                 LauncherAppWidgetHostView view = mViews.get(appWidgetId);
                 if (view == null) {
-                    view = onCreateView(mContext, appWidgetId, appWidget);
+                    view = new ListenableHostView(mContext);
                 }
                 view.setAppWidget(appWidgetId, appWidget);
                 view.switchToErrorView();
@@ -422,41 +373,11 @@
     }
 
     /**
-     * Called to return a proper view when creating a view
-     * @param context The context for which the widget view is created
-     * @param appWidgetId The ID of the added widget
-     * @param appWidget The provider info of the added widget
-     * @return A view for the specified app widget
-     */
-    @NonNull
-    public LauncherAppWidgetHostView onCreateView(Context context, int appWidgetId,
-            AppWidgetProviderInfo appWidget) {
-        final LauncherAppWidgetHostView view;
-        if (getPendingView(appWidgetId) != null) {
-            view = getPendingView(appWidgetId);
-            removePendingView(appWidgetId);
-        } else if (mDeferredViews.get(appWidgetId) != null) {
-            // In case the widget view is deferred, we will simply return the deferred view as
-            // opposed to instantiate a new instance of LauncherAppWidgetHostView since launcher
-            // already added the former to the workspace.
-            view = mDeferredViews.get(appWidgetId);
-        } else {
-            view = new LauncherAppWidgetHostView(context);
-        }
-        mViews.put(appWidgetId, view);
-        return view;
-    }
-
-    /**
      * Clears all the views from the host
      */
     public void clearViews() {
         LauncherAppWidgetHost tempHost = (LauncherAppWidgetHost) mWidgetHost;
         tempHost.clearViews();
-        if (FeatureFlags.ENABLE_CACHED_WIDGET.get()) {
-            // Clear previously cached content from existing widgets
-            mDeferredViews.clear();
-        }
         mViews.clear();
     }
 
@@ -496,14 +417,6 @@
         return (flags & FLAGS_SHOULD_LISTEN) == FLAGS_SHOULD_LISTEN;
     }
 
-    @Nullable
-    private RemoteViews getCachedRemoteViews(int appWidgetId) {
-        final LauncherAppState state = LauncherAppState.getInstance(mContext);
-        synchronized (state.mCachedRemoteViews) {
-            return state.mCachedRemoteViews.get(appWidgetId);
-        }
-    }
-
     /**
      * Returns the new LauncherWidgetHolder instance
      */
diff --git a/src/com/android/launcher3/widget/PendingAppWidgetHostView.java b/src/com/android/launcher3/widget/PendingAppWidgetHostView.java
index 1c88c4a..25979c2 100644
--- a/src/com/android/launcher3/widget/PendingAppWidgetHostView.java
+++ b/src/com/android/launcher3/widget/PendingAppWidgetHostView.java
@@ -18,17 +18,20 @@
 
 import static com.android.launcher3.graphics.PreloadIconDrawable.newPendingIcon;
 import static com.android.launcher3.icons.FastBitmapDrawable.getDisabledColorFilter;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 
 import android.content.Context;
 import android.graphics.Canvas;
 import android.graphics.Color;
 import android.graphics.PorterDuff;
 import android.graphics.Rect;
+import android.graphics.drawable.ColorDrawable;
 import android.graphics.drawable.Drawable;
 import android.os.Bundle;
 import android.text.Layout;
 import android.text.StaticLayout;
 import android.text.TextPaint;
+import android.text.TextUtils;
 import android.util.SizeF;
 import android.util.TypedValue;
 import android.view.ContextThemeWrapper;
@@ -36,17 +39,18 @@
 import android.view.View.OnClickListener;
 import android.widget.RemoteViews;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.R;
 import com.android.launcher3.icons.FastBitmapDrawable;
-import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.icons.IconCache.ItemInfoUpdateReceiver;
 import com.android.launcher3.model.data.ItemInfoWithIcon;
 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
 import com.android.launcher3.model.data.PackageItemInfo;
-import com.android.launcher3.touch.ItemClickHandler;
+import com.android.launcher3.util.SafeCloseable;
 import com.android.launcher3.util.Themes;
 
 import java.util.List;
@@ -56,11 +60,24 @@
     private static final float SETUP_ICON_SIZE_FACTOR = 2f / 5;
     private static final float MIN_SATUNATION = 0.7f;
 
+    private static final int FLAG_DRAW_SETTINGS = 1;
+    private static final int FLAG_DRAW_ICON = 2;
+    private static final int FLAG_DRAW_LABEL = 4;
+
+    private static final int DEFERRED_ALPHA = 0x77;
+
     private final Rect mRect = new Rect();
-    private OnClickListener mClickListener;
+
+    private final LauncherAppWidgetProviderInfo mAppwidget;
     private final LauncherAppWidgetInfo mInfo;
     private final int mStartState;
     private final boolean mDisabledForSafeMode;
+    private final CharSequence mLabel;
+
+    private OnClickListener mClickListener;
+    private SafeCloseable mOnDetachCleanup;
+
+    private int mDragFlags;
 
     private Drawable mCenterDrawable;
     private Drawable mSettingIconDrawable;
@@ -71,19 +88,9 @@
     private Layout mSetupTextLayout;
 
     public PendingAppWidgetHostView(Context context, LauncherAppWidgetInfo info,
-            IconCache cache, boolean disabledForSafeMode) {
-        super(new ContextThemeWrapper(context, R.style.WidgetContainerTheme));
-
-        mInfo = info;
-        mStartState = info.restoreStatus;
-        mDisabledForSafeMode = disabledForSafeMode;
-
-        mPaint = new TextPaint();
-        mPaint.setColor(Themes.getAttrColor(getContext(), android.R.attr.textColorPrimary));
-        mPaint.setTextSize(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX,
-                mLauncher.getDeviceProfile().iconTextSizePx, getResources().getDisplayMetrics()));
-        setBackgroundResource(R.drawable.pending_widget_bg);
-        setWillNotDraw(false);
+            @Nullable LauncherAppWidgetProviderInfo appWidget) {
+        this(context, info, appWidget,
+                context.getResources().getText(R.string.gadget_complete_setup_text));
 
         super.updateAppWidget(null);
         setOnClickListener(mLauncher.getItemOnClickListener());
@@ -91,22 +98,102 @@
         if (info.pendingItemInfo == null) {
             info.pendingItemInfo = new PackageItemInfo(info.providerName.getPackageName(),
                     info.user);
-            cache.updateIconInBackground(this, info.pendingItemInfo);
+            LauncherAppState.getInstance(context).getIconCache()
+                    .updateIconInBackground(this, info.pendingItemInfo);
         } else {
             reapplyItemInfo(info.pendingItemInfo);
         }
     }
 
+    public PendingAppWidgetHostView(
+            Context context, int appWidgetId, @NonNull LauncherAppWidgetProviderInfo appWidget) {
+        this(context, new LauncherAppWidgetInfo(appWidgetId, appWidget.provider),
+                appWidget, appWidget.label);
+        getBackground().mutate().setAlpha(DEFERRED_ALPHA);
+
+        mCenterDrawable = new ColorDrawable(Color.TRANSPARENT);
+        mDragFlags = FLAG_DRAW_LABEL;
+        mDrawableSizeChanged = true;
+    }
+
+    private PendingAppWidgetHostView(Context context, LauncherAppWidgetInfo info,
+            LauncherAppWidgetProviderInfo appwidget, CharSequence label) {
+        super(new ContextThemeWrapper(context, R.style.WidgetContainerTheme));
+
+        mAppwidget = appwidget;
+        mInfo = info;
+        mStartState = info.restoreStatus;
+        mDisabledForSafeMode = LauncherAppState.getInstance(context).isSafeModeEnabled();
+        mLabel = label;
+
+        mPaint = new TextPaint();
+        mPaint.setColor(Themes.getAttrColor(getContext(), android.R.attr.textColorPrimary));
+        mPaint.setTextSize(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX,
+                mLauncher.getDeviceProfile().iconTextSizePx, getResources().getDisplayMetrics()));
+
+        setWillNotDraw(false);
+        setBackgroundResource(R.drawable.pending_widget_bg);
+    }
+
     @Override
     public void updateAppWidget(RemoteViews remoteViews) {
+        checkIfRestored();
+    }
+
+    private void checkIfRestored() {
         WidgetManagerHelper widgetManagerHelper = new WidgetManagerHelper(getContext());
         if (widgetManagerHelper.isAppWidgetRestored(mInfo.appWidgetId)) {
-            super.updateAppWidget(remoteViews);
-            reInflate();
+            MAIN_EXECUTOR.getHandler().post(this::reInflate);
         }
     }
 
     @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+
+        if ((mAppwidget != null)
+                && !mInfo.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_ID_NOT_VALID)
+                && mInfo.restoreStatus != LauncherAppWidgetInfo.RESTORE_COMPLETED) {
+            // If the widget is not completely restored, but has a valid ID, then listen of
+            // updates from provider app for potential restore complete.
+            if (mOnDetachCleanup != null) {
+                mOnDetachCleanup.close();
+            }
+            mOnDetachCleanup = mLauncher.getAppWidgetHolder()
+                    .addOnUpdateListener(mInfo.appWidgetId, mAppwidget, this::checkIfRestored);
+            checkIfRestored();
+        }
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        if (mOnDetachCleanup != null) {
+            mOnDetachCleanup.close();
+            mOnDetachCleanup = null;
+        }
+    }
+
+    /**
+     * Forces the Launcher to reinflate the widget view
+     */
+    public void reInflate() {
+        if (!isAttachedToWindow()) {
+            return;
+        }
+        LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) getTag();
+        if (info == null) {
+            // This occurs when LauncherAppWidgetHostView is used to render a preview layout.
+            return;
+        }
+        // Remove and rebind the current widget (which was inflated in the wrong
+        // orientation), but don't delete it from the database
+        mLauncher.removeItem(this, info, false  /* deleteFromDb */,
+                "widget removed because of configuration change");
+        mLauncher.bindAppWidget(info);
+    }
+
+    @Override
     public void updateAppWidgetSize(Bundle newOptions, int minWidth, int minHeight, int maxWidth,
             int maxHeight) {
         // No-op
@@ -147,7 +234,10 @@
             mCenterDrawable.setCallback(null);
             mCenterDrawable = null;
         }
+        mDragFlags = 0;
         if (info.bitmap.icon != null) {
+            mDragFlags = FLAG_DRAW_ICON;
+
             Drawable widgetCategoryIcon = getWidgetCategoryIcon();
             // The view displays three modes,
             //   1) App icon in the center
@@ -169,6 +259,8 @@
                         : widgetCategoryIcon;
                 mSettingIconDrawable = getResources().getDrawable(R.drawable.ic_setting).mutate();
                 updateSettingColor(info.bitmap.color);
+
+                mDragFlags |= FLAG_DRAW_SETTINGS | FLAG_DRAW_LABEL;
             } else {
                 mCenterDrawable = widgetCategoryIcon == null
                         ? newPendingIcon(getContext(), info)
@@ -239,68 +331,63 @@
         int availableWidth = getWidth() - paddingLeft - paddingRight - 2 * minPadding;
         int availableHeight = getHeight() - paddingTop - paddingBottom - 2 * minPadding;
 
-        if (mSettingIconDrawable == null) {
-            int maxSize = grid.iconSizePx;
-            int size = Math.min(maxSize, Math.min(availableWidth, availableHeight));
+        float iconSize = ((mDragFlags & FLAG_DRAW_ICON) == 0) ? 0
+                : Math.max(0, Math.min(availableWidth, availableHeight));
+        // Use twice the setting size factor, as the setting is drawn at a corner and the
+        // icon is drawn in the center.
+        float settingIconScaleFactor = ((mDragFlags & FLAG_DRAW_SETTINGS) == 0) ? 0
+                : 1 + SETUP_ICON_SIZE_FACTOR * 2;
 
-            mRect.set(0, 0, size, size);
-            mRect.offsetTo((getWidth() - mRect.width()) / 2, (getHeight() - mRect.height()) / 2);
-            mCenterDrawable.setBounds(mRect);
-        } else  {
-            float iconSize = Math.max(0, Math.min(availableWidth, availableHeight));
+        int maxSize = Math.max(availableWidth, availableHeight);
+        if (iconSize * settingIconScaleFactor > maxSize) {
+            // There is an overlap
+            iconSize = maxSize / settingIconScaleFactor;
+        }
 
-            // Use twice the setting size factor, as the setting is drawn at a corner and the
-            // icon is drawn in the center.
-            float settingIconScaleFactor = 1 + SETUP_ICON_SIZE_FACTOR * 2;
-            int maxSize = Math.max(availableWidth, availableHeight);
-            if (iconSize * settingIconScaleFactor > maxSize) {
-                // There is an overlap
-                iconSize = maxSize / settingIconScaleFactor;
+        int actualIconSize = (int) Math.min(iconSize, grid.iconSizePx);
+
+        // Icon top when we do not draw the text
+        int iconTop = (getHeight() - actualIconSize) / 2;
+        mSetupTextLayout = null;
+
+        if (availableWidth > 0 && !TextUtils.isEmpty(mLabel)
+                && ((mDragFlags & FLAG_DRAW_LABEL) != 0)) {
+            // Recreate the setup text.
+            mSetupTextLayout = new StaticLayout(
+                    mLabel, mPaint, availableWidth, Layout.Alignment.ALIGN_CENTER, 1, 0, true);
+            int textHeight = mSetupTextLayout.getHeight();
+
+            // Extra icon size due to the setting icon
+            float minHeightWithText = textHeight + actualIconSize * settingIconScaleFactor
+                    + grid.iconDrawablePaddingPx;
+
+            if (minHeightWithText < availableHeight) {
+                // We can draw the text as well
+                iconTop = (getHeight() - textHeight
+                        - grid.iconDrawablePaddingPx - actualIconSize) / 2;
+
+            } else {
+                // We can't draw the text. Let the iconTop be same as before.
+                mSetupTextLayout = null;
             }
+        }
 
-            int actualIconSize = (int) Math.min(iconSize, grid.iconSizePx);
+        mRect.set(0, 0, actualIconSize, actualIconSize);
+        mRect.offset((getWidth() - actualIconSize) / 2, iconTop);
+        mCenterDrawable.setBounds(mRect);
 
-            // Icon top when we do not draw the text
-            int iconTop = (getHeight() - actualIconSize) / 2;
-            mSetupTextLayout = null;
-
-            if (availableWidth > 0) {
-                // Recreate the setup text.
-                mSetupTextLayout = new StaticLayout(
-                        getResources().getText(R.string.gadget_complete_setup_text), mPaint,
-                        availableWidth, Layout.Alignment.ALIGN_CENTER, 1, 0, true);
-                int textHeight = mSetupTextLayout.getHeight();
-
-                // Extra icon size due to the setting icon
-                float minHeightWithText = textHeight + actualIconSize * settingIconScaleFactor
-                        + grid.iconDrawablePaddingPx;
-
-                if (minHeightWithText < availableHeight) {
-                    // We can draw the text as well
-                    iconTop = (getHeight() - textHeight -
-                            grid.iconDrawablePaddingPx - actualIconSize) / 2;
-
-                } else {
-                    // We can't draw the text. Let the iconTop be same as before.
-                    mSetupTextLayout = null;
-                }
-            }
-
-            mRect.set(0, 0, actualIconSize, actualIconSize);
-            mRect.offset((getWidth() - actualIconSize) / 2, iconTop);
-            mCenterDrawable.setBounds(mRect);
-
+        if (mSettingIconDrawable != null) {
             mRect.left = paddingLeft + minPadding;
             mRect.right = mRect.left + (int) (SETUP_ICON_SIZE_FACTOR * actualIconSize);
             mRect.top = paddingTop + minPadding;
             mRect.bottom = mRect.top + (int) (SETUP_ICON_SIZE_FACTOR * actualIconSize);
             mSettingIconDrawable.setBounds(mRect);
+        }
 
-            if (mSetupTextLayout != null) {
-                // Set up position for dragging the text
-                mRect.left = paddingLeft + minPadding;
-                mRect.top = mCenterDrawable.getBounds().bottom + grid.iconDrawablePaddingPx;
-            }
+        if (mSetupTextLayout != null) {
+            // Set up position for dragging the text
+            mRect.left = paddingLeft + minPadding;
+            mRect.top = mCenterDrawable.getBounds().bottom + grid.iconDrawablePaddingPx;
         }
     }
 
diff --git a/src/com/android/launcher3/widget/WidgetCell.java b/src/com/android/launcher3/widget/WidgetCell.java
index c30342a..8f5e2b6 100644
--- a/src/com/android/launcher3/widget/WidgetCell.java
+++ b/src/com/android/launcher3/widget/WidgetCell.java
@@ -147,6 +147,11 @@
         return mAppWidgetHostViewScale;
     }
 
+    /** Returns the {@link WidgetItem} for this {@link WidgetCell}. */
+    public WidgetItem getWidgetItem() {
+        return mItem;
+    }
+
     /**
      * Called to clear the view and free attached resources. (e.g., {@link Bitmap}
      */
diff --git a/src/com/android/launcher3/widget/WidgetHostViewLoader.java b/src/com/android/launcher3/widget/WidgetHostViewLoader.java
index b18cd47..1cc00ef 100644
--- a/src/com/android/launcher3/widget/WidgetHostViewLoader.java
+++ b/src/com/android/launcher3/widget/WidgetHostViewLoader.java
@@ -1,7 +1,6 @@
 package com.android.launcher3.widget;
 
 import android.appwidget.AppWidgetHostView;
-import android.content.Context;
 import android.os.Bundle;
 import android.os.Handler;
 import android.util.Log;
@@ -117,7 +116,7 @@
                     return;
                 }
                 AppWidgetHostView hostView = mLauncher.getAppWidgetHolder().createView(
-                        (Context) mLauncher, mWidgetLoadingId, pInfo);
+                        mWidgetLoadingId, pInfo);
                 mInfo.boundWidget = hostView;
 
                 // We used up the widget Id in binding the above view.
diff --git a/src/com/android/launcher3/widget/WidgetInflater.kt b/src/com/android/launcher3/widget/WidgetInflater.kt
new file mode 100644
index 0000000..dd50b71
--- /dev/null
+++ b/src/com/android/launcher3/widget/WidgetInflater.kt
@@ -0,0 +1,204 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.widget
+
+import android.content.Context
+import com.android.launcher3.Launcher
+import com.android.launcher3.LauncherAppState
+import com.android.launcher3.backuprestore.LauncherRestoreEventLogger.RestoreError
+import com.android.launcher3.logging.FileLog
+import com.android.launcher3.model.WidgetsModel
+import com.android.launcher3.model.data.LauncherAppWidgetInfo
+import com.android.launcher3.qsb.QsbContainerView
+
+/** Utility class for handling widget inflation taking into account all the restore state updates */
+class WidgetInflater(private val context: Context) {
+
+    private val widgetHelper = WidgetManagerHelper(context)
+
+    fun inflateAppWidget(
+        item: LauncherAppWidgetInfo,
+    ): InflationResult {
+        if (item.hasOptionFlag(LauncherAppWidgetInfo.OPTION_SEARCH_WIDGET)) {
+            item.providerName = QsbContainerView.getSearchComponentName(context)
+            if (item.providerName == null) {
+                return InflationResult(
+                    TYPE_DELETE,
+                    reason = "search widget removed because search component cannot be found",
+                    restoreErrorType = RestoreError.NO_SEARCH_WIDGET
+                )
+            }
+        }
+        if (LauncherAppState.INSTANCE.get(context).isSafeModeEnabled) {
+            return InflationResult(TYPE_PENDING)
+        }
+        val appWidgetInfo: LauncherAppWidgetProviderInfo?
+        var removalReason = ""
+        @RestoreError var logReason = RestoreError.APP_NOT_INSTALLED
+        var update = false
+
+        if (item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_ID_NOT_VALID)) {
+            // The widget id is not valid. Try to find the widget based on the provider info.
+            appWidgetInfo = widgetHelper.findProvider(item.providerName, item.user)
+            if (appWidgetInfo == null) {
+                if (WidgetsModel.GO_DISABLE_WIDGETS) {
+                    removalReason = "widgets are disabled on go device."
+                    logReason = RestoreError.WIDGETS_DISABLED
+                } else {
+                    removalReason = "WidgetManagerHelper cannot find a provider from provider info."
+                    logReason = RestoreError.MISSING_WIDGET_PROVIDER
+                }
+            } else if (item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY)) {
+                // since appWidgetInfo is not null anymore, update the provider status
+                item.restoreStatus =
+                    item.restoreStatus and LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY.inv()
+                update = true
+            }
+        } else {
+            appWidgetInfo =
+                widgetHelper.getLauncherAppWidgetInfo(item.appWidgetId, item.targetComponent)
+            if (appWidgetInfo == null) {
+                if (item.appWidgetId <= LauncherAppWidgetInfo.CUSTOM_WIDGET_ID) {
+                    removalReason = "CustomWidgetManager cannot find provider from that widget id."
+                    logReason = RestoreError.MISSING_INFO
+                } else {
+                    removalReason =
+                        ("AppWidgetManager cannot find provider for that widget id." +
+                            " It could be because AppWidgetService is not available, or the" +
+                            " appWidgetId has not been bound to a the provider yet, or you" +
+                            " don't have access to that appWidgetId.")
+                    logReason = RestoreError.INVALID_WIDGET_ID
+                }
+            }
+        }
+
+        // If the provider is ready, but the widget is not yet restored, try to restore it.
+        if (
+            !item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY) &&
+                item.restoreStatus != LauncherAppWidgetInfo.RESTORE_COMPLETED
+        ) {
+            if (appWidgetInfo == null) {
+                return InflationResult(
+                    type = TYPE_DELETE,
+                    reason =
+                        "Removing restored widget: id=${item.appWidgetId} belongs to component ${item.providerName} user ${item.user}, as the provider is null and $removalReason",
+                    restoreErrorType = logReason
+                )
+            }
+
+            // If we do not have a valid id, try to bind an id.
+            if (item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_ID_NOT_VALID)) {
+                if (!item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_ID_ALLOCATED)) {
+                    // Id has not been allocated yet. Allocate a new id.
+                    LauncherWidgetHolder.newInstance(context).let {
+                        item.appWidgetId = it.allocateAppWidgetId()
+                        it.destroy()
+                    }
+                    item.restoreStatus =
+                        item.restoreStatus or LauncherAppWidgetInfo.FLAG_ID_ALLOCATED
+
+                    // Also try to bind the widget. If the bind fails, the user will be shown
+                    // a click to setup UI, which will ask for the bind permission.
+                    val pendingInfo = PendingAddWidgetInfo(appWidgetInfo, item.sourceContainer)
+                    pendingInfo.spanX = item.spanX
+                    pendingInfo.spanY = item.spanY
+                    pendingInfo.minSpanX = item.minSpanX
+                    pendingInfo.minSpanY = item.minSpanY
+                    var options = pendingInfo.getDefaultSizeOptions(context)
+                    val isDirectConfig =
+                        item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_DIRECT_CONFIG)
+                    if (isDirectConfig && item.bindOptions != null) {
+                        val newOptions = item.bindOptions.extras
+                        if (options != null) {
+                            newOptions!!.putAll(options)
+                        }
+                        options = newOptions
+                    }
+                    val success =
+                        widgetHelper.bindAppWidgetIdIfAllowed(
+                            item.appWidgetId,
+                            appWidgetInfo,
+                            options
+                        )
+
+                    // We tried to bind once. If we were not able to bind, we would need to
+                    // go through the permission dialog, which means we cannot skip the config
+                    // activity.
+                    item.bindOptions = null
+                    item.restoreStatus =
+                        item.restoreStatus and LauncherAppWidgetInfo.FLAG_DIRECT_CONFIG.inv()
+
+                    // Bind succeeded
+                    if (success) {
+                        // If the widget has a configure activity, it is still needs to set it
+                        // up, otherwise the widget is ready to go.
+                        item.restoreStatus =
+                            if ((appWidgetInfo.configure == null) || isDirectConfig)
+                                LauncherAppWidgetInfo.RESTORE_COMPLETED
+                            else LauncherAppWidgetInfo.FLAG_UI_NOT_READY
+                    }
+                    update = true
+                }
+            } else if (
+                (item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_UI_NOT_READY) &&
+                    (appWidgetInfo.configure == null))
+            ) {
+                // The widget was marked as UI not ready, but there is no configure activity to
+                // update the UI.
+                item.restoreStatus = LauncherAppWidgetInfo.RESTORE_COMPLETED
+                update = true
+            } else if (
+                (item.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_UI_NOT_READY) &&
+                    appWidgetInfo.configure != null)
+            ) {
+                if (widgetHelper.isAppWidgetRestored(item.appWidgetId)) {
+                    item.restoreStatus = LauncherAppWidgetInfo.RESTORE_COMPLETED
+                    update = true
+                }
+            }
+        }
+
+        if (item.restoreStatus == LauncherAppWidgetInfo.RESTORE_COMPLETED) {
+            // Verify that we own the widget
+            if (appWidgetInfo == null) {
+                FileLog.e(Launcher.TAG, "Removing invalid widget: id=" + item.appWidgetId)
+                return InflationResult(TYPE_DELETE, reason = removalReason)
+            }
+            item.minSpanX = appWidgetInfo.minSpanX
+            item.minSpanY = appWidgetInfo.minSpanY
+            return InflationResult(TYPE_REAL, isUpdate = update, widgetInfo = appWidgetInfo)
+        } else {
+            return InflationResult(TYPE_PENDING, isUpdate = update, widgetInfo = appWidgetInfo)
+        }
+    }
+
+    data class InflationResult(
+        val type: Int,
+        val reason: String? = null,
+        @RestoreError val restoreErrorType: String = RestoreError.APP_NOT_INSTALLED,
+        val isUpdate: Boolean = false,
+        val widgetInfo: LauncherAppWidgetProviderInfo? = null
+    )
+
+    companion object {
+        const val TYPE_DELETE = 0
+
+        const val TYPE_PENDING = 1
+
+        const val TYPE_REAL = 2
+    }
+}
diff --git a/src/com/android/launcher3/widget/WidgetManagerHelper.java b/src/com/android/launcher3/widget/WidgetManagerHelper.java
index 0860e72..058523b 100644
--- a/src/com/android/launcher3/widget/WidgetManagerHelper.java
+++ b/src/com/android/launcher3/widget/WidgetManagerHelper.java
@@ -61,8 +61,7 @@
             int appWidgetId, ComponentName componentName) {
 
         // For custom widgets.
-        if (appWidgetId <= LauncherAppWidgetInfo.CUSTOM_WIDGET_ID && !CustomWidgetManager
-                .INSTANCE.get(mContext).getWidgetIdForCustomProvider(componentName).equals("")) {
+        if (appWidgetId <= LauncherAppWidgetInfo.CUSTOM_WIDGET_ID) {
             return CustomWidgetManager.INSTANCE.get(mContext).getWidgetProvider(componentName);
         }
 
diff --git a/src/com/android/launcher3/widget/custom/CustomAppWidgetProviderInfo.java b/src/com/android/launcher3/widget/custom/CustomAppWidgetProviderInfo.java
index 44571a6..398b1df 100644
--- a/src/com/android/launcher3/widget/custom/CustomAppWidgetProviderInfo.java
+++ b/src/com/android/launcher3/widget/custom/CustomAppWidgetProviderInfo.java
@@ -33,13 +33,9 @@
 public class CustomAppWidgetProviderInfo extends LauncherAppWidgetProviderInfo
         implements Parcelable {
 
-    public final String providerId;
-
-    protected CustomAppWidgetProviderInfo(Parcel parcel, boolean readSelf, String providerId) {
+    protected CustomAppWidgetProviderInfo(Parcel parcel, boolean readSelf) {
         super(parcel);
         if (readSelf) {
-            this.providerId = parcel.readString();
-
             provider = new ComponentName(parcel.readString(),
                     CLS_CUSTOM_WIDGET_PREFIX + parcel.readString());
 
@@ -53,8 +49,6 @@
             spanY = parcel.readInt();
             minSpanX = parcel.readInt();
             minSpanY = parcel.readInt();
-        } else {
-            this.providerId = providerId;
         }
     }
 
@@ -77,7 +71,6 @@
     @Override
     public void writeToParcel(Parcel out, int flags) {
         super.writeToParcel(out, flags);
-        out.writeString(providerId);
         out.writeString(provider.getPackageName());
         out.writeString(provider.getClassName());
 
@@ -93,12 +86,12 @@
         out.writeInt(minSpanY);
     }
 
-    public static final Parcelable.Creator<CustomAppWidgetProviderInfo> CREATOR
-            = new Parcelable.Creator<CustomAppWidgetProviderInfo>() {
+    public static final Parcelable.Creator<CustomAppWidgetProviderInfo> CREATOR =
+            new Parcelable.Creator<>() {
 
         @Override
         public CustomAppWidgetProviderInfo createFromParcel(Parcel parcel) {
-            return new CustomAppWidgetProviderInfo(parcel, true, "");
+            return new CustomAppWidgetProviderInfo(parcel, true);
         }
 
         @Override
diff --git a/src/com/android/launcher3/widget/custom/CustomWidgetManager.java b/src/com/android/launcher3/widget/custom/CustomWidgetManager.java
index 7cf0221..2fdf354 100644
--- a/src/com/android/launcher3/widget/custom/CustomWidgetManager.java
+++ b/src/com/android/launcher3/widget/custom/CustomWidgetManager.java
@@ -18,6 +18,7 @@
 
 import static com.android.launcher3.config.FeatureFlags.SMARTSPACE_AS_A_WIDGET;
 import static com.android.launcher3.model.data.LauncherAppWidgetInfo.CUSTOM_WIDGET_ID;
+import static com.android.launcher3.widget.LauncherAppWidgetProviderInfo.CLS_CUSTOM_WIDGET_PREFIX;
 
 import android.appwidget.AppWidgetManager;
 import android.appwidget.AppWidgetProviderInfo;
@@ -44,7 +45,6 @@
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
 import java.util.function.Consumer;
 import java.util.stream.Stream;
 
@@ -57,17 +57,16 @@
             new MainThreadInitializedObject<>(CustomWidgetManager::new);
 
     private static final String TAG = "CustomWidgetManager";
+    private static final String PLUGIN_PKG = "android";
     private final Context mContext;
-    private final HashMap<String, CustomWidgetPlugin> mPlugins;
+    private final HashMap<ComponentName, CustomWidgetPlugin> mPlugins;
     private final List<CustomAppWidgetProviderInfo> mCustomWidgets;
-    private final HashMap<ComponentName, String> mWidgetsIdMap;
     private Consumer<PackageUserKey> mWidgetRefreshCallback;
 
     private CustomWidgetManager(Context context) {
         mContext = context;
         mPlugins = new HashMap<>();
         mCustomWidgets = new ArrayList<>();
-        mWidgetsIdMap = new HashMap<>();
         PluginManagerWrapper.INSTANCE.get(context)
                 .addPluginListener(this, CustomWidgetPlugin.class, true);
 
@@ -78,8 +77,7 @@
                     Class<?> cls = Class.forName(s);
                     CustomWidgetPlugin plugin = (CustomWidgetPlugin)
                             cls.getDeclaredConstructor(Context.class).newInstance(context);
-                    mPlugins.put(plugin.getId(), plugin);
-                    onPluginConnected(mPlugins.get(plugin.getId()), context);
+                    onPluginConnected(plugin, context);
                 } catch (ClassNotFoundException | InstantiationException | IllegalAccessException
                          | ClassCastException | NoSuchMethodException
                          | InvocationTargetException e) {
@@ -102,35 +100,17 @@
         Parcel parcel = Parcel.obtain();
         providers.get(0).writeToParcel(parcel, 0);
         parcel.setDataPosition(0);
-        CustomAppWidgetProviderInfo info = newInfo(plugin.getId(), plugin, parcel, context);
+        CustomAppWidgetProviderInfo info = newInfo(plugin, parcel);
         parcel.recycle();
+        mPlugins.put(info.provider, plugin);
         mCustomWidgets.add(info);
-        mWidgetsIdMap.put(info.provider, plugin.getId());
     }
 
     @Override
     public void onPluginDisconnected(CustomWidgetPlugin plugin) {
-        String providerId = plugin.getId();
-        if (mPlugins.containsKey(providerId)) {
-            mPlugins.remove(providerId);
-        }
-
-        ComponentName cn = null;
-        for (Map.Entry entry: mWidgetsIdMap.entrySet()) {
-            if (entry.getValue().equals(providerId)) {
-                cn = (ComponentName) entry.getKey();
-            }
-        }
-
-        if (cn != null) {
-            mWidgetsIdMap.remove(cn);
-            for (int i = 0; i < mCustomWidgets.size(); i++) {
-                if (mCustomWidgets.get(i).getComponent().equals(cn)) {
-                    mCustomWidgets.remove(i);
-                    return;
-                }
-            }
-        }
+        ComponentName cn = getWidgetProviderComponent(plugin);
+        mPlugins.remove(cn);
+        mCustomWidgets.removeIf(w -> w.getComponent().equals(cn));
     }
 
     /**
@@ -145,7 +125,7 @@
      */
     public void onViewCreated(LauncherAppWidgetHostView view) {
         CustomAppWidgetProviderInfo info = (CustomAppWidgetProviderInfo) view.getAppWidgetInfo();
-        CustomWidgetPlugin plugin = mPlugins.get(info.providerId);
+        CustomWidgetPlugin plugin = mPlugins.get(info.provider);
         if (plugin == null) return;
         plugin.onViewCreated(view);
     }
@@ -159,32 +139,18 @@
     }
 
     /**
-     * Returns the widget id for a specific provider.
-     */
-    public String getWidgetIdForCustomProvider(@NonNull ComponentName provider) {
-        if (mWidgetsIdMap.containsKey(provider)) {
-            return mWidgetsIdMap.get(provider);
-        } else {
-            return "";
-        }
-    }
-
-    /**
      * Returns the widget provider in respect to given widget id.
      */
     @Nullable
-    public LauncherAppWidgetProviderInfo getWidgetProvider(ComponentName componentName) {
-        for (LauncherAppWidgetProviderInfo info : mCustomWidgets) {
-            if (info.provider.equals(componentName)) return info;
-        }
-        return null;
+    public LauncherAppWidgetProviderInfo getWidgetProvider(ComponentName cn) {
+        return mCustomWidgets.stream()
+                .filter(w -> w.getComponent().equals(cn)).findAny().orElse(null);
     }
 
-    private static CustomAppWidgetProviderInfo newInfo(String providerId, CustomWidgetPlugin plugin,
-            Parcel parcel, Context context) {
-        CustomAppWidgetProviderInfo info = new CustomAppWidgetProviderInfo(
-                parcel, false, providerId);
-        plugin.updateWidgetInfo(info, context);
+    private CustomAppWidgetProviderInfo newInfo(CustomWidgetPlugin plugin, Parcel parcel) {
+        CustomAppWidgetProviderInfo info = new CustomAppWidgetProviderInfo(parcel, false);
+        info.provider = getWidgetProviderComponent(plugin);
+        plugin.updateWidgetInfo(info, mContext);
         return info;
     }
 
@@ -195,4 +161,8 @@
         return CUSTOM_WIDGET_ID - mCustomWidgets.indexOf(getWidgetProvider(componentName));
     }
 
+    private ComponentName getWidgetProviderComponent(CustomWidgetPlugin plugin) {
+        return new ComponentName(
+                PLUGIN_PKG, CLS_CUSTOM_WIDGET_PREFIX + plugin.getClass().getName());
+    }
 }
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..54c9324 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java
@@ -15,7 +15,10 @@
  */
 package com.android.launcher3.widget.picker;
 
+import static com.android.launcher3.Flags.enableUnfoldedTwoPanePicker;
+
 import android.content.Context;
+import android.content.res.Configuration;
 import android.graphics.Outline;
 import android.os.Process;
 import android.util.AttributeSet;
@@ -23,12 +26,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 +52,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 +125,32 @@
     }
 
     @Override
+    protected void onConfigurationChanged(Configuration newConfig) {}
+
+    @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/src/com/android/launcher3/widget/util/WidgetSizes.java b/src/com/android/launcher3/widget/util/WidgetSizes.java
index 7049509..4688359 100644
--- a/src/com/android/launcher3/widget/util/WidgetSizes.java
+++ b/src/com/android/launcher3/widget/util/WidgetSizes.java
@@ -15,8 +15,11 @@
  */
 package com.android.launcher3.widget.util;
 
+import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
+
 import android.appwidget.AppWidgetHostView;
 import android.appwidget.AppWidgetManager;
+import android.appwidget.AppWidgetProviderInfo;
 import android.content.ComponentName;
 import android.content.Context;
 import android.graphics.Point;
@@ -91,20 +94,33 @@
      */
     public static void updateWidgetSizeRanges(AppWidgetHostView widgetView, Context context,
             int spanX, int spanY) {
-        AppWidgetManager widgetManager = AppWidgetManager.getInstance(context);
-        int widgetId = widgetView.getAppWidgetId();
-        if (widgetId <= 0) {
+        updateWidgetSizeRangesAsync(
+                widgetView.getAppWidgetId(), widgetView.getAppWidgetInfo(), context, spanX, spanY);
+    }
+
+    /**
+     * Updates a given {@code widgetId} with size, {@code spanX}, {@code spanY} asynchronously.
+     *
+     * <p>On Android S+, it also updates the given {@code widgetView} with a list of sizes derived
+     * from {@code spanX}, {@code spanY} in all supported device profiles.
+     */
+    public static void updateWidgetSizeRangesAsync(int widgetId,
+            AppWidgetProviderInfo info, Context context, int spanX, int spanY) {
+        if (widgetId <= 0 || info == null) {
             return;
         }
-        Bundle sizeOptions = getWidgetSizeOptions(context, widgetView.getAppWidgetInfo().provider,
-                spanX, spanY);
-        if (sizeOptions.<SizeF>getParcelableArrayList(
-                AppWidgetManager.OPTION_APPWIDGET_SIZES).equals(
-                widgetManager.getAppWidgetOptions(widgetId).<SizeF>getParcelableArrayList(
-                        AppWidgetManager.OPTION_APPWIDGET_SIZES))) {
-            return;
-        }
-        widgetManager.updateAppWidgetOptions(widgetId, sizeOptions);
+
+        UI_HELPER_EXECUTOR.execute(() -> {
+            AppWidgetManager widgetManager = AppWidgetManager.getInstance(context);
+            Bundle sizeOptions = getWidgetSizeOptions(context, info.provider, spanX, spanY);
+            if (sizeOptions.<SizeF>getParcelableArrayList(
+                    AppWidgetManager.OPTION_APPWIDGET_SIZES).equals(
+                    widgetManager.getAppWidgetOptions(widgetId).<SizeF>getParcelableArrayList(
+                            AppWidgetManager.OPTION_APPWIDGET_SIZES))) {
+                return;
+            }
+            widgetManager.updateAppWidgetOptions(widgetId, sizeOptions);
+        });
     }
 
     /**
diff --git a/src_plugins/com/android/systemui/plugins/CustomWidgetPlugin.java b/src_plugins/com/android/systemui/plugins/CustomWidgetPlugin.java
index af4f22c..6452ea2 100644
--- a/src_plugins/com/android/systemui/plugins/CustomWidgetPlugin.java
+++ b/src_plugins/com/android/systemui/plugins/CustomWidgetPlugin.java
@@ -39,13 +39,16 @@
 
     /**
      * Get the UUID for the custom widget.
+     *
+     * @deprecated Not used
      */
-    String getId();
+    @Deprecated
+    default String getId() {
+        return "";
+    }
 
     /**
      * Used to modify a widgets' info.
      */
-    default void updateWidgetInfo(AppWidgetProviderInfo info, Context context) {
-
-    }
+    default void updateWidgetInfo(AppWidgetProviderInfo info, Context context) { }
 }
diff --git a/src_shortcuts_overrides/com/android/launcher3/model/WidgetsModel.java b/src_shortcuts_overrides/com/android/launcher3/model/WidgetsModel.java
index 2f16065..4c5bfd8 100644
--- a/src_shortcuts_overrides/com/android/launcher3/model/WidgetsModel.java
+++ b/src_shortcuts_overrides/com/android/launcher3/model/WidgetsModel.java
@@ -70,6 +70,34 @@
     private final Map<PackageItemInfo, List<WidgetItem>> mWidgetsList = new HashMap<>();
 
     /**
+     * Returns a list of {@link WidgetsListBaseEntry} filtered using given widget item filter. All
+     * {@link WidgetItem}s in a single row are sorted (based on label and user), but the overall
+     * list of {@link WidgetsListBaseEntry}s is not sorted.
+     *
+     * @see com.android.launcher3.widget.picker.WidgetsListAdapter#setWidgets(List)
+     */
+    public synchronized ArrayList<WidgetsListBaseEntry> getFilteredWidgetsListForPicker(
+            Context context,
+            Predicate<WidgetItem> widgetItemFilter) {
+        ArrayList<WidgetsListBaseEntry> result = new ArrayList<>();
+        AlphabeticIndexCompat indexer = new AlphabeticIndexCompat(context);
+
+        for (Map.Entry<PackageItemInfo, List<WidgetItem>> entry : mWidgetsList.entrySet()) {
+            PackageItemInfo pkgItem = entry.getKey();
+            List<WidgetItem> widgetItems = entry.getValue()
+                    .stream()
+                    .filter(widgetItemFilter).toList();
+            if (!widgetItems.isEmpty()) {
+                String sectionName = (pkgItem.title == null) ? "" :
+                        indexer.computeSectionName(pkgItem.title);
+                result.add(WidgetsListHeaderEntry.create(pkgItem, sectionName, widgetItems));
+                result.add(new WidgetsListContentEntry(pkgItem, sectionName, widgetItems));
+            }
+        }
+        return result;
+    }
+
+    /**
      * Returns a list of {@link WidgetsListBaseEntry}. All {@link WidgetItem} in a single row
      * are sorted (based on label and user), but the overall list of
      * {@link WidgetsListBaseEntry}s is not sorted.
@@ -77,18 +105,8 @@
      * @see com.android.launcher3.widget.picker.WidgetsListAdapter#setWidgets(List)
      */
     public synchronized ArrayList<WidgetsListBaseEntry> getWidgetsListForPicker(Context context) {
-        ArrayList<WidgetsListBaseEntry> result = new ArrayList<>();
-        AlphabeticIndexCompat indexer = new AlphabeticIndexCompat(context);
-
-        for (Map.Entry<PackageItemInfo, List<WidgetItem>> entry : mWidgetsList.entrySet()) {
-            PackageItemInfo pkgItem = entry.getKey();
-            List<WidgetItem> widgetItems = entry.getValue();
-            String sectionName = (pkgItem.title == null) ? "" :
-                    indexer.computeSectionName(pkgItem.title);
-            result.add(WidgetsListHeaderEntry.create(pkgItem, sectionName, widgetItems));
-            result.add(new WidgetsListContentEntry(pkgItem, sectionName, widgetItems));
-        }
-        return result;
+        // return all items
+        return getFilteredWidgetsListForPicker(context, /*widgetItemFilter=*/ item -> true);
     }
 
     /** Returns a mapping of packages to their widgets without static shortcuts. */
diff --git a/src_ui_overrides/com/android/launcher3/uioverrides/ApiWrapper.java b/src_ui_overrides/com/android/launcher3/uioverrides/ApiWrapper.java
index fe5c1fd..b9f9ac5 100644
--- a/src_ui_overrides/com/android/launcher3/uioverrides/ApiWrapper.java
+++ b/src_ui_overrides/com/android/launcher3/uioverrides/ApiWrapper.java
@@ -19,9 +19,11 @@
 import android.app.ActivityOptions;
 import android.app.Person;
 import android.content.Context;
+import android.content.Intent;
 import android.content.pm.LauncherActivityInfo;
 import android.content.pm.ShortcutInfo;
 import android.graphics.drawable.ColorDrawable;
+import android.net.Uri;
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.util.ArrayMap;
@@ -29,6 +31,7 @@
 import com.android.launcher3.Utilities;
 import com.android.launcher3.util.UserIconInfo;
 
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
@@ -80,6 +83,30 @@
         return users;
     }
 
+    /**
+     * Returns the list of the system packages that are installed at user creation.
+     * An empty list denotes that all system packages are installed for that user at creation.
+     */
+    public static List<String> getPreInstalledSystemPackages(Context context, UserHandle user) {
+        return new ArrayList<>();
+    }
+
+    /**
+     * Returns an intent which can be used to start the App Market activity (Installer
+     * Activity).
+     */
+    public static Intent getAppMarketActivityIntent(Context context, String packageName,
+            UserHandle user) {
+        return new Intent(Intent.ACTION_VIEW)
+                .setData(new Uri.Builder()
+                        .scheme("market")
+                        .authority("details")
+                        .appendQueryParameter("id", packageName)
+                        .build())
+                .putExtra(Intent.EXTRA_REFERRER, new Uri.Builder().scheme("android-app")
+                        .authority(context.getPackageName()).build());
+    }
+
     private static class NoopDrawable extends ColorDrawable {
         @Override
         public int getIntrinsicHeight() {
diff --git a/tests/Android.bp b/tests/Android.bp
index 84c3951..dd0ba9e 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -66,14 +66,14 @@
 filegroup {
     name: "launcher-oop-tests-src",
     srcs: [
-      "src/com/android/launcher3/allapps/TaplOpenCloseAllApps.java",
-      "src/com/android/launcher3/allapps/TaplTestsAllAppsIconsWorking.java",
+      "src/com/android/launcher3/allapps/TaplOpenCloseAllAppsTest.java",
+      "src/com/android/launcher3/allapps/TaplAllAppsIconsWorkingTest.java",
       "src/com/android/launcher3/appiconmenu/TaplAppIconMenuTest.java",
       "src/com/android/launcher3/dragging/TaplDragTest.java",
-      "src/com/android/launcher3/dragging/TaplUninstallRemove.java",
+      "src/com/android/launcher3/dragging/TaplUninstallRemoveTest.java",
       "src/com/android/launcher3/ui/AbstractLauncherUiTest.java",
       "src/com/android/launcher3/ui/PortraitLandscapeRunner.java",
-      "src/com/android/launcher3/ui/TaplTestsLauncher3.java",
+      "src/com/android/launcher3/ui/TaplTestsLauncher3Test.java",
       "src/com/android/launcher3/ui/widget/TaplWidgetPickerTest.java",
       "src/com/android/launcher3/ui/workspace/TaplWorkspaceTest.java",
       "src/com/android/launcher3/util/LauncherLayoutBuilder.java",
@@ -174,3 +174,77 @@
     sdk_version: "current",
     min_sdk_version: min_launcher3_sdk_version,
  }
+
+filegroup {
+    name: "launcher-testing-helpers",
+    srcs: [
+      "src/**/*.java",
+      "src/**/*.kt",
+      "src/com/android/launcher3/ui/AbstractLauncherUiTest.java",
+      "tapl/com/android/launcher3/tapl/*.java",
+      "tapl/com/android/launcher3/tapl/*.kt",
+    ],
+    exclude_srcs: [
+        // Test classes
+        "src/**/*Test.java",
+        "src/**/*Test.kt",
+    ],
+}
+
+android_library {
+    name: "Launcher3Lib",
+    srcs: [
+        ":launcher-src",
+        ":launcher-src_shortcuts_overrides",
+        ":launcher-src_ui_overrides",
+    ],
+    static_libs: [
+        "Launcher3CommonDepsLib",
+    ],
+}
+
+android_robolectric_test {
+    enabled: true,
+    name: "Launcher3RoboTests",
+    srcs: [
+        "src/com/android/launcher3/util/*.java",
+        "src/com/android/launcher3/util/*.kt",
+
+        // Test util classes
+        ":launcher-testing-helpers",
+        ":launcher-testing-shared",
+    ],
+    exclude_srcs: [
+        "src/com/android/launcher3/util/CellContentDimensionsTest.kt", // Failing - b/316553889
+
+        // requires modification to work with inline mock maker
+        "src/com/android/launcher3/util/rule/StaticMockitoRule.java",
+
+        // requires kotlin mockito
+        "src/com/android/launcher3/util/LockedUserStateTest.kt",
+        "src/com/android/launcher3/util/DisplayControllerTest.kt",
+    ],
+    java_resource_dirs: ["config"],
+    static_libs: [
+        "flag-junit-base",
+        "com_android_launcher3_flags_lib",
+        "com_android_wm_shell_flags_lib",
+        "androidx.test.uiautomator_uiautomator",
+        "androidx.core_core-animation-testing",
+        "androidx.test.ext.junit",
+        "inline-mockito-robolectric-prebuilt",
+        "platform-parametric-runner-lib",
+        "testables",
+        "Launcher3TestResources",
+        "SystemUISharedLib",
+        "launcher-testing-shared",
+    ],
+    libs: [
+        "android.test.runner",
+        "android.test.base",
+        "android.test.mock",
+        "truth",
+    ],
+    instrumentation_for: "Launcher3",
+    upstream: true,
+}
diff --git a/tests/AndroidManifest-common.xml b/tests/AndroidManifest-common.xml
index bd9da0a..e5e3354 100644
--- a/tests/AndroidManifest-common.xml
+++ b/tests/AndroidManifest-common.xml
@@ -149,7 +149,8 @@
             android:name="com.android.launcher3.testcomponent.BaseTestingActivity"
             android:label="LauncherTestApp"
             android:exported="true"
-            android:taskAffinity="com.android.launcher3.testcomponent.Affinity1">
+            android:taskAffinity="com.android.launcher3.testcomponent.Affinity1"
+            android:theme="@style/Theme.TestActivities">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
                 <category android:name="android.intent.category.LAUNCHER"/>
@@ -343,6 +344,24 @@
                 <category android:name="android.intent.category.DEFAULT" />
             </intent-filter>
         </activity-alias>
+        <activity-alias android:name="AAAActivity"
+            android:label="AAA"
+            android:exported="true"
+            android:targetActivity="com.android.launcher3.testcomponent.BaseTestingActivity">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity-alias>
+        <activity-alias android:name="ZZZActivity"
+            android:label="ZZZ"
+            android:exported="true"
+            android:targetActivity="com.android.launcher3.testcomponent.BaseTestingActivity">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity-alias>
 
         <!-- [b/197780098] Disable eager initialization of Jetpack libraries. -->
         <provider
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
index 5cf96c8..2596b75 100644
--- a/tests/AndroidManifest.xml
+++ b/tests/AndroidManifest.xml
@@ -23,6 +23,14 @@
 
     <application android:debuggable="true">
         <uses-library android:name="android.test.runner" />
+
+        <receiver android:name="com.android.launcher3.compat.PromiseIconUiTest$UnarchiveBroadcastReceiver"
+                  android:enabled="true"
+                  android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.UNARCHIVE_PACKAGE"/>
+            </intent-filter>
+        </receiver>
     </application>
 
     <instrumentation
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/tabletLandscape.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/tabletLandscape.txt
index 1781673..92caf23 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/tabletLandscape.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/tabletLandscape.txt
@@ -55,7 +55,7 @@
 	bottomSheetCloseDuration: 500
 	bottomSheetWorkspaceScale: 0.97
 	bottomSheetDepth: 0.0
-	allAppsShiftRange: 1496.0px (748.0dp)
+	allAppsShiftRange: 1600.0px (800.0dp)
 	allAppsOpenDuration: 500
 	allAppsCloseDuration: 500
 	allAppsIconSizePx: 120.0px (60.0dp)
@@ -87,7 +87,7 @@
 	numShownHotseatIcons: 6
 	hotseatBorderSpace: 100.0px (50.0dp)
 	isQsbInline: false
-	hotseatQsbWidth: 1224.0px (612.0dp)
+	hotseatQsbWidth: 1214.0px (607.0dp)
 	isTaskbarPresent:false
 	isTaskbarPresentInApps:true
 	taskbarHeight: 0.0px (0.0dp)
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/tabletLandscape3Button.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/tabletLandscape3Button.txt
index bd9e267..3815fa9 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/tabletLandscape3Button.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/tabletLandscape3Button.txt
@@ -55,7 +55,7 @@
 	bottomSheetCloseDuration: 500
 	bottomSheetWorkspaceScale: 0.97
 	bottomSheetDepth: 0.0
-	allAppsShiftRange: 1496.0px (748.0dp)
+	allAppsShiftRange: 1600.0px (800.0dp)
 	allAppsOpenDuration: 500
 	allAppsCloseDuration: 500
 	allAppsIconSizePx: 120.0px (60.0dp)
@@ -87,7 +87,7 @@
 	numShownHotseatIcons: 6
 	hotseatBorderSpace: 100.0px (50.0dp)
 	isQsbInline: false
-	hotseatQsbWidth: 1224.0px (612.0dp)
+	hotseatQsbWidth: 1214.0px (607.0dp)
 	isTaskbarPresent:false
 	isTaskbarPresentInApps:true
 	taskbarHeight: 0.0px (0.0dp)
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/tabletPortrait.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/tabletPortrait.txt
index e983ef7..7e0f316 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/tabletPortrait.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/tabletPortrait.txt
@@ -55,7 +55,7 @@
 	bottomSheetCloseDuration: 500
 	bottomSheetWorkspaceScale: 0.97
 	bottomSheetDepth: 0.0
-	allAppsShiftRange: 2019.0px (1009.5dp)
+	allAppsShiftRange: 2560.0px (1280.0dp)
 	allAppsOpenDuration: 500
 	allAppsCloseDuration: 500
 	allAppsIconSizePx: 120.0px (60.0dp)
@@ -66,7 +66,7 @@
 	allAppsBorderSpacePxX: 16.0px (8.0dp)
 	allAppsBorderSpacePxY: 32.0px (16.0dp)
 	numShownAllAppsColumns: 6
-	allAppsPadding.top: 541.0px (270.5dp)
+	allAppsPadding.top: 104.0px (52.0dp)
 	allAppsPadding.left: 32.0px (16.0dp)
 	allAppsPadding.right: 32.0px (16.0dp)
 	allAppsLeftRightMargin: 152.0px (76.0dp)
@@ -87,7 +87,7 @@
 	numShownHotseatIcons: 6
 	hotseatBorderSpace: 116.0px (58.0dp)
 	isQsbInline: false
-	hotseatQsbWidth: 1300.0px (650.0dp)
+	hotseatQsbWidth: 1290.0px (645.0dp)
 	isTaskbarPresent:false
 	isTaskbarPresentInApps:true
 	taskbarHeight: 0.0px (0.0dp)
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/tabletPortrait3Button.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/tabletPortrait3Button.txt
index aa92838..58c3890 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/tabletPortrait3Button.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/tabletPortrait3Button.txt
@@ -55,7 +55,7 @@
 	bottomSheetCloseDuration: 500
 	bottomSheetWorkspaceScale: 0.97
 	bottomSheetDepth: 0.0
-	allAppsShiftRange: 2019.0px (1009.5dp)
+	allAppsShiftRange: 2560.0px (1280.0dp)
 	allAppsOpenDuration: 500
 	allAppsCloseDuration: 500
 	allAppsIconSizePx: 120.0px (60.0dp)
@@ -66,7 +66,7 @@
 	allAppsBorderSpacePxX: 16.0px (8.0dp)
 	allAppsBorderSpacePxY: 32.0px (16.0dp)
 	numShownAllAppsColumns: 6
-	allAppsPadding.top: 541.0px (270.5dp)
+	allAppsPadding.top: 104.0px (52.0dp)
 	allAppsPadding.left: 32.0px (16.0dp)
 	allAppsPadding.right: 32.0px (16.0dp)
 	allAppsLeftRightMargin: 152.0px (76.0dp)
@@ -87,7 +87,7 @@
 	numShownHotseatIcons: 6
 	hotseatBorderSpace: 116.0px (58.0dp)
 	isQsbInline: false
-	hotseatQsbWidth: 1300.0px (650.0dp)
+	hotseatQsbWidth: 1290.0px (645.0dp)
 	isTaskbarPresent:false
 	isTaskbarPresentInApps:true
 	taskbarHeight: 0.0px (0.0dp)
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelLandscape.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelLandscape.txt
index 43e4a60..1e363a2 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelLandscape.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelLandscape.txt
@@ -55,7 +55,7 @@
 	bottomSheetCloseDuration: 500
 	bottomSheetWorkspaceScale: 0.97
 	bottomSheetDepth: 1.0
-	allAppsShiftRange: 1730.0px (659.0476dp)
+	allAppsShiftRange: 1840.0px (700.9524dp)
 	allAppsOpenDuration: 500
 	allAppsCloseDuration: 500
 	allAppsIconSizePx: 141.0px (53.714287dp)
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelLandscape3Button.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelLandscape3Button.txt
index e7ea839..617b54b 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelLandscape3Button.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelLandscape3Button.txt
@@ -55,7 +55,7 @@
 	bottomSheetCloseDuration: 500
 	bottomSheetWorkspaceScale: 0.97
 	bottomSheetDepth: 1.0
-	allAppsShiftRange: 1730.0px (659.0476dp)
+	allAppsShiftRange: 1840.0px (700.9524dp)
 	allAppsOpenDuration: 500
 	allAppsCloseDuration: 500
 	allAppsIconSizePx: 141.0px (53.714287dp)
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelPortrait.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelPortrait.txt
index 043380c..483b5e7 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelPortrait.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelPortrait.txt
@@ -55,7 +55,7 @@
 	bottomSheetCloseDuration: 500
 	bottomSheetWorkspaceScale: 0.97
 	bottomSheetDepth: 1.0
-	allAppsShiftRange: 2075.0px (790.4762dp)
+	allAppsShiftRange: 2208.0px (841.1429dp)
 	allAppsOpenDuration: 500
 	allAppsCloseDuration: 500
 	allAppsIconSizePx: 141.0px (53.714287dp)
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelPortrait3Button.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelPortrait3Button.txt
index a1b3e95..8d0640c 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelPortrait3Button.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelPortrait3Button.txt
@@ -55,7 +55,7 @@
 	bottomSheetCloseDuration: 500
 	bottomSheetWorkspaceScale: 0.97
 	bottomSheetDepth: 1.0
-	allAppsShiftRange: 2075.0px (790.4762dp)
+	allAppsShiftRange: 2208.0px (841.1429dp)
 	allAppsOpenDuration: 500
 	allAppsCloseDuration: 500
 	allAppsIconSizePx: 141.0px (53.714287dp)
diff --git a/tests/config/robolectric.properties b/tests/config/robolectric.properties
new file mode 100644
index 0000000..fab7251
--- /dev/null
+++ b/tests/config/robolectric.properties
@@ -0,0 +1 @@
+sdk=NEWEST_SDK
diff --git a/tests/res/values/styles.xml b/tests/res/values/styles.xml
new file mode 100644
index 0000000..1e1a2cd
--- /dev/null
+++ b/tests/res/values/styles.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<resources>
+    <style name="Theme.TestActivities" parent="@android:style/Theme.DeviceDefault.DayNight">
+        <!-- Hardcoded ActionBar height to avoid changes while emulating -->
+        <!-- (56dp - default for handheld) -->
+        <item name="android:actionBarSize">168px</item>
+    </style>
+</resources>
\ No newline at end of file
diff --git a/tests/res/xml/invalid_responsive_spec_1.xml b/tests/res/xml/invalid_responsive_spec_1.xml
index d1bcf65..7845d1e 100644
--- a/tests/res/xml/invalid_responsive_spec_1.xml
+++ b/tests/res/xml/invalid_responsive_spec_1.xml
@@ -27,7 +27,7 @@
     <workspaceSpec
         launcher:maxAvailableSize="9999dp"
         launcher:dimensionType="width">
-        <cellSize launcher:ofRemainderSpace="0.25" />
+        <cellSize launcher:ofRemainderSpace="1" />
         <endPadding launcher:fixedSize="22dp" />
         <gutter launcher:fixedSize="16dp" />
         <startPadding launcher:fixedSize="22dp" />
diff --git a/tests/res/xml/invalid_responsive_spec_2.xml b/tests/res/xml/invalid_responsive_spec_2.xml
index 49e1f93..ae30bb9 100644
--- a/tests/res/xml/invalid_responsive_spec_2.xml
+++ b/tests/res/xml/invalid_responsive_spec_2.xml
@@ -28,7 +28,7 @@
         <workspaceSpec
             launcher:maxAvailableSize="9999dp"
             launcher:dimensionType="width">
-            <cellSize launcher:ofRemainderSpace="0.25" />
+            <cellSize launcher:ofRemainderSpace="1" />
             <endPadding launcher:fixedSize="22dp" />
             <gutter launcher:fixedSize="16dp" />
             <startPadding launcher:fixedSize="22dp" />
@@ -38,7 +38,7 @@
     <workspaceSpec
         launcher:maxAvailableSize="9999dp"
         launcher:dimensionType="width">
-        <cellSize launcher:ofRemainderSpace="0.25" />
+        <cellSize launcher:ofRemainderSpace="1" />
         <endPadding launcher:fixedSize="22dp" />
         <gutter launcher:fixedSize="16dp" />
         <startPadding launcher:fixedSize="22dp" />
diff --git a/tests/res/xml/valid_responsive_spec_unsorted.xml b/tests/res/xml/valid_responsive_spec_unsorted.xml
index 9a463d5..8676f48 100644
--- a/tests/res/xml/valid_responsive_spec_unsorted.xml
+++ b/tests/res/xml/valid_responsive_spec_unsorted.xml
@@ -22,7 +22,7 @@
             <startPadding launcher:fixedSize="0dp" />
             <endPadding launcher:fixedSize="34dp" />
             <gutter launcher:fixedSize="12dp" />
-            <cellSize launcher:ofRemainderSpace="0.25" />
+            <cellSize launcher:ofRemainderSpace="1" />
         </workspaceSpec>
 
         <!-- Height spec -->
@@ -32,7 +32,7 @@
             <startPadding launcher:fixedSize="0dp" />
             <endPadding launcher:fixedSize="24dp" />
             <gutter launcher:fixedSize="12dp" />
-            <cellSize launcher:ofRemainderSpace="0.25" />
+            <cellSize launcher:ofRemainderSpace="1" />
         </workspaceSpec>
 
         <!-- Width spec -->
@@ -42,7 +42,7 @@
             <startPadding launcher:fixedSize="16dp" />
             <endPadding launcher:fixedSize="64dp" />
             <gutter launcher:fixedSize="12dp" />
-            <cellSize launcher:ofRemainderSpace="0.25" />
+            <cellSize launcher:ofRemainderSpace="1" />
         </workspaceSpec>
         <workspaceSpec
             launcher:dimensionType="width"
@@ -50,7 +50,7 @@
             <startPadding launcher:fixedSize="36dp" />
             <endPadding launcher:fixedSize="80dp" />
             <gutter launcher:fixedSize="12dp" />
-            <cellSize launcher:ofRemainderSpace="0.25" />
+            <cellSize launcher:ofRemainderSpace="1" />
         </workspaceSpec>
         <workspaceSpec
             launcher:dimensionType="width"
@@ -58,7 +58,7 @@
             <startPadding launcher:fixedSize="0dp" />
             <endPadding launcher:fixedSize="36dp" />
             <gutter launcher:fixedSize="12dp" />
-            <cellSize launcher:ofRemainderSpace="0.25" />
+            <cellSize launcher:ofRemainderSpace="1" />
         </workspaceSpec>
     </specs>
 
@@ -71,7 +71,7 @@
             <startPadding launcher:fixedSize="2dp" />
             <endPadding launcher:fixedSize="2dp" />
             <gutter launcher:fixedSize="8dp" />
-            <cellSize launcher:ofRemainderSpace="0.25" />
+            <cellSize launcher:ofRemainderSpace="1" />
         </workspaceSpec>
 
         <!-- Width spec -->
@@ -81,7 +81,7 @@
             <startPadding launcher:fixedSize="1dp" />
             <endPadding launcher:fixedSize="1dp" />
             <gutter launcher:fixedSize="8dp" />
-            <cellSize launcher:ofRemainderSpace="0.25" />
+            <cellSize launcher:ofRemainderSpace="1" />
         </workspaceSpec>
     </specs>
 
@@ -122,7 +122,7 @@
             <startPadding launcher:fixedSize="22dp" />
             <endPadding launcher:fixedSize="22dp" />
             <gutter launcher:fixedSize="16dp" />
-            <cellSize launcher:ofRemainderSpace="0.25" />
+            <cellSize launcher:ofRemainderSpace="1" />
         </workspaceSpec>
     </specs>
 </workspaceSpecs>
diff --git a/tests/res/xml/valid_workspace_file.xml b/tests/res/xml/valid_workspace_file.xml
index 9c44502..dc9963a 100644
--- a/tests/res/xml/valid_workspace_file.xml
+++ b/tests/res/xml/valid_workspace_file.xml
@@ -54,7 +54,7 @@
             <startPadding launcher:fixedSize="22dp" />
             <endPadding launcher:fixedSize="22dp" />
             <gutter launcher:fixedSize="16dp" />
-            <cellSize launcher:ofRemainderSpace="0.25" />
+            <cellSize launcher:ofRemainderSpace="1" />
         </workspaceSpec>
     </specs>
 
@@ -67,7 +67,7 @@
             <startPadding launcher:fixedSize="0dp" />
             <endPadding launcher:fixedSize="24dp" />
             <gutter launcher:fixedSize="12dp" />
-            <cellSize launcher:ofRemainderSpace="0.25" />
+            <cellSize launcher:ofRemainderSpace="1" />
         </workspaceSpec>
         <workspaceSpec
             launcher:dimensionType="height"
@@ -75,7 +75,7 @@
             <startPadding launcher:fixedSize="0dp" />
             <endPadding launcher:fixedSize="34dp" />
             <gutter launcher:fixedSize="12dp" />
-            <cellSize launcher:ofRemainderSpace="0.25" />
+            <cellSize launcher:ofRemainderSpace="1" />
         </workspaceSpec>
 
         <!-- Width spec -->
@@ -85,7 +85,7 @@
             <startPadding launcher:fixedSize="0dp" />
             <endPadding launcher:fixedSize="36dp" />
             <gutter launcher:fixedSize="12dp" />
-            <cellSize launcher:ofRemainderSpace="0.25" />
+            <cellSize launcher:ofRemainderSpace="1" />
         </workspaceSpec>
         <workspaceSpec
             launcher:dimensionType="width"
@@ -93,7 +93,7 @@
             <startPadding launcher:fixedSize="16dp" />
             <endPadding launcher:fixedSize="64dp" />
             <gutter launcher:fixedSize="12dp" />
-            <cellSize launcher:ofRemainderSpace="0.25" />
+            <cellSize launcher:ofRemainderSpace="1" />
         </workspaceSpec>
         <workspaceSpec
             launcher:dimensionType="width"
@@ -101,7 +101,7 @@
             <startPadding launcher:fixedSize="36dp" />
             <endPadding launcher:fixedSize="80dp" />
             <gutter launcher:fixedSize="12dp" />
-            <cellSize launcher:ofRemainderSpace="0.25" />
+            <cellSize launcher:ofRemainderSpace="1" />
         </workspaceSpec>
     </specs>
 </workspaceSpecs>
diff --git a/tests/res/xml/valid_workspace_unsorted_file.xml b/tests/res/xml/valid_workspace_unsorted_file.xml
index 6bf7c78..f783e9f 100644
--- a/tests/res/xml/valid_workspace_unsorted_file.xml
+++ b/tests/res/xml/valid_workspace_unsorted_file.xml
@@ -52,7 +52,7 @@
             <startPadding launcher:fixedSize="22dp" />
             <endPadding launcher:fixedSize="22dp" />
             <gutter launcher:fixedSize="16dp" />
-            <cellSize launcher:ofRemainderSpace="0.25" />
+            <cellSize launcher:ofRemainderSpace="1" />
         </workspaceSpec>
     </specs>
 </workspaceSpecs>
diff --git a/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java b/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java
index fcb5158..3e188e6 100644
--- a/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java
+++ b/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java
@@ -107,7 +107,7 @@
     public static final String REQUEST_WIDGETS_SCROLL_Y = "widgets-scroll-y";
     public static final String REQUEST_TARGET_INSETS = "target-insets";
     public static final String REQUEST_WINDOW_INSETS = "window-insets";
-    public static final String REQUEST_IME_INSETS = "ime-insets";
+    public static final String REQUEST_SYSTEM_GESTURE_REGION = "gesture-region";
     public static final String REQUEST_PID = "pid";
     public static final String REQUEST_FORCE_GC = "gc";
     public static final String REQUEST_RECENT_TASKS_LIST = "recent-tasks-list";
@@ -147,6 +147,8 @@
     public static final String REQUEST_GET_GRID_TASK_SIZE_RECT_FOR_TABLET =
             "get-grid-task-size-rect-for-tablet";
     public static final String REQUEST_GET_OVERVIEW_PAGE_SPACING = "get-overview-page-spacing";
+    public static final String REQUEST_GET_OVERVIEW_CURRENT_PAGE_INDEX =
+            "get-overview-current-page-index";
     public static final String REQUEST_ENABLE_ROTATION = "enable_rotation";
     public static final String REQUEST_ENABLE_SUGGESTION = "enable-suggestion";
     public static final String REQUEST_MODEL_QUEUE_CLEARED = "model-queue-cleared";
@@ -172,6 +174,9 @@
 
     public static final String REQUEST_FLAG_ENABLE_GRID_ONLY_OVERVIEW = "enable-grid-only-overview";
 
+    public static final String REQUEST_UNSTASH_BUBBLE_BAR_IF_STASHED =
+            "unstash-bubble-bar-if-stashed";
+
     /** Logs {@link Log#d(String, String)} if {@link #sDebugTracing} is true. */
     public static void testLogD(String tag, String message) {
         if (!sDebugTracing) {
diff --git a/tests/src/com/android/launcher3/AbstractDeviceProfileTest.kt b/tests/src/com/android/launcher3/AbstractDeviceProfileTest.kt
index e46726d..d44ccf5 100644
--- a/tests/src/com/android/launcher3/AbstractDeviceProfileTest.kt
+++ b/tests/src/com/android/launcher3/AbstractDeviceProfileTest.kt
@@ -17,11 +17,13 @@
 
 import android.content.Context
 import android.content.res.Configuration
+import android.content.res.Resources
 import android.graphics.Point
 import android.graphics.Rect
 import android.util.DisplayMetrics
 import android.view.Surface
 import androidx.test.core.app.ApplicationProvider
+import androidx.test.platform.app.InstrumentationRegistry
 import com.android.launcher3.testing.shared.ResourceUtils
 import com.android.launcher3.util.DisplayController
 import com.android.launcher3.util.MainThreadInitializedObject.SandboxContext
@@ -30,6 +32,8 @@
 import com.android.launcher3.util.rule.TestStabilityRule
 import com.android.launcher3.util.window.CachedDisplayInfo
 import com.android.launcher3.util.window.WindowManagerProxy
+import com.android.wm.shell.Flags
+import com.google.common.truth.Truth
 import java.io.BufferedReader
 import java.io.File
 import java.io.PrintWriter
@@ -49,11 +53,18 @@
  * For an implementation that mocks InvariantDeviceProfile, use [FakeInvariantDeviceProfileTest]
  */
 abstract class AbstractDeviceProfileTest {
+    protected val testContext: Context = InstrumentationRegistry.getInstrumentation().context
     protected lateinit var context: SandboxContext
     protected open val runningContext: Context = ApplicationProvider.getApplicationContext()
     private val displayController: DisplayController = mock()
     private val windowManagerProxy: WindowManagerProxy = mock()
     private val launcherPrefs: LauncherPrefs = mock()
+    private val allowLeftRightSplitInPortrait: Boolean = initAllowLeftRightSplitInPortrait()
+    fun initAllowLeftRightSplitInPortrait() : Boolean {
+        val res = Resources.getSystem();
+        val resId = res.getIdentifier("config_leftRightSplitInPortrait", "bool", "android")
+        return Flags.enableLeftRightSplitInPortrait() && resId > 0 && res.getBoolean(resId)
+    }
 
     @Rule @JvmField val testStabilityRule = TestStabilityRule()
 
@@ -306,6 +317,25 @@
         whenever(info.isTransientTaskbar).thenReturn(isGestureMode)
     }
 
+    /** Asserts that the given device profile matches a previously dumped device profile state. */
+    protected fun assertDump(dp: DeviceProfile, folderName: String, filename: String) {
+        val dump = dump(context!!, dp, "${folderName}_$filename.txt")
+        var expected = readDumpFromAssets(testContext, "$folderName/$filename.txt")
+
+        // TODO(b/315230497): We don't currently have device-specific device profile dumps, so just
+        //  update the result before we do the comparison
+        if (allowLeftRightSplitInPortrait) {
+            val isLeftRightSplitInPortrait = when {
+                allowLeftRightSplitInPortrait && dp.isTablet -> !dp.isLandscape
+                else -> dp.isLandscape
+            }
+            expected = expected.replace(Regex("isLeftRightSplit:\\w+"),
+                    "isLeftRightSplit:$isLeftRightSplitInPortrait")
+        }
+
+        Truth.assertThat(dump).isEqualTo(expected)
+    }
+
     /** Create a new dump of DeviceProfile, saves to a file in the device and returns it */
     protected fun dump(context: Context, dp: DeviceProfile, fileName: String): String {
         val stringWriter = StringWriter()
diff --git a/tests/src/com/android/launcher3/allapps/AlphabeticalAppsListTest.java b/tests/src/com/android/launcher3/allapps/AlphabeticalAppsListTest.java
index 259f519..3068785 100644
--- a/tests/src/com/android/launcher3/allapps/AlphabeticalAppsListTest.java
+++ b/tests/src/com/android/launcher3/allapps/AlphabeticalAppsListTest.java
@@ -19,6 +19,9 @@
 import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
 
 import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_PRIVATE_SPACE_HEADER;
+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 static com.android.launcher3.allapps.UserProfileManager.STATE_DISABLED;
 import static com.android.launcher3.allapps.UserProfileManager.STATE_ENABLED;
 import static com.android.launcher3.allapps.UserProfileManager.STATE_TRANSITION;
@@ -61,6 +64,8 @@
     private static final int PRIVATE_SPACE_HEADER_ITEM_COUNT = 1;
     private static final int MAIN_USER_APP_COUNT = 2;
     private static final int PRIVATE_USER_APP_COUNT = 1;
+    private static final int NUM_APP_COLS = 4;
+    private static final int NUM_APP_ROWS = 3;
 
     private AlphabeticalAppsList<?> mAlphabeticalAppsList;
     @Mock
@@ -81,6 +86,7 @@
                 info != null && info.user.equals(PRIVATE_HANDLE));
         mAlphabeticalAppsList = new AlphabeticalAppsList<>(mContext, mAllAppsStore,
                 null, mPrivateProfileManager);
+        mAlphabeticalAppsList.setNumAppsPerRowAllApps(NUM_APP_COLS);
     }
 
     @Test
@@ -182,6 +188,94 @@
                 .toList().size());
     }
 
+    @Test
+    public void getRoundRegions_whenIndexIsMiddleOfLastRow_roundNothing() {
+        int index = 3;
+
+        int roundRegions = mAlphabeticalAppsList.getRoundRegions(index,
+                NUM_APP_COLS * NUM_APP_ROWS);
+
+        assertEquals(ROUND_NOTHING, roundRegions);
+    }
+
+    @Test
+    public void getRoundRegions_whenIndexIsInEndOfLastRow_roundBottomRight() {
+        int index = 11;
+
+        int roundRegions = mAlphabeticalAppsList.getRoundRegions(index,
+                NUM_APP_COLS * NUM_APP_ROWS);
+
+        assertEquals(ROUND_BOTTOM_RIGHT, roundRegions);
+    }
+
+    @Test
+    public void getRoundRegions_whenIndexIsInBeginningOfLastRow_roundBottomLeft() {
+        int index = 8;
+
+        int roundRegions = mAlphabeticalAppsList.getRoundRegions(index,
+                NUM_APP_COLS * NUM_APP_ROWS);
+
+        assertEquals(ROUND_BOTTOM_LEFT, roundRegions);
+    }
+
+    @Test
+    public void getRoundRegions_whenIndexIsInMiddleOfLastRow_roundNothing() {
+        int index = 9;
+
+        int roundRegions = mAlphabeticalAppsList.getRoundRegions(index,
+                NUM_APP_COLS * NUM_APP_ROWS);
+
+        assertEquals(ROUND_NOTHING, roundRegions);
+    }
+
+    @Test
+    public void getRoundRegions_whenIndexIsInMiddleRow_roundNothing() {
+        int index = 5;
+
+        int roundRegions = mAlphabeticalAppsList.getRoundRegions(index,
+                NUM_APP_COLS * NUM_APP_ROWS);
+
+        assertEquals(ROUND_NOTHING, roundRegions);
+    }
+
+    @Test
+    public void getRoundRegions_whenIndexIsInBeginningOfTopRow_roundNothing() {
+        int index = 0;
+
+        int roundRegions = mAlphabeticalAppsList.getRoundRegions(index,
+                NUM_APP_COLS * NUM_APP_ROWS);
+
+        assertEquals(ROUND_NOTHING, roundRegions);
+    }
+
+    @Test
+    public void getRoundRegions_whenIndexIsInLastOfTopRow_roundNothing() {
+        int index = 3;
+
+        int roundRegions = mAlphabeticalAppsList.getRoundRegions(index,
+                NUM_APP_COLS * NUM_APP_ROWS);
+
+        assertEquals(ROUND_NOTHING, roundRegions);
+    }
+
+    @Test
+    public void getRoundRegions_whenIndexIsInMiddleOfLastRowLastItem_roundBottomRight() {
+        int index = 9;
+
+        int roundRegions = mAlphabeticalAppsList.getRoundRegions(index, index+1);
+
+        assertEquals(ROUND_BOTTOM_RIGHT, roundRegions);
+    }
+
+    @Test
+    public void getRoundRegions_whenIndexIsInBeginningOfLastRowLastItem_roundBottomRight() {
+        int index = 8;
+
+        int roundRegions = mAlphabeticalAppsList.getRoundRegions(index, index+1);
+
+        assertEquals(ROUND_BOTTOM_RIGHT | ROUND_BOTTOM_LEFT, roundRegions);
+    }
+
     private int addPrivateSpaceHeader(List<BaseAllAppsAdapter.AdapterItem> adapterItemList) {
         adapterItemList.add(new BaseAllAppsAdapter.AdapterItem(VIEW_TYPE_PRIVATE_SPACE_HEADER));
         return adapterItemList.size();
diff --git a/tests/src/com/android/launcher3/allapps/PrivateProfileManagerTest.java b/tests/src/com/android/launcher3/allapps/PrivateProfileManagerTest.java
index 79d00c9..24f9acd 100644
--- a/tests/src/com/android/launcher3/allapps/PrivateProfileManagerTest.java
+++ b/tests/src/com/android/launcher3/allapps/PrivateProfileManagerTest.java
@@ -22,6 +22,7 @@
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Mockito.doNothing;
@@ -81,6 +82,8 @@
     @Mock
     private PackageManager mPackageManager;
 
+    private boolean mRunnableCalled = false;
+
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
@@ -110,7 +113,7 @@
     public void unlockPrivateProfile_requestsQuietModeAsFalse() throws Exception {
         when(mAllAppsStore.hasModelFlag(FLAG_PRIVATE_PROFILE_QUIET_MODE_ENABLED)).thenReturn(true);
 
-        mPrivateProfileManager.unlockPrivateProfile();
+        mPrivateProfileManager.unlockPrivateProfile(() -> {});
 
         awaitTasksCompleted();
         Mockito.verify(mUserManager).requestQuietModeEnabled(false, PRIVATE_HANDLE);
@@ -133,6 +136,23 @@
     }
 
     @Test
+    public void transitioningToUnlocked_resetCallsPendingRunnable() throws Exception {
+        PrivateProfileManager privateProfileManager = spy(mPrivateProfileManager);
+        doNothing().when(privateProfileManager).resetPrivateSpaceDecorator(anyInt());
+        when(mAllAppsStore.hasModelFlag(FLAG_PRIVATE_PROFILE_QUIET_MODE_ENABLED))
+                .thenReturn(false);
+        when(privateProfileManager.getCurrentState()).thenReturn(STATE_DISABLED);
+        mRunnableCalled = false;
+
+        privateProfileManager.unlockPrivateProfile(this::testRunnable);
+        privateProfileManager.reset();
+
+        awaitTasksCompleted();
+        Mockito.verify(privateProfileManager).applyUnlockRunnable();
+        assertTrue(mRunnableCalled);
+    }
+
+    @Test
     public void openPrivateSpaceSettings_triggersSecurityAndPrivacyIntent() {
         Intent expectedIntent = new Intent(SAFETY_CENTER_INTENT);
         expectedIntent.putExtra(PS_SETTINGS_FRAGMENT_KEY, PS_SETTINGS_FRAGMENT_VALUE);
@@ -150,4 +170,8 @@
     private static void awaitTasksCompleted() throws Exception {
         UI_HELPER_EXECUTOR.submit(() -> null).get();
     }
+
+    private void testRunnable() {
+        mRunnableCalled = true;
+    }
 }
diff --git a/tests/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewControllerTest.java b/tests/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewControllerTest.java
index bc09cdd..92fff49 100644
--- a/tests/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewControllerTest.java
+++ b/tests/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewControllerTest.java
@@ -64,13 +64,16 @@
     private RelativeLayout mPsHeaderLayout;
     @Mock
     private PrivateProfileManager mPrivateProfileManager;
+    @Mock
+    private ActivityAllAppsContainerView mAllApps;
 
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
         mContext = new ActivityContextWrapper(getApplicationContext());
         mLayoutInflater = LayoutInflater.from(getApplicationContext());
-        mPsHeaderViewController = new PrivateSpaceHeaderViewController(mPrivateProfileManager);
+        mPsHeaderViewController = new PrivateSpaceHeaderViewController(mAllApps,
+                mPrivateProfileManager);
         mPsHeaderLayout = (RelativeLayout) mLayoutInflater.inflate(R.layout.private_space_header,
                 null);
     }
diff --git a/tests/src/com/android/launcher3/allapps/TaplTestsAllAppsIconsWorking.java b/tests/src/com/android/launcher3/allapps/TaplAllAppsIconsWorkingTest.java
similarity index 96%
rename from tests/src/com/android/launcher3/allapps/TaplTestsAllAppsIconsWorking.java
rename to tests/src/com/android/launcher3/allapps/TaplAllAppsIconsWorkingTest.java
index 9f6bbdf..27a2c75 100644
--- a/tests/src/com/android/launcher3/allapps/TaplTestsAllAppsIconsWorking.java
+++ b/tests/src/com/android/launcher3/allapps/TaplAllAppsIconsWorkingTest.java
@@ -33,7 +33,7 @@
  * The test runs in Out of process (Oop) and in process.
  * Makes sure the basic behaviors of Icons on AllApps are working.
  */
-public class TaplTestsAllAppsIconsWorking extends AbstractLauncherUiTest {
+public class TaplAllAppsIconsWorkingTest extends AbstractLauncherUiTest {
 
     @Before
     public void setUp() throws Exception {
diff --git a/tests/src/com/android/launcher3/allapps/TaplOpenCloseAllApps.java b/tests/src/com/android/launcher3/allapps/TaplOpenCloseAllAppsTest.java
similarity index 94%
rename from tests/src/com/android/launcher3/allapps/TaplOpenCloseAllApps.java
rename to tests/src/com/android/launcher3/allapps/TaplOpenCloseAllAppsTest.java
index 4a42887..da0beb1 100644
--- a/tests/src/com/android/launcher3/allapps/TaplOpenCloseAllApps.java
+++ b/tests/src/com/android/launcher3/allapps/TaplOpenCloseAllAppsTest.java
@@ -43,7 +43,7 @@
  * Test that we can open and close the all apps in multiple situations.
  * The test runs in Out of process (Oop) and in process.
  */
-public class TaplOpenCloseAllApps extends AbstractLauncherUiTest {
+public class TaplOpenCloseAllAppsTest extends AbstractLauncherUiTest {
 
     public static final String READ_DEVICE_CONFIG_PERMISSION =
             "android.permission.READ_DEVICE_CONFIG";
@@ -130,6 +130,7 @@
      */
     @Test
     @PortraitLandscape
+    @PlatinumTest(focusArea = "launcher")
     public void testAllAppsFromHome() {
         // Test opening all apps
         assertNotNull("switchToAllApps() returned null",
@@ -181,10 +182,10 @@
             executeOnLauncher(launcher -> assertTrue("flingBackward() didn't scroll App Apps",
                     flingBackwardY < flingForwardY));
 
-            // Test scrolling down to YouTube.
-            assertNotNull("All apps: can't find YouTube", allApps.getAppIcon("YouTube"));
-            // Test scrolling up to Camera.
-            assertNotNull("All apps: can't find Camera", allApps.getAppIcon("Camera"));
+            // Test scrolling down to the end of the app list.
+            assertNotNull("All apps: can't find YouTube", allApps.getAppIcon("ZZZ"));
+            // Test scrolling up to the beginning oof the app list.
+            assertNotNull("All apps: can't find Camera", allApps.getAppIcon("AAA"));
             // Test failing to find a non-existing app.
             final AllApps allAppsFinal = allApps;
             expectFail("All apps: could find a non-existing app",
@@ -208,13 +209,13 @@
         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();
-        mLauncher.getWorkspace();
+        mLauncher.getLaunchedAppState().pressBackToWorkspace();
         waitForState("Launcher internal state didn't switch to Home", () -> LauncherState.NORMAL);
     }
 
diff --git a/tests/src/com/android/launcher3/celllayout/FavoriteItemsTransaction.java b/tests/src/com/android/launcher3/celllayout/FavoriteItemsTransaction.java
index fb364ad..dbb2715 100644
--- a/tests/src/com/android/launcher3/celllayout/FavoriteItemsTransaction.java
+++ b/tests/src/com/android/launcher3/celllayout/FavoriteItemsTransaction.java
@@ -64,7 +64,7 @@
         runOnExecutorSync(MODEL_EXECUTOR, () -> {
             ModelDbController controller = model.getModelDbController();
             // Migrate any previous data so that the DB state is correct
-            controller.tryMigrateDB();
+            controller.tryMigrateDB(null /* restoreEventLogger */);
 
             // Create DB again to load fresh data
             controller.createEmptyDB();
diff --git a/tests/src/com/android/launcher3/compat/TaplPromiseIconUiTest.java b/tests/src/com/android/launcher3/compat/TaplPromiseIconUiTest.java
index 8200c94..bfa0d34 100644
--- a/tests/src/com/android/launcher3/compat/TaplPromiseIconUiTest.java
+++ b/tests/src/com/android/launcher3/compat/TaplPromiseIconUiTest.java
@@ -15,9 +15,19 @@
  */
 package com.android.launcher3.compat;
 
+import static com.android.launcher3.Flags.FLAG_ENABLE_SUPPORT_FOR_ARCHIVING;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
 import android.content.pm.PackageInstaller.SessionParams;
 import android.content.pm.PackageManager;
 import android.graphics.Bitmap;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.text.TextUtils;
 
 import androidx.test.filters.LargeTest;
@@ -27,12 +37,15 @@
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.ui.AbstractLauncherUiTest;
 import com.android.launcher3.util.LauncherBindableItemsContainer.ItemOperator;
+import com.android.launcher3.util.TestUtil;
 import com.android.launcher3.util.rule.ViewCaptureRule;
 
 import org.junit.After;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.io.IOException;
 import java.util.UUID;
 
 
@@ -43,6 +56,14 @@
 @RunWith(AndroidJUnit4.class)
 public class TaplPromiseIconUiTest extends AbstractLauncherUiTest {
 
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule =
+            DeviceFlagsValueProvider.createCheckFlagsRule();
+
+    public static final String PACKAGE_NAME = "test.promise.app";
+    public static final String DUMMY_PACKAGE = "com.example.android.aardwolf";
+    public static final String DUMMY_LABEL = "Aardwolf";
+
     private int mSessionId = -1;
 
     @Override
@@ -55,18 +76,19 @@
     }
 
     @After
-    public void tearDown() {
+    public void tearDown() throws IOException {
         if (mSessionId > -1) {
             mTargetContext.getPackageManager().getPackageInstaller().abandonSession(mSessionId);
         }
+        TestUtil.uninstallDummyApp();
     }
 
     /**
      * Create a session and return the id.
      */
-    private int createSession(String label, Bitmap icon) throws Throwable {
+    private int createSession(String packageName, String label, Bitmap icon) throws Throwable {
         SessionParams params = new SessionParams(SessionParams.MODE_FULL_INSTALL);
-        params.setAppPackageName("test.promise.app");
+        params.setAppPackageName(packageName);
         params.setAppLabel(label);
         params.setAppIcon(icon);
         params.setInstallReason(PackageManager.INSTALL_REASON_USER);
@@ -80,7 +102,8 @@
                 info != null && TextUtils.equals(info.title, appLabel);
 
         // Create and add test session
-        mSessionId = createSession(appLabel, Bitmap.createBitmap(100, 100, Bitmap.Config.ALPHA_8));
+        mSessionId = createSession(PACKAGE_NAME, appLabel,
+                Bitmap.createBitmap(100, 100, Bitmap.Config.ALPHA_8));
 
         // Verify promise icon is added
         waitForLauncherCondition("Test Promise App not found on workspace", launcher ->
@@ -103,7 +126,7 @@
                 info != null && TextUtils.equals(info.title, appLabel);
 
         // Create and add test session without icon or label
-        mSessionId = createSession(null, null);
+        mSessionId = createSession(PACKAGE_NAME, null, null);
 
         // Sleep for duration of animation if a view was to be added + some buffer time.
         Thread.sleep(Launcher.NEW_APPS_PAGE_MOVE_DELAY + Launcher.NEW_APPS_ANIMATION_DELAY + 500);
@@ -112,4 +135,40 @@
         waitForLauncherCondition("Test Promise App not found on workspace", launcher ->
                 launcher.getWorkspace().getFirstMatch(findPromiseApp) == null);
     }
+
+    @Test
+    @RequiresFlagsEnabled(FLAG_ENABLE_SUPPORT_FOR_ARCHIVING)
+    public void testPromiseIcon_addedArchivedApp() throws Throwable {
+        installDummyAppAndWaitForUIUpdate();
+        assertThat(mDevice.executeShellCommand(String.format("pm archive %s", DUMMY_PACKAGE)))
+                .isEqualTo("Success\n");
+
+        final ItemOperator findPromiseApp = (info, view) ->
+                info != null && TextUtils.equals(info.title, DUMMY_LABEL);
+
+        // Create and add test session
+        mSessionId = createSession(DUMMY_PACKAGE, /* label= */ "",
+                Bitmap.createBitmap(100, 100, Bitmap.Config.ALPHA_8));
+
+        // Verify promise icon is added
+        waitForLauncherCondition("Test Promise App not found on workspace", launcher ->
+                launcher.getWorkspace().getFirstMatch(findPromiseApp) != null);
+
+        // Remove session
+        mTargetContext.getPackageManager().getPackageInstaller().abandonSession(mSessionId);
+        mSessionId = -1;
+    }
+
+    // Dummy receiver to fulfill archiving platform requirements, unused in reality.
+    public static class UnarchiveBroadcastReceiver extends BroadcastReceiver {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+        }
+    }
+
+    private void installDummyAppAndWaitForUIUpdate() throws IOException {
+        TestUtil.installDummyApp();
+        mLauncher.waitForModelQueueCleared();
+        mLauncher.waitForLauncherInitialized();
+    }
 }
diff --git a/tests/src/com/android/launcher3/dragging/TaplDragTest.java b/tests/src/com/android/launcher3/dragging/TaplDragTest.java
index e040367..94a96aa 100644
--- a/tests/src/com/android/launcher3/dragging/TaplDragTest.java
+++ b/tests/src/com/android/launcher3/dragging/TaplDragTest.java
@@ -20,6 +20,8 @@
 import static com.android.launcher3.util.TestConstants.AppNames.MAPS_APP_NAME;
 import static com.android.launcher3.util.TestConstants.AppNames.STORE_APP_NAME;
 import static com.android.launcher3.ui.AbstractLauncherUiTest.initialize;
+import static com.android.launcher3.util.rule.TestStabilityRule.LOCAL;
+import static com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_POSTSUBMIT;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
@@ -40,9 +42,9 @@
 import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape;
 import com.android.launcher3.util.TestUtil;
 import com.android.launcher3.util.rule.ScreenRecordRule.ScreenRecord;
+import com.android.launcher3.util.rule.TestStabilityRule;
 
 import org.junit.Before;
-import org.junit.Ignore;
 import org.junit.Test;
 
 /**
@@ -71,7 +73,8 @@
     @Test
     @PortraitLandscape
     @ScreenRecord
-    @Ignore // b/233075289
+    // Staging; will be promoted to presubmit if stable
+    @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT)
     @PlatinumTest(focusArea = "launcher")
     public void testDragToFolder() {
         // TODO: add the use case to drag an icon to an existing folder. Currently it either fails
diff --git a/tests/src/com/android/launcher3/dragging/TaplUninstallRemove.java b/tests/src/com/android/launcher3/dragging/TaplUninstallRemoveTest.java
similarity index 94%
rename from tests/src/com/android/launcher3/dragging/TaplUninstallRemove.java
rename to tests/src/com/android/launcher3/dragging/TaplUninstallRemoveTest.java
index ed34307..0b9de0f 100644
--- a/tests/src/com/android/launcher3/dragging/TaplUninstallRemove.java
+++ b/tests/src/com/android/launcher3/dragging/TaplUninstallRemoveTest.java
@@ -51,7 +51,7 @@
  * Test runs in Out of process (Oop) and In process (Ipc)
  * Test the behaviour of uninstalling and removing apps both from AllApps, Workspace and Hotseat.
  */
-public class TaplUninstallRemove extends AbstractLauncherUiTest {
+public class TaplUninstallRemoveTest extends AbstractLauncherUiTest {
 
     @Before
     public void setUp() throws Exception {
@@ -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/folder/PreviewItemManagerTest.kt b/tests/src/com/android/launcher3/folder/PreviewItemManagerTest.kt
new file mode 100644
index 0000000..4ec5b0e
--- /dev/null
+++ b/tests/src/com/android/launcher3/folder/PreviewItemManagerTest.kt
@@ -0,0 +1,217 @@
+/*
+ * 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.folder
+
+import android.R
+import android.content.Context
+import android.os.Process
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import com.android.launcher3.LauncherPrefs
+import com.android.launcher3.LauncherPrefs.Companion.get
+import com.android.launcher3.icons.BaseIconFactory
+import com.android.launcher3.icons.FastBitmapDrawable
+import com.android.launcher3.icons.UserBadgeDrawable
+import com.android.launcher3.model.data.WorkspaceItemInfo
+import com.android.launcher3.util.ActivityContextWrapper
+import com.android.launcher3.util.FlagOp
+import com.android.launcher3.util.LauncherLayoutBuilder
+import com.android.launcher3.util.LauncherModelHelper
+import com.android.launcher3.util.UserIconInfo
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** Tests for [PreviewItemManager] */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class PreviewItemManagerTest {
+
+    private lateinit var previewItemManager: PreviewItemManager
+    private lateinit var context: Context
+    private lateinit var folderItems: ArrayList<WorkspaceItemInfo>
+    private lateinit var modelHelper: LauncherModelHelper
+    private lateinit var folderIcon: FolderIcon
+
+    @Before
+    fun setup() {
+        getInstrumentation().runOnMainSync {
+            folderIcon =
+                FolderIcon(ActivityContextWrapper(ApplicationProvider.getApplicationContext()))
+        }
+        context = getInstrumentation().targetContext
+        previewItemManager = PreviewItemManager(folderIcon)
+        modelHelper = LauncherModelHelper()
+        modelHelper
+            .setupDefaultLayoutProvider(
+                LauncherLayoutBuilder()
+                    .atWorkspace(0, 0, 1)
+                    .putFolder(R.string.copy)
+                    .addApp(LauncherModelHelper.TEST_PACKAGE, LauncherModelHelper.TEST_ACTIVITY)
+                    .addApp(LauncherModelHelper.TEST_PACKAGE, LauncherModelHelper.TEST_ACTIVITY2)
+                    .addApp(LauncherModelHelper.TEST_PACKAGE, LauncherModelHelper.TEST_ACTIVITY3)
+                    .addApp(LauncherModelHelper.TEST_PACKAGE, LauncherModelHelper.TEST_ACTIVITY4)
+                    .build()
+            )
+            .loadModelSync()
+        folderItems = modelHelper.bgDataModel.folders.valueAt(0).contents
+        folderIcon.mInfo = modelHelper.bgDataModel.folders.valueAt(0)
+        folderIcon.mInfo.contents = folderItems
+
+        // Set first icon to be themed.
+        folderItems[0]
+            .bitmap
+            .setMonoIcon(
+                folderItems[0].bitmap.icon,
+                BaseIconFactory(
+                    context,
+                    context.resources.configuration.densityDpi,
+                    previewItemManager.mIconSize
+                )
+            )
+
+        // Set second icon to be non-themed.
+        folderItems[1]
+            .bitmap
+            .setMonoIcon(
+                null,
+                BaseIconFactory(
+                    context,
+                    context.resources.configuration.densityDpi,
+                    previewItemManager.mIconSize
+                )
+            )
+
+        // Set third icon to be themed with badge.
+        folderItems[2]
+            .bitmap
+            .setMonoIcon(
+                folderItems[2].bitmap.icon,
+                BaseIconFactory(
+                    context,
+                    context.resources.configuration.densityDpi,
+                    previewItemManager.mIconSize
+                )
+            )
+        folderItems[2].bitmap =
+            folderItems[2].bitmap.withFlags(profileFlagOp(UserIconInfo.TYPE_WORK))
+
+        // Set fourth icon to be non-themed with badge.
+        folderItems[3].bitmap =
+            folderItems[3].bitmap.withFlags(profileFlagOp(UserIconInfo.TYPE_WORK))
+        folderItems[3]
+            .bitmap
+            .setMonoIcon(
+                null,
+                BaseIconFactory(
+                    context,
+                    context.resources.configuration.densityDpi,
+                    previewItemManager.mIconSize
+                )
+            )
+    }
+    @After
+    @Throws(Exception::class)
+    fun tearDown() {
+        modelHelper.destroy()
+    }
+
+    @Test
+    fun checkThemedIconWithThemingOn_iconShouldBeThemed() {
+        get(context).put(LauncherPrefs.THEMED_ICONS, true)
+        val drawingParams = PreviewItemDrawingParams(0f, 0f, 0f)
+
+        previewItemManager.setDrawable(drawingParams, folderItems[0])
+
+        assert((drawingParams.drawable as FastBitmapDrawable).isThemed)
+    }
+
+    @Test
+    fun checkThemedIconWithThemingOff_iconShouldNotBeThemed() {
+        get(context).put(LauncherPrefs.THEMED_ICONS, false)
+        val drawingParams = PreviewItemDrawingParams(0f, 0f, 0f)
+
+        previewItemManager.setDrawable(drawingParams, folderItems[0])
+
+        assert(!(drawingParams.drawable as FastBitmapDrawable).isThemed)
+    }
+
+    @Test
+    fun checkUnthemedIconWithThemingOn_iconShouldNotBeThemed() {
+        get(context).put(LauncherPrefs.THEMED_ICONS, true)
+        val drawingParams = PreviewItemDrawingParams(0f, 0f, 0f)
+
+        previewItemManager.setDrawable(drawingParams, folderItems[1])
+
+        assert(!(drawingParams.drawable as FastBitmapDrawable).isThemed)
+    }
+
+    @Test
+    fun checkUnthemedIconWithThemingOff_iconShouldNotBeThemed() {
+        get(context).put(LauncherPrefs.THEMED_ICONS, false)
+        val drawingParams = PreviewItemDrawingParams(0f, 0f, 0f)
+
+        previewItemManager.setDrawable(drawingParams, folderItems[1])
+
+        assert(!(drawingParams.drawable as FastBitmapDrawable).isThemed)
+    }
+
+    @Test
+    fun checkThemedIconWithBadgeWithThemingOn_iconAndBadgeShouldBeThemed() {
+        get(context).put(LauncherPrefs.THEMED_ICONS, true)
+        val drawingParams = PreviewItemDrawingParams(0f, 0f, 0f)
+
+        previewItemManager.setDrawable(drawingParams, folderItems[2])
+
+        assert((drawingParams.drawable as FastBitmapDrawable).isThemed)
+        assert(
+            ((drawingParams.drawable as FastBitmapDrawable).badge as UserBadgeDrawable).mIsThemed
+        )
+    }
+
+    @Test
+    fun checkUnthemedIconWithBadgeWithThemingOn_badgeShouldBeThemed() {
+        get(context).put(LauncherPrefs.THEMED_ICONS, true)
+        val drawingParams = PreviewItemDrawingParams(0f, 0f, 0f)
+
+        previewItemManager.setDrawable(drawingParams, folderItems[3])
+
+        assert(!(drawingParams.drawable as FastBitmapDrawable).isThemed)
+        assert(
+            ((drawingParams.drawable as FastBitmapDrawable).badge as UserBadgeDrawable).mIsThemed
+        )
+    }
+
+    @Test
+    fun checkUnthemedIconWithBadgeWithThemingOff_iconAndBadgeShouldNotBeThemed() {
+        get(context).put(LauncherPrefs.THEMED_ICONS, false)
+        val drawingParams = PreviewItemDrawingParams(0f, 0f, 0f)
+
+        previewItemManager.setDrawable(drawingParams, folderItems[3])
+
+        assert(!(drawingParams.drawable as FastBitmapDrawable).isThemed)
+        assert(
+            !((drawingParams.drawable as FastBitmapDrawable).badge as UserBadgeDrawable).mIsThemed
+        )
+    }
+
+    private fun profileFlagOp(type: Int) =
+        UserIconInfo(Process.myUserHandle(), type).applyBitmapInfoFlags(FlagOp.NO_OP)
+}
diff --git a/tests/src/com/android/launcher3/model/LoaderCursorTest.java b/tests/src/com/android/launcher3/model/LoaderCursorTest.java
index 389ec5c..56ac960 100644
--- a/tests/src/com/android/launcher3/model/LoaderCursorTest.java
+++ b/tests/src/com/android/launcher3/model/LoaderCursorTest.java
@@ -101,7 +101,7 @@
         });
 
         UserManagerState ums = new UserManagerState();
-        mLoaderCursor = new LoaderCursor(mCursor, mApp, ums);
+        mLoaderCursor = new LoaderCursor(mCursor, mApp, ums, null);
         ums.allUsers.put(0, Process.myUserHandle());
     }
 
diff --git a/tests/src/com/android/launcher3/model/LoaderTaskTest.kt b/tests/src/com/android/launcher3/model/LoaderTaskTest.kt
index def27b8..cb57918 100644
--- a/tests/src/com/android/launcher3/model/LoaderTaskTest.kt
+++ b/tests/src/com/android/launcher3/model/LoaderTaskTest.kt
@@ -1,11 +1,10 @@
 package com.android.launcher3.model
 
-import android.content.Context
+import android.appwidget.AppWidgetManager
 import android.os.UserHandle
 import android.platform.test.flag.junit.SetFlagsRule
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import androidx.test.platform.app.InstrumentationRegistry
 import com.android.launcher3.Flags
 import com.android.launcher3.InvariantDeviceProfile
 import com.android.launcher3.LauncherAppState
@@ -15,17 +14,20 @@
 import com.android.launcher3.icons.cache.CachingLogic
 import com.android.launcher3.icons.cache.IconCacheUpdateHandler
 import com.android.launcher3.pm.UserCache
+import com.android.launcher3.ui.TestViewHelpers
 import com.android.launcher3.util.Executors.MODEL_EXECUTOR
+import com.android.launcher3.util.LauncherModelHelper.SandboxModelContext
 import com.android.launcher3.util.LooperIdleLock
 import com.android.launcher3.util.UserIconInfo
-import com.android.launcher3.util.rule.StaticMockitoRule
 import com.google.common.truth.Truth
 import java.util.concurrent.CountDownLatch
+import org.junit.After
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.Mock
 import org.mockito.Mockito
 import org.mockito.Mockito.times
@@ -33,12 +35,14 @@
 import org.mockito.Mockito.`when`
 import org.mockito.MockitoAnnotations
 import org.mockito.Spy
+import org.mockito.kotlin.doReturn
 
 private const val INSERTION_STATEMENT_FILE = "databases/workspace_items.sql"
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class LoaderTaskTest {
+    private var context = SandboxModelContext()
     @Mock private lateinit var app: LauncherAppState
     @Mock private lateinit var bgAllAppsList: AllAppsList
     @Mock private lateinit var modelDelegate: ModelDelegate
@@ -52,21 +56,24 @@
 
     @Spy private var userManagerState: UserManagerState? = UserManagerState()
 
-    @get:Rule(order = 0) val staticMockitoRule = StaticMockitoRule(UserCache::class.java)
-    @get:Rule(order = 1)
-    val setFlagsRule = SetFlagsRule().apply { initAllFlagsToReleaseConfigDefault() }
+    @get:Rule val setFlagsRule = SetFlagsRule().apply { initAllFlagsToReleaseConfigDefault() }
 
     @Before
     fun setup() {
-        val context = InstrumentationRegistry.getInstrumentation().targetContext
+        MockitoAnnotations.initMocks(this)
+
         val idp =
-            InvariantDeviceProfile.INSTANCE[context].apply {
+            InvariantDeviceProfile().apply {
                 numRows = 5
                 numColumns = 6
                 numDatabaseHotseatIcons = 5
             }
+        context.putObject(InvariantDeviceProfile.INSTANCE, idp)
+        context.putObject(LauncherAppState.INSTANCE, app)
 
-        MockitoAnnotations.initMocks(this)
+        doReturn(TestViewHelpers.findWidgetProvider(false))
+            .`when`(context.spyService(AppWidgetManager::class.java))
+            .getAppWidgetInfo(anyInt())
         `when`(app.context).thenReturn(context)
         `when`(app.model).thenReturn(launcherModel)
         `when`(launcherModel.beginLoader(any(LoaderTask::class.java))).thenReturn(transaction)
@@ -77,7 +84,12 @@
         `when`(launcherBinder.newIdleLock(any(LoaderTask::class.java))).thenReturn(idleLock)
         `when`(idleLock.awaitLocked(1000)).thenReturn(false)
         `when`(iconCache.updateHandler).thenReturn(iconCacheUpdateHandler)
-        `when`(UserCache.getInstance(any(Context::class.java))).thenReturn(userCache)
+        context.putObject(UserCache.INSTANCE, userCache)
+    }
+
+    @After
+    fun tearDown() {
+        context.onDestroy()
     }
 
     @Test
diff --git a/tests/src/com/android/launcher3/model/ModelMultiCallbacksTest.java b/tests/src/com/android/launcher3/model/ModelMultiCallbacksTest.java
index 3dfd6b4..25a4c4e 100644
--- a/tests/src/com/android/launcher3/model/ModelMultiCallbacksTest.java
+++ b/tests/src/com/android/launcher3/model/ModelMultiCallbacksTest.java
@@ -73,6 +73,10 @@
         TestUtil.uninstallDummyApp();
     }
 
+    private ModelLauncherCallbacks getCallbacks() {
+        return mModelHelper.getModel().newModelCallbacks();
+    }
+
     @Test
     public void testTwoCallbacks_loadedTogether() throws Exception {
         setupWorkspacePages(3);
@@ -127,14 +131,14 @@
 
         // Install package 1
         TestUtil.installDummyApp();
-        mModelHelper.getModel().onPackageAdded(TestUtil.DUMMY_PACKAGE, Process.myUserHandle());
+        getCallbacks().onPackageAdded(TestUtil.DUMMY_PACKAGE, Process.myUserHandle());
         waitForLoaderAndTempMainThread();
         assertTrue(cb1.allApps().contains(TestUtil.DUMMY_PACKAGE));
         assertTrue(cb2.allApps().contains(TestUtil.DUMMY_PACKAGE));
 
         // Uninstall package 2
         TestUtil.uninstallDummyApp();
-        mModelHelper.getModel().onPackageRemoved(TestUtil.DUMMY_PACKAGE, Process.myUserHandle());
+        getCallbacks().onPackageRemoved(TestUtil.DUMMY_PACKAGE, Process.myUserHandle());
         waitForLoaderAndTempMainThread();
         assertFalse(cb1.allApps().contains(TestUtil.DUMMY_PACKAGE));
         assertFalse(cb2.allApps().contains(TestUtil.DUMMY_PACKAGE));
@@ -142,7 +146,7 @@
         // Unregister a callback and verify updates no longer received
         Executors.MAIN_EXECUTOR.execute(() -> mModelHelper.getModel().removeCallbacks(cb2));
         TestUtil.installDummyApp();
-        mModelHelper.getModel().onPackageAdded(TestUtil.DUMMY_PACKAGE, Process.myUserHandle());
+        getCallbacks().onPackageAdded(TestUtil.DUMMY_PACKAGE, Process.myUserHandle());
         waitForLoaderAndTempMainThread();
 
         // cb2 didn't get the update
diff --git a/tests/src/com/android/launcher3/nonquickstep/DeviceProfileDumpTest.kt b/tests/src/com/android/launcher3/nonquickstep/DeviceProfileDumpTest.kt
index 9b67310..9409ac1 100644
--- a/tests/src/com/android/launcher3/nonquickstep/DeviceProfileDumpTest.kt
+++ b/tests/src/com/android/launcher3/nonquickstep/DeviceProfileDumpTest.kt
@@ -30,7 +30,6 @@
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class DeviceProfileDumpTest : AbstractDeviceProfileTest() {
-    private val testContext: Context = InstrumentationRegistry.getInstrumentation().context
     private val folderName: String = "DeviceProfileDumpTest"
     @Test
     fun phonePortrait3Button() {
@@ -154,9 +153,6 @@
     }
 
     private fun assertDump(dp: DeviceProfile, filename: String) {
-        val dump = dump(context!!, dp, "${folderName}_$filename.txt")
-        val expected = readDumpFromAssets(testContext, "$folderName/$filename.txt")
-
-        assertThat(dump).isEqualTo(expected)
+        assertDump(dp, folderName, filename);
     }
 }
diff --git a/tests/src/com/android/launcher3/nonquickstep/HotseatWidthCalculationTest.kt b/tests/src/com/android/launcher3/nonquickstep/HotseatWidthCalculationTest.kt
index d102397..9912a34 100644
--- a/tests/src/com/android/launcher3/nonquickstep/HotseatWidthCalculationTest.kt
+++ b/tests/src/com/android/launcher3/nonquickstep/HotseatWidthCalculationTest.kt
@@ -47,7 +47,7 @@
         assertThat(dp.getHotseatLayoutPadding(context).right).isEqualTo(177)
 
         assertThat(dp.isQsbInline).isFalse()
-        assertThat(dp.hotseatQsbWidth).isEqualTo(1445)
+        assertThat(dp.hotseatQsbWidth).isEqualTo(1435)
     }
 
     /**
@@ -69,7 +69,7 @@
         assertThat(dp.getHotseatLayoutPadding(context).right).isEqualTo(110)
 
         assertThat(dp.isQsbInline).isFalse()
-        assertThat(dp.hotseatQsbWidth).isEqualTo(1080)
+        assertThat(dp.hotseatQsbWidth).isEqualTo(1070)
     }
 
     /**
@@ -90,7 +90,7 @@
         assertThat(dp.getHotseatLayoutPadding(context).right).isEqualTo(370)
 
         assertThat(dp.isQsbInline).isFalse()
-        assertThat(dp.hotseatQsbWidth).isEqualTo(1468)
+        assertThat(dp.hotseatQsbWidth).isEqualTo(1455)
     }
 
     /**
@@ -115,7 +115,7 @@
         assertThat(dp.getHotseatLayoutPadding(context).right).isEqualTo(668)
 
         assertThat(dp.isQsbInline).isFalse()
-        assertThat(dp.hotseatQsbWidth).isEqualTo(1224)
+        assertThat(dp.hotseatQsbWidth).isEqualTo(1214)
     }
 
     /** This is a case when after setting the hotseat, the QSB width needs to be changed to fit */
@@ -134,7 +134,7 @@
         assertThat(dp.getHotseatLayoutPadding(context).right).isEqualTo(640)
 
         assertThat(dp.isQsbInline).isFalse()
-        assertThat(dp.hotseatQsbWidth).isEqualTo(1179)
+        assertThat(dp.hotseatQsbWidth).isEqualTo(1169)
     }
 
     /**
@@ -156,7 +156,7 @@
         assertThat(dp.getHotseatLayoutPadding(context).right).isEqualTo(582)
 
         assertThat(dp.isQsbInline).isFalse()
-        assertThat(dp.hotseatQsbWidth).isEqualTo(1095)
+        assertThat(dp.hotseatQsbWidth).isEqualTo(1085)
     }
 
     @Test
@@ -176,7 +176,7 @@
 
             assertThat(dp.getHotseatLayoutPadding(context).left).isEqualTo(177)
             assertThat(dp.getHotseatLayoutPadding(context).right).isEqualTo(177)
-            assertThat(dp.hotseatQsbWidth).isEqualTo(1445)
+            assertThat(dp.hotseatQsbWidth).isEqualTo(1435)
         }
     }
 }
diff --git a/tests/src/com/android/launcher3/provider/RestoreDbTaskTest.java b/tests/src/com/android/launcher3/provider/RestoreDbTaskTest.java
index 10d9133..733f1e9 100644
--- a/tests/src/com/android/launcher3/provider/RestoreDbTaskTest.java
+++ b/tests/src/com/android/launcher3/provider/RestoreDbTaskTest.java
@@ -59,6 +59,7 @@
 import com.android.launcher3.LauncherPrefs;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.LauncherSettings.Favorites;
+import com.android.launcher3.backuprestore.LauncherRestoreEventLogger;
 import com.android.launcher3.model.ModelDbController;
 import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.LauncherModelHelper;
@@ -90,6 +91,7 @@
     private SQLiteDatabase mMockDb;
     private Cursor mMockCursor;
     private LauncherPrefs mPrefs;
+    private LauncherRestoreEventLogger mMockRestoreEventLogger;
 
     @Before
     public void setup() {
@@ -100,6 +102,7 @@
         mMockDb = mock(SQLiteDatabase.class);
         mMockCursor = mock(Cursor.class);
         mPrefs = new LauncherPrefs(mContext);
+        mMockRestoreEventLogger = mock(LauncherRestoreEventLogger.class);
     }
 
     @After
@@ -178,7 +181,7 @@
         assertEquals(10, getItemCountForProfile(db, myProfileId_old));
         assertEquals(6, getItemCountForProfile(db, workProfileId_old));
 
-        mTask.sanitizeDB(mContext, controller, controller.getDb(), bm);
+        mTask.sanitizeDB(mContext, controller, controller.getDb(), bm, mMockRestoreEventLogger);
 
         // All the data has been migrated to the new user ids
         assertEquals(0, getItemCountForProfile(db, myProfileId_old));
@@ -207,7 +210,7 @@
         assertEquals(10, getItemCountForProfile(db, myProfileId_old));
         assertEquals(6, getItemCountForProfile(db, workProfileId_old));
 
-        mTask.sanitizeDB(mContext, controller, controller.getDb(), bm);
+        mTask.sanitizeDB(mContext, controller, controller.getDb(), bm, mMockRestoreEventLogger);
 
         // All the data has been migrated to the new user ids
         assertEquals(0, getItemCountForProfile(db, myProfileId_old));
@@ -219,7 +222,7 @@
     @Test
     public void givenLauncherPrefsHasNoIds_whenRestoreAppWidgetIdsIfExists_thenIdsAreRemoved() {
         // When
-        mTask.restoreAppWidgetIdsIfExists(mContext, mMockController);
+        mTask.restoreAppWidgetIdsIfExists(mContext, mMockController, mMockRestoreEventLogger);
         // Then
         assertThat(mPrefs.has(OLD_APP_WIDGET_IDS, APP_WIDGET_IDS)).isFalse();
     }
@@ -235,7 +238,7 @@
 
         // When
         setRestoredAppWidgetIds(mContext, expectedOldIds, expectedNewIds);
-        mTask.restoreAppWidgetIdsIfExists(mContext, mMockController);
+        mTask.restoreAppWidgetIdsIfExists(mContext, mMockController, mMockRestoreEventLogger);
 
         // Then
         assertThat(expectedHost.getAppWidgetIds()).isEqualTo(expectedOldIds);
@@ -257,7 +260,7 @@
 
         // When
         setRestoredAppWidgetIds(mContext, expectedOldIds, expectedNewIds);
-        mTask.restoreAppWidgetIdsIfExists(mContext, mMockController);
+        mTask.restoreAppWidgetIdsIfExists(mContext, mMockController, mMockRestoreEventLogger);
 
         // Then
         assertThat(expectedHost.getAppWidgetIds()).isEqualTo(expectedOldIds);
@@ -280,12 +283,13 @@
         when(mMockDb.query(any(), any(), any(), any(), any(), any(), any()))
                 .thenReturn(mMockCursor);
         when(mMockCursor.moveToFirst()).thenReturn(true);
+        when(mMockCursor.getColumnNames()).thenReturn(new String[] {});
         when(mMockCursor.isAfterLast()).thenReturn(true);
         RestoreDbTask.setPending(mContext);
 
         // When
         setRestoredAppWidgetIds(mContext, expectedOldIds, expectedNewIds);
-        mTask.restoreAppWidgetIdsIfExists(mContext, mMockController);
+        mTask.restoreAppWidgetIdsIfExists(mContext, mMockController, mMockRestoreEventLogger);
 
         // Then
         assertThat(expectedHost.getAppWidgetIds()).isEqualTo(allExpectedIds);
diff --git a/tests/src/com/android/launcher3/responsive/ResponsiveSpecsProviderTest.kt b/tests/src/com/android/launcher3/responsive/ResponsiveSpecsProviderTest.kt
index 9681ca8..54a1dc5 100644
--- a/tests/src/com/android/launcher3/responsive/ResponsiveSpecsProviderTest.kt
+++ b/tests/src/com/android/launcher3/responsive/ResponsiveSpecsProviderTest.kt
@@ -84,7 +84,7 @@
                     startPadding = SizeSpec(1f.dpToPx()),
                     endPadding = SizeSpec(1f.dpToPx()),
                     gutter = SizeSpec(8f.dpToPx()),
-                    cellSize = SizeSpec(ofRemainderSpace = .25f)
+                    cellSize = SizeSpec(ofRemainderSpace = 1f)
                 )
             )
 
@@ -97,7 +97,7 @@
                     startPadding = SizeSpec(2f.dpToPx()),
                     endPadding = SizeSpec(2f.dpToPx()),
                     gutter = SizeSpec(8f.dpToPx()),
-                    cellSize = SizeSpec(ofRemainderSpace = .25f)
+                    cellSize = SizeSpec(ofRemainderSpace = 1f)
                 ),
             )
 
@@ -182,7 +182,7 @@
                     startPadding = SizeSpec(22f.dpToPx()),
                     endPadding = SizeSpec(22f.dpToPx()),
                     gutter = sizeSpec16,
-                    cellSize = SizeSpec(ofRemainderSpace = .25f)
+                    cellSize = SizeSpec(ofRemainderSpace = 1f)
                 )
             )
 
@@ -231,7 +231,7 @@
                     startPadding = SizeSpec(0f.dpToPx()),
                     endPadding = SizeSpec(36f.dpToPx()),
                     gutter = sizeSpec12,
-                    cellSize = SizeSpec(ofRemainderSpace = .25f)
+                    cellSize = SizeSpec(ofRemainderSpace = 1f)
                 ),
                 ResponsiveSpec(
                     maxAvailableSize = 716.dpToPx(),
@@ -240,7 +240,7 @@
                     startPadding = SizeSpec(16f.dpToPx()),
                     endPadding = SizeSpec(64f.dpToPx()),
                     gutter = sizeSpec12,
-                    cellSize = SizeSpec(ofRemainderSpace = .25f)
+                    cellSize = SizeSpec(ofRemainderSpace = 1f)
                 ),
                 ResponsiveSpec(
                     maxAvailableSize = 9999.dpToPx(),
@@ -249,7 +249,7 @@
                     startPadding = SizeSpec(36f.dpToPx()),
                     endPadding = SizeSpec(80f.dpToPx()),
                     gutter = sizeSpec12,
-                    cellSize = SizeSpec(ofRemainderSpace = .25f)
+                    cellSize = SizeSpec(ofRemainderSpace = 1f)
                 )
             )
 
@@ -262,7 +262,7 @@
                     startPadding = SizeSpec(0f),
                     endPadding = SizeSpec(24f.dpToPx()),
                     gutter = sizeSpec12,
-                    cellSize = SizeSpec(ofRemainderSpace = .25f)
+                    cellSize = SizeSpec(ofRemainderSpace = 1f)
                 ),
                 ResponsiveSpec(
                     maxAvailableSize = 9999.dpToPx(),
@@ -271,7 +271,7 @@
                     startPadding = SizeSpec(0f),
                     endPadding = SizeSpec(34f.dpToPx()),
                     gutter = sizeSpec12,
-                    cellSize = SizeSpec(ofRemainderSpace = .25f)
+                    cellSize = SizeSpec(ofRemainderSpace = 1f)
                 ),
             )
 
diff --git a/tests/src/com/android/launcher3/responsive/WorkspaceSpecsTest.kt b/tests/src/com/android/launcher3/responsive/WorkspaceSpecsTest.kt
index 9781645..17b0ee4 100644
--- a/tests/src/com/android/launcher3/responsive/WorkspaceSpecsTest.kt
+++ b/tests/src/com/android/launcher3/responsive/WorkspaceSpecsTest.kt
@@ -159,7 +159,7 @@
                     "maxSize=2147483647), " +
                     "cellSize=SizeSpec(fixedSize=0.0, " +
                     "ofAvailableSpace=0.0, " +
-                    "ofRemainderSpace=0.25, " +
+                    "ofRemainderSpace=1.0, " +
                     "matchWorkspace=false, " +
                     "maxSize=2147483647)" +
                     ")"
diff --git a/tests/src/com/android/launcher3/tapl/TaplUtilityTest.java b/tests/src/com/android/launcher3/tapl/TaplUtilityTest.java
new file mode 100644
index 0000000..b6ded97
--- /dev/null
+++ b/tests/src/com/android/launcher3/tapl/TaplUtilityTest.java
@@ -0,0 +1,65 @@
+/*
+ * 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.tapl;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+public class TaplUtilityTest {
+
+    @Test
+    public void testMakeMultilinePattern() {
+        // Original title will match.
+        assertTrue(AppIcon.makeMultilinePattern("Play Store")
+                .matcher("Play Store").matches());
+        assertTrue(AppIcon.makeMultilinePattern("PlayStore")
+                .matcher("PlayStore").matches());
+
+        // Original title with whitespace added will match.
+        assertTrue(AppIcon.makeMultilinePattern("PlayStore")
+                .matcher("Play\nStore").matches());
+        assertTrue(AppIcon.makeMultilinePattern("PlayStore")
+                .matcher("Play Store").matches());
+        // Original title with whitespace removed will also match.
+        assertTrue(AppIcon.makeMultilinePattern("Play Store")
+                .matcher("PlayStore").matches());
+        // Or whitespace replaced with a different kind of whitespace (both of above conditions).
+        assertTrue(AppIcon.makeMultilinePattern("Play Store")
+                .matcher("Play\nStore").matches());
+        assertTrue(AppIcon.makeMultilinePattern("Play Store")
+                .matcher("Play \n Store").matches());
+
+        // Any non-whitespace character added to the title will not match.
+        assertFalse(AppIcon.makeMultilinePattern("Play Store")
+                .matcher("Play Store has 7 notifications").matches());
+        assertFalse(AppIcon.makeMultilinePattern("Play Store")
+                .matcher("Play  Store!").matches());
+        // Title is case-sensitive.
+        assertFalse(AppIcon.makeMultilinePattern("Play Store")
+                .matcher("play store").matches());
+        assertFalse(AppIcon.makeMultilinePattern("Play Store")
+                .matcher("play  store").matches());
+        // Removing non whitespace characters will not match.
+        assertFalse(AppIcon.makeMultilinePattern("Play Store")
+                .matcher("").matches());
+        assertFalse(AppIcon.makeMultilinePattern("Play Store")
+                .matcher("Play Stor").matches());
+        assertFalse(AppIcon.makeMultilinePattern("Play Store")
+                .matcher("Play").matches());
+    }
+}
diff --git a/tests/src/com/android/launcher3/tapl/TaplUtilityTests.java b/tests/src/com/android/launcher3/tapl/TaplUtilityTests.java
deleted file mode 100644
index 15db1d8..0000000
--- a/tests/src/com/android/launcher3/tapl/TaplUtilityTests.java
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * 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.tapl;
-
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-
-import org.junit.Test;
-
-public class TaplUtilityTests {
-
-    @Test
-    public void testNewStringWithRegex() {
-        assertTrue(AppIcon.makeMultilinePattern("Play Store")
-                .matcher("Play Store has 7 notifications").matches());
-        assertTrue(AppIcon.makeMultilinePattern("Play Store")
-                .matcher("Play  Store!").matches());
-        assertFalse(AppIcon.makeMultilinePattern("Play Store")
-                .matcher("play  store").matches());
-        assertFalse(AppIcon.makeMultilinePattern("Play Store")
-                .matcher("").matches());
-        assertTrue(AppIcon.makeMultilinePattern("Play Store")
-                .matcher("Play \n Store").matches());
-    }
-}
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/src/com/android/launcher3/ui/TaplTestsLauncher3.java b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3Test.java
similarity index 95%
rename from tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
rename to tests/src/com/android/launcher3/ui/TaplTestsLauncher3Test.java
index 229ea45..d26a9db 100644
--- a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
+++ b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3Test.java
@@ -27,7 +27,7 @@
 
 @LargeTest
 @RunWith(AndroidJUnit4.class)
-public class TaplTestsLauncher3 extends AbstractLauncherUiTest {
+public class TaplTestsLauncher3Test extends AbstractLauncherUiTest {
 
     @Before
     public void setUp() throws Exception {
diff --git a/tests/src/com/android/launcher3/ui/TaplWorkProfileTest.java b/tests/src/com/android/launcher3/ui/TaplWorkProfileTest.java
index f818564..cb30854 100644
--- a/tests/src/com/android/launcher3/ui/TaplWorkProfileTest.java
+++ b/tests/src/com/android/launcher3/ui/TaplWorkProfileTest.java
@@ -19,7 +19,10 @@
 import static com.android.launcher3.LauncherState.ALL_APPS;
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.allapps.AllAppsStore.DEFER_UPDATES_TEST;
+import static com.android.launcher3.testing.shared.TestProtocol.NORMAL_STATE_ORDINAL;
 import static com.android.launcher3.util.TestUtil.installDummyAppForUser;
+import static com.android.launcher3.util.rule.TestStabilityRule.LOCAL;
+import static com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_POSTSUBMIT;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
@@ -39,12 +42,13 @@
 import com.android.launcher3.allapps.WorkProfileManager;
 import com.android.launcher3.tapl.LauncherInstrumentation;
 import com.android.launcher3.util.TestUtil;
+import com.android.launcher3.util.rule.TestStabilityRule;
 
 import org.junit.After;
 import org.junit.Before;
-import org.junit.Ignore;
 import org.junit.Test;
 
+import java.io.IOException;
 import java.util.Objects;
 import java.util.function.Predicate;
 
@@ -98,7 +102,17 @@
             launcher.getAppsView().getAppsStore().disableDeferUpdates(DEFER_UPDATES_TEST);
         });
         TestUtil.uninstallDummyApp();
-        mDevice.executeShellCommand("pm remove-user " + mProfileUserId);
+
+        mLauncher.runToState(
+                () -> {
+                    try {
+                        mDevice.executeShellCommand("pm remove-user " + mProfileUserId);
+                    } catch (IOException e) {
+                        throw new RuntimeException(e);
+                    }
+                },
+                NORMAL_STATE_ORDINAL,
+                "executing pm 'remove-user' command");
     }
 
     private void waitForWorkTabSetup() {
@@ -123,8 +137,10 @@
                 LauncherInstrumentation.WAIT_TIME_MS);
     }
 
+    // Staging; will be promoted to presubmit if stable
+    @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT)
+
     @Test
-    @Ignore("b/243855320")
     public void toggleWorks() {
         assumeTrue(mWorkProfileSetupSuccessful);
         waitForWorkTabSetup();
diff --git a/tests/src/com/android/launcher3/ui/widget/TaplAddWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/TaplAddWidgetTest.java
index 27fda9b..db38c68 100644
--- a/tests/src/com/android/launcher3/ui/widget/TaplAddWidgetTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/TaplAddWidgetTest.java
@@ -18,7 +18,6 @@
 import static org.junit.Assert.assertNotNull;
 
 import android.platform.test.annotations.PlatinumTest;
-import android.platform.test.rule.ScreenRecordRule;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.LargeTest;
@@ -29,9 +28,11 @@
 import com.android.launcher3.ui.AbstractLauncherUiTest;
 import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape;
 import com.android.launcher3.ui.TestViewHelpers;
+import com.android.launcher3.util.rule.ScreenRecordRule.ScreenRecord;
 import com.android.launcher3.util.rule.ShellCommandRule;
 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 
+import org.junit.Before;
 import org.junit.Ignore;
 import org.junit.Rule;
 import org.junit.Test;
@@ -47,10 +48,16 @@
     @Rule
     public ShellCommandRule mGrantWidgetRule = ShellCommandRule.grantWidgetBind();
 
-    @PlatinumTest(focusArea = "launcher")
+    @Override
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+        AbstractLauncherUiTest.initialize(this);
+    }
+
     @Test
     @PortraitLandscape
-    @ScreenRecordRule.ScreenRecord // b/289161193
+    @ScreenRecord // b/316910614
     public void testDragIcon() throws Throwable {
         mLauncher.enableDebugTracing(); // b/289161193
         new FavoriteItemsTransaction(mTargetContext).commitAndLoadHome(mLauncher);
@@ -100,6 +107,7 @@
      */
     @PlatinumTest(focusArea = "launcher")
     @Test
+    @ScreenRecord // b/316910614
     public void testResizeWidget() throws Throwable {
         new FavoriteItemsTransaction(mTargetContext).commitAndLoadHome(mLauncher);
 
diff --git a/tests/src/com/android/launcher3/ui/workspace/TaplWorkspaceTest.java b/tests/src/com/android/launcher3/ui/workspace/TaplWorkspaceTest.java
index 59c82a7..4edeb42 100644
--- a/tests/src/com/android/launcher3/ui/workspace/TaplWorkspaceTest.java
+++ b/tests/src/com/android/launcher3/ui/workspace/TaplWorkspaceTest.java
@@ -15,7 +15,6 @@
  */
 package com.android.launcher3.ui.workspace;
 
-import static com.android.launcher3.ui.AbstractLauncherUiTest.initialize;
 import static com.android.launcher3.util.TestConstants.AppNames.CHROME_APP_NAME;
 
 import static org.junit.Assert.assertEquals;
@@ -23,8 +22,6 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 
-import android.platform.test.annotations.PlatinumTest;
-
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.tapl.HomeAppIcon;
@@ -70,7 +67,6 @@
      * move between workspaces. After, make sure we can launch an app from the Workspace.
      * @throws Exception if we can't set the defaults icons that will appear at the beginning.
      */
-    @PlatinumTest(focusArea = "launcher")
     @Test
     public void testWorkspace() throws Exception {
         // Set workspace  that includes the chrome Activity app icon on the hotseat.
@@ -123,7 +119,6 @@
      * Similar to {@link TaplWorkspaceTest#testWorkspace} but here we also make sure we can delete
      * the pages.
      */
-    @PlatinumTest(focusArea = "launcher")
     @Test
     public void testAddAndDeletePageAndFling() {
         Workspace workspace = mLauncher.getWorkspace();
diff --git a/tests/src/com/android/launcher3/util/ExecutorRunnableTest.kt b/tests/src/com/android/launcher3/util/ExecutorRunnableTest.kt
new file mode 100644
index 0000000..972b592
--- /dev/null
+++ b/tests/src/com/android/launcher3/util/ExecutorRunnableTest.kt
@@ -0,0 +1,108 @@
+/*
+ * 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.util
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.util.rule.TestStabilityRule
+import java.util.concurrent.ExecutorService
+import junit.framework.Assert.assertEquals
+import junit.framework.Assert.assertFalse
+import junit.framework.Assert.assertTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** Unit test for [ExecutorRunnable] */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ExecutorRunnableTest {
+
+    private lateinit var underTest: ExecutorRunnable<Int>
+
+    private var result: Int = -1
+    private var isTaskExecuted = false
+    private var isCallbackExecuted = false
+
+    @get:Rule(order = 0) val testStabilityRule = TestStabilityRule()
+
+    @Before
+    fun setup() {
+        reset()
+        underTest =
+            ExecutorRunnable.createAndExecute(
+                Executors.UI_HELPER_EXECUTOR,
+                {
+                    isTaskExecuted = true
+                    1
+                },
+                Executors.VIEW_PREINFLATION_EXECUTOR,
+                {
+                    isCallbackExecuted = true
+                    result = it + 1
+                }
+            )
+    }
+
+    @Test
+    fun run_and_complete() {
+        awaitAllExecutorCompleted()
+
+        assertTrue("task should be executed", isTaskExecuted)
+        assertTrue("callback should be executed", isCallbackExecuted)
+        assertEquals(2, result)
+    }
+
+    @Test
+    @TestStabilityRule.Stability(
+        flavors = TestStabilityRule.LOCAL or TestStabilityRule.PLATFORM_POSTSUBMIT
+    ) // b/316588649
+    fun run_and_cancel_cancelCallback() {
+        underTest.cancel(false)
+        awaitAllExecutorCompleted()
+
+        assertFalse("callback should not be executed.", isCallbackExecuted)
+        assertEquals(0, result)
+    }
+
+    @Test
+    fun run_and_cancelAfterCompletion_executeAll() {
+        awaitAllExecutorCompleted()
+
+        underTest.cancel(false)
+
+        assertTrue("task should be executed", isTaskExecuted)
+        assertTrue("callback should be executed", isCallbackExecuted)
+        assertEquals(2, result)
+    }
+
+    private fun awaitExecutorCompleted(executor: ExecutorService) {
+        executor.submit<Any> { null }.get()
+    }
+
+    private fun awaitAllExecutorCompleted() {
+        awaitExecutorCompleted(Executors.UI_HELPER_EXECUTOR)
+        awaitExecutorCompleted(Executors.VIEW_PREINFLATION_EXECUTOR)
+    }
+
+    private fun reset() {
+        result = 0
+        isTaskExecuted = false
+        isCallbackExecuted = false
+    }
+}
diff --git a/tests/src/com/android/launcher3/util/LauncherModelHelper.java b/tests/src/com/android/launcher3/util/LauncherModelHelper.java
index 244dc26..e806d1d 100644
--- a/tests/src/com/android/launcher3/util/LauncherModelHelper.java
+++ b/tests/src/com/android/launcher3/util/LauncherModelHelper.java
@@ -233,7 +233,7 @@
         private final PackageManager mPm;
         private final File mDbDir;
 
-        SandboxModelContext() {
+        public SandboxModelContext() {
             super(ApplicationProvider.getApplicationContext(),
                     UserCache.INSTANCE, InstallSessionHelper.INSTANCE, LauncherPrefs.INSTANCE,
                     LauncherAppState.INSTANCE, InvariantDeviceProfile.INSTANCE,
diff --git a/tests/src/com/android/launcher3/util/ModelTestExtensions.kt b/tests/src/com/android/launcher3/util/ModelTestExtensions.kt
index 61ec669..6bd182b 100644
--- a/tests/src/com/android/launcher3/util/ModelTestExtensions.kt
+++ b/tests/src/com/android/launcher3/util/ModelTestExtensions.kt
@@ -1,7 +1,27 @@
 package com.android.launcher3.util
 
+import android.content.ContentValues
 import com.android.launcher3.LauncherModel
+import com.android.launcher3.LauncherSettings.Favorites
+import com.android.launcher3.LauncherSettings.Favorites.APPWIDGET_ID
+import com.android.launcher3.LauncherSettings.Favorites.APPWIDGET_PROVIDER
+import com.android.launcher3.LauncherSettings.Favorites.APPWIDGET_SOURCE
+import com.android.launcher3.LauncherSettings.Favorites.CELLX
+import com.android.launcher3.LauncherSettings.Favorites.CELLY
+import com.android.launcher3.LauncherSettings.Favorites.CONTAINER
+import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP
+import com.android.launcher3.LauncherSettings.Favorites.INTENT
+import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE
+import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION
+import com.android.launcher3.LauncherSettings.Favorites.PROFILE_ID
+import com.android.launcher3.LauncherSettings.Favorites.RESTORED
+import com.android.launcher3.LauncherSettings.Favorites.SCREEN
+import com.android.launcher3.LauncherSettings.Favorites.SPANX
+import com.android.launcher3.LauncherSettings.Favorites.SPANY
+import com.android.launcher3.LauncherSettings.Favorites.TITLE
+import com.android.launcher3.LauncherSettings.Favorites._ID
 import com.android.launcher3.model.BgDataModel
+import com.android.launcher3.model.ModelDbController
 
 object ModelTestExtensions {
     /** Clears and reloads Launcher db to cleanup the workspace */
@@ -10,7 +30,7 @@
         loadModelSync()
         TestUtil.runOnExecutorSync(Executors.MODEL_EXECUTOR) {
             modelDbController.run {
-                tryMigrateDB()
+                tryMigrateDB(null /* restoreEventLogger */)
                 createEmptyDB()
                 clearEmptyDbFlag()
             }
@@ -20,6 +40,7 @@
         loadModelSync()
     }
 
+    /** Loads the model in memory synchronously */
     fun LauncherModel.loadModelSync() {
         val mockCb: BgDataModel.Callbacks = object : BgDataModel.Callbacks {}
         TestUtil.runOnExecutorSync(Executors.MAIN_EXECUTOR) { addCallbacksAndLoad(mockCb) }
@@ -27,4 +48,54 @@
         TestUtil.runOnExecutorSync(Executors.MAIN_EXECUTOR) {}
         TestUtil.runOnExecutorSync(Executors.MAIN_EXECUTOR) { removeCallbacks(mockCb) }
     }
+
+    /** Adds and commits a new item to Launcher.db */
+    fun LauncherModel.addItem(
+        title: String = "LauncherTestApp",
+        intent: String =
+            "#Intent;action=android.intent.action.MAIN;category=android.intent.category.LAUNCHER;component=com.google.android.apps.nexuslauncher.tests/com.android.launcher3.testcomponent.BaseTestingActivity;launchFlags=0x10200000;end",
+        type: Int = ITEM_TYPE_APPLICATION,
+        restoreFlags: Int = 0,
+        screen: Int = 0,
+        container: Int = CONTAINER_DESKTOP,
+        x: Int,
+        y: Int,
+        spanX: Int = 1,
+        spanY: Int = 1,
+        id: Int = 0,
+        profileId: Int = 0,
+        tableName: String = Favorites.TABLE_NAME,
+        appWidgetId: Int = -1,
+        appWidgetSource: Int = -1,
+        appWidgetProvider: String? = null
+    ) {
+        loadModelSync()
+        TestUtil.runOnExecutorSync(Executors.MODEL_EXECUTOR) {
+            val controller: ModelDbController = modelDbController
+            controller.tryMigrateDB(null /* restoreEventLogger */)
+            modelDbController.newTransaction().use { transaction ->
+                val values =
+                    ContentValues().apply {
+                        values[_ID] = id
+                        values[TITLE] = title
+                        values[PROFILE_ID] = profileId
+                        values[CONTAINER] = container
+                        values[SCREEN] = screen
+                        values[CELLX] = x
+                        values[CELLY] = y
+                        values[SPANX] = spanX
+                        values[SPANY] = spanY
+                        values[ITEM_TYPE] = type
+                        values[RESTORED] = restoreFlags
+                        values[INTENT] = intent
+                        values[APPWIDGET_ID] = appWidgetId
+                        values[APPWIDGET_SOURCE] = appWidgetSource
+                        values[APPWIDGET_PROVIDER] = appWidgetProvider
+                    }
+                // Migrate any previous data so that the DB state is correct
+                controller.insert(tableName, values)
+                transaction.commit()
+            }
+        }
+    }
 }
diff --git a/tests/src/com/android/launcher3/util/TestUtil.java b/tests/src/com/android/launcher3/util/TestUtil.java
index 683f323..95444ba 100644
--- a/tests/src/com/android/launcher3/util/TestUtil.java
+++ b/tests/src/com/android/launcher3/util/TestUtil.java
@@ -103,7 +103,9 @@
             out.close();
 
             final String result = UiDevice.getInstance(instrumentation)
-                    .executeShellCommand("pm install --user " + userId + " " + apkFilename);
+                    .executeShellCommand(String.format("pm install -i %s --user ",
+                            instrumentation.getContext().getPackageName())
+                            + userId + " " + apkFilename);
             Assert.assertTrue(
                     "Failed to install wellbeing test apk; make sure the device is rooted",
                     "Success".equals(result.replaceAll("\\s+", "")));
diff --git a/tests/src/com/android/launcher3/util/rule/StaticMockitoRule.java b/tests/src/com/android/launcher3/util/rule/StaticMockitoRule.java
deleted file mode 100644
index 6b91474..0000000
--- a/tests/src/com/android/launcher3/util/rule/StaticMockitoRule.java
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.util.rule;
-
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
-
-import com.android.dx.mockito.inline.extended.StaticMockitoSession;
-import com.android.dx.mockito.inline.extended.StaticMockitoSessionBuilder;
-
-import org.junit.rules.MethodRule;
-import org.junit.runners.model.FrameworkMethod;
-import org.junit.runners.model.Statement;
-import org.mockito.junit.MockitoRule;
-import org.mockito.quality.Strictness;
-
-/**
- * Similar to {@link MockitoRule}, but uses {@link StaticMockitoSession}, which allows mocking
- * static methods.
- */
-public class StaticMockitoRule implements MethodRule {
-    private Class<?>[] mClasses;
-
-    public StaticMockitoRule(Class<?>... classes) {
-        mClasses = classes;
-    }
-
-    @Override
-    public Statement apply(Statement base, FrameworkMethod method, Object target) {
-        return new Statement() {
-            public void evaluate() throws Throwable {
-                StaticMockitoSessionBuilder builder =
-                        mockitoSession()
-                                .name(target.getClass().getSimpleName() + "." + method.getName())
-                                .initMocks(target)
-                                .strictness(Strictness.STRICT_STUBS);
-
-                for (Class<?> clazz : mClasses) {
-                    builder.mockStatic(clazz);
-                }
-
-                StaticMockitoSession session = builder.startMocking();
-                Throwable testFailure = evaluateSafely(base);
-                session.finishMocking(testFailure);
-                if (testFailure != null) {
-                    throw testFailure;
-                }
-            }
-
-            private Throwable evaluateSafely(Statement base) {
-                try {
-                    base.evaluate();
-                    return null;
-                } catch (Throwable throwable) {
-                    return throwable;
-                }
-            }
-        };
-    }
-}
diff --git a/tests/src/com/android/launcher3/util/rule/TestStabilityRule.java b/tests/src/com/android/launcher3/util/rule/TestStabilityRule.java
index b51045f..909aabd 100644
--- a/tests/src/com/android/launcher3/util/rule/TestStabilityRule.java
+++ b/tests/src/com/android/launcher3/util/rule/TestStabilityRule.java
@@ -83,6 +83,7 @@
 
     public static int getRunFlavor() {
         if (sRunFlavor != 0) return sRunFlavor;
+        if (isRobolectricTest()) return PLATFORM_POSTSUBMIT;
 
         final String flavorOverride = InstrumentationRegistry.getArguments().getString("flavor");
 
@@ -150,4 +151,8 @@
     public static boolean isPresubmit() {
         return getRunFlavor() == PLATFORM_PRESUBMIT;
     }
+
+    public static boolean isRobolectricTest() {
+        return Build.FINGERPRINT.contains("robolectric");
+    }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/AllApps.java b/tests/tapl/com/android/launcher3/tapl/AllApps.java
index 0e78565..2dbc29d 100644
--- a/tests/tapl/com/android/launcher3/tapl/AllApps.java
+++ b/tests/tapl/com/android/launcher3/tapl/AllApps.java
@@ -21,6 +21,7 @@
 
 import static com.android.launcher3.tapl.LauncherInstrumentation.DEFAULT_POLL_INTERVAL;
 import static com.android.launcher3.tapl.LauncherInstrumentation.WAIT_TIME_MS;
+import static com.android.launcher3.testing.shared.TestProtocol.NORMAL_STATE_ORDINAL;
 
 import android.graphics.Point;
 import android.graphics.Rect;
@@ -362,6 +363,7 @@
 
     /**
      * Taps outside bottom sheet to dismiss it. Available on tablets only.
+     *
      * @param tapRight Tap on the right of bottom sheet if true, or left otherwise.
      */
     public void dismissByTappingOutsideForTablet(boolean tapRight) {
@@ -375,7 +377,7 @@
                     ? mLauncher.waitForLauncherObject(FAST_SCROLLER_RES_ID) :
                     mLauncher.waitForLauncherObject(BOTTOM_SHEET_RES_ID);
 
-            mLauncher.touchOutsideContainer(container, tapRight, false);
+            touchOutside(tapRight, container);
             try (LauncherInstrumentation.Closable tapped = mLauncher.addContextLayer(
                     "tapped outside AllApps bottom sheet")) {
                 verifyVisibleContainerOnDismiss();
@@ -383,10 +385,14 @@
         }
     }
 
+    protected void touchOutside(boolean tapRight, UiObject2 container) {
+        mLauncher.touchOutsideContainer(container, tapRight, false);
+    }
+
     /** Presses the meta keyboard shortcut to dismiss AllApps. */
     public void dismissByKeyboardShortcut() {
         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
-            mLauncher.getDevice().pressKeyCode(KEYCODE_META_RIGHT);
+            pressMetaKey();
             try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
                     "pressed meta key")) {
                 verifyVisibleContainerOnDismiss();
@@ -394,12 +400,19 @@
         }
     }
 
+    protected void pressMetaKey() {
+        mLauncher.getDevice().pressKeyCode(KEYCODE_META_RIGHT);
+    }
+
     /** Presses the esc key to dismiss AllApps. */
     public void dismissByEscKey() {
         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
             mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_ALT_ESC_DOWN);
             mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_ALT_ESC_UP);
-            mLauncher.getDevice().pressKeyCode(KEYCODE_ESCAPE);
+            mLauncher.runToState(
+                    () -> mLauncher.getDevice().pressKeyCode(KEYCODE_ESCAPE),
+                    NORMAL_STATE_ORDINAL,
+                    "pressing esc key");
             try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
                     "pressed esc key")) {
                 verifyVisibleContainerOnDismiss();
@@ -411,6 +424,7 @@
 
     /**
      * Return the QSB UI object on the AllApps screen.
+     *
      * @return the QSB UI object.
      */
     @NonNull
diff --git a/tests/tapl/com/android/launcher3/tapl/AppIcon.java b/tests/tapl/com/android/launcher3/tapl/AppIcon.java
index 85098c8..156568b 100644
--- a/tests/tapl/com/android/launcher3/tapl/AppIcon.java
+++ b/tests/tapl/com/android/launcher3/tapl/AppIcon.java
@@ -36,9 +36,24 @@
         super(launcher, icon);
     }
 
+    /**
+     * Find an app icon with the given name.
+     *
+     * @param appName app icon to look for
+     */
+    static BySelector getAppIconSelector(String appName) {
+        // focusable=true to avoid matching folder labels
+        return By.clazz(TextView.class).text(makeMultilinePattern(appName)).focusable(true);
+    }
+
+    /**
+     * Find an app icon with the given name.
+     *
+     * @param appName app icon to look for
+     * @param launcher (optional) - only match ui elements from Launcher's package
+     */
     static BySelector getAppIconSelector(String appName, LauncherInstrumentation launcher) {
-        return By.clazz(TextView.class).desc(makeMultilinePattern(appName))
-                .pkg(launcher.getLauncherPackageName());
+        return getAppIconSelector(appName).pkg(launcher.getLauncherPackageName());
     }
 
     static BySelector getMenuItemSelector(String text, LauncherInstrumentation launcher) {
@@ -109,13 +124,16 @@
     }
 
     /**
-     * Create a regular expression pattern that matches strings starting with the app name, where
-     * spaces in the app name are replaced with zero or more occurrences of the "\s" character
-     * (which represents a whitespace character in regular expressions), followed by any characters
-     * after the app name.
+     * Create a regular expression pattern that matches strings containing all of the non-whitespace
+     * characters of the app name, with any amount of whitespace added between characters (e.g.
+     * newline for multiline app labels).
      */
     static Pattern makeMultilinePattern(String appName) {
-        return Pattern.compile(appName.replaceAll("\\s+", "\\\\s*") + ".*",
-                Pattern.DOTALL);
+        // Remove any existing whitespace.
+        appName = appName.replaceAll("\\s", "");
+        // Allow whitespace between characters, e.g. newline for 2 line app label.
+        StringBuilder regexBuldier = new StringBuilder("\\s*");
+        appName.chars().forEach(letter -> regexBuldier.append((char) letter).append("\\s*"));
+        return Pattern.compile(regexBuldier.toString());
     }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/Background.java b/tests/tapl/com/android/launcher3/tapl/Background.java
index 8713b68..9f2ce22 100644
--- a/tests/tapl/com/android/launcher3/tapl/Background.java
+++ b/tests/tapl/com/android/launcher3/tapl/Background.java
@@ -16,8 +16,6 @@
 
 package com.android.launcher3.tapl;
 
-import static android.view.accessibility.AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED;
-
 import static com.android.launcher3.tapl.OverviewTask.TASK_START_EVENT;
 import static com.android.launcher3.testing.shared.TestProtocol.OVERVIEW_STATE_ORDINAL;
 
@@ -244,12 +242,11 @@
                     endY = startY;
                 }
 
-                mLauncher.executeAndWaitForEvent(
+                mLauncher.executeAndWaitForLauncherStop(
                         () -> mLauncher.linearGesture(
                                 startX, startY, endX, endY, 20, false,
                                 LauncherInstrumentation.GestureScope.EXPECT_PILFER),
-                        event -> event.getEventType() == TYPE_WINDOW_STATE_CHANGED,
-                        () -> "Quick switch gesture didn't change window state", "swiping");
+                        "swiping");
             } else {
                 // Double press the recents button.
                 UiObject2 recentsButton = mLauncher.waitForNavigationUiObject("recent_apps");
@@ -258,10 +255,8 @@
                         "clicking Recents button for the first time");
                 mLauncher.getOverview();
                 mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, SQUARE_BUTTON_EVENT);
-                mLauncher.executeAndWaitForEvent(
+                mLauncher.executeAndWaitForLauncherStop(
                         () -> recentsButton.click(),
-                        event -> event.getEventType() == TYPE_WINDOW_STATE_CHANGED,
-                        () -> "Pressing recents button didn't change window state",
                         "clicking Recents button for the second time");
             }
             mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, TASK_START_EVENT);
diff --git a/tests/tapl/com/android/launcher3/tapl/BaseOverview.java b/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
index b6b4a47..1bc489c 100644
--- a/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
+++ b/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
@@ -16,7 +16,10 @@
 
 package com.android.launcher3.tapl;
 
+import static android.view.KeyEvent.KEYCODE_ESCAPE;
+
 import static com.android.launcher3.tapl.LauncherInstrumentation.TASKBAR_RES_ID;
+import static com.android.launcher3.testing.shared.TestProtocol.NORMAL_STATE_ORDINAL;
 
 import android.graphics.Rect;
 
@@ -27,15 +30,24 @@
 import androidx.test.uiautomator.Direction;
 import androidx.test.uiautomator.UiObject2;
 
+import com.android.launcher3.testing.shared.TestProtocol;
+
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
+import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 
 /**
  * Common overview panel for both Launcher and fallback recents
  */
 public class BaseOverview extends LauncherInstrumentation.VisibleContainer {
+
+    private static final Pattern EVENT_ALT_ESC_DOWN = Pattern.compile(
+            "Key event: KeyEvent.*?action=ACTION_DOWN.*?keyCode=KEYCODE_ESCAPE.*?metaState=0");
+    private static final Pattern EVENT_ALT_ESC_UP = Pattern.compile(
+            "Key event: KeyEvent.*?action=ACTION_UP.*?keyCode=KEYCODE_ESCAPE.*?metaState=0");
+
     private static final int FLINGS_FOR_DISMISS_LIMIT = 40;
 
     BaseOverview(LauncherInstrumentation launcher) {
@@ -128,8 +140,19 @@
                 flingForwardImpl();
             }
 
-            mLauncher.clickLauncherObject(
-                    mLauncher.waitForObjectInContainer(verifyActiveContainer(), clearAllSelector));
+            final Runnable clickClearAll = () -> mLauncher.clickLauncherObject(
+                    mLauncher.waitForObjectInContainer(verifyActiveContainer(),
+                            clearAllSelector));
+            if (mLauncher.is3PLauncher()) {
+                mLauncher.executeAndWaitForLauncherStop(
+                        clickClearAll,
+                        "clicking 'Clear All'");
+            } else {
+                mLauncher.runToState(
+                        clickClearAll,
+                        NORMAL_STATE_ORDINAL,
+                        "clicking 'Clear All'");
+            }
 
             mLauncher.waitUntilLauncherObjectGone(clearAllSelector);
         }
@@ -150,9 +173,12 @@
 
             OverviewTask currentTask = flingToFirstTask();
 
-            mLauncher.touchOutsideContainer(currentTask.getUiObject(),
-                    /* tapRight= */ true,
-                    /* halfwayToEdge= */ false);
+            mLauncher.runToState(
+                    () -> mLauncher.touchOutsideContainer(currentTask.getUiObject(),
+                            /* tapRight= */ true,
+                            /* halfwayToEdge= */ false),
+                    NORMAL_STATE_ORDINAL,
+                    "touching outside of first task");
 
             return new Workspace(mLauncher);
         }
@@ -185,11 +211,15 @@
     public void touchTaskbarBottomCorner(boolean tapRight) {
         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
             Taskbar taskbar = new Taskbar(mLauncher);
-            taskbar.touchBottomCorner(tapRight);
             if (mLauncher.isTransientTaskbar()) {
+                mLauncher.runToState(
+                        () -> taskbar.touchBottomCorner(tapRight),
+                        NORMAL_STATE_ORDINAL,
+                        "touching taskbar");
                 // Tapping outside Transient Taskbar returns to Workspace, wait for that state.
                 new Workspace(mLauncher);
             } else {
+                taskbar.touchBottomCorner(tapRight);
                 // Should stay in Overview.
                 verifyActiveContainer();
                 verifyActionsViewVisibility();
@@ -367,6 +397,23 @@
         return !task.isTaskSplit();
     }
 
+    /**
+     * Presses the esc key to dismiss Overview.
+     */
+    public Workspace dismissByEscKey() {
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
+            mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_ALT_ESC_DOWN);
+            mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_ALT_ESC_UP);
+            mLauncher.runToState(
+                    () -> mLauncher.getDevice().pressKeyCode(KEYCODE_ESCAPE),
+                    NORMAL_STATE_ORDINAL, "pressing esc key");
+            try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                    "pressed esc key")) {
+                return mLauncher.getWorkspace();
+            }
+        }
+    }
+
     private void verifyActionsViewVisibility() {
         try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
                 "want to assert overview actions view visibility")) {
diff --git a/tests/tapl/com/android/launcher3/tapl/HomeAllApps.java b/tests/tapl/com/android/launcher3/tapl/HomeAllApps.java
index d9b179c..f8e1c10 100644
--- a/tests/tapl/com/android/launcher3/tapl/HomeAllApps.java
+++ b/tests/tapl/com/android/launcher3/tapl/HomeAllApps.java
@@ -30,8 +30,8 @@
 
     /**
      * Swipes up or down to dismiss to Workspace.
-     * @param swipeDown Swipe all apps down to dismiss, otherwise swipe up to dismiss by going home.
      *
+     * @param swipeDown Swipe all apps down to dismiss, otherwise swipe up to dismiss by going home.
      * @return the Workspace object.
      */
     @NonNull
@@ -118,4 +118,33 @@
     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);
+        }
+    }
+
+    @Override
+    protected void touchOutside(boolean tapRight, UiObject2 container) {
+        mLauncher.runToState(
+                () -> super.touchOutside(tapRight, container),
+                NORMAL_STATE_ORDINAL,
+                "touching outside");
+    }
+
+    @Override
+    protected void pressMetaKey() {
+        mLauncher.runToState(
+                () -> super.pressMetaKey(),
+                NORMAL_STATE_ORDINAL,
+                "pressing meta key");
+    }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/HomeQsb.java b/tests/tapl/com/android/launcher3/tapl/HomeQsb.java
index 5385c65..c1fc45f 100644
--- a/tests/tapl/com/android/launcher3/tapl/HomeQsb.java
+++ b/tests/tapl/com/android/launcher3/tapl/HomeQsb.java
@@ -15,6 +15,8 @@
  */
 package com.android.launcher3.tapl;
 
+import static com.android.launcher3.testing.shared.TestProtocol.ALL_APPS_STATE_ORDINAL;
+
 import androidx.test.uiautomator.UiObject2;
 
 /**
@@ -25,4 +27,13 @@
     HomeQsb(LauncherInstrumentation launcher, UiObject2 hotseat) {
         super(launcher, hotseat, "search_container_hotseat");
     }
+
+    @Override
+    protected void clickQsb() {
+        // Clicking Qsb will switch to All Apps state.
+        mLauncher.runToState(
+                () -> super.clickQsb(),
+                ALL_APPS_STATE_ORDINAL,
+                "Clicking Qsb");
+    }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/KeyboardQuickSwitch.java b/tests/tapl/com/android/launcher3/tapl/KeyboardQuickSwitch.java
index a1d8059..5ef82ca 100644
--- a/tests/tapl/com/android/launcher3/tapl/KeyboardQuickSwitch.java
+++ b/tests/tapl/com/android/launcher3/tapl/KeyboardQuickSwitch.java
@@ -58,15 +58,15 @@
 
     private final LauncherInstrumentation mLauncher;
     private final LauncherInstrumentation.ContainerType mStartingContainerType;
-    private final boolean mExpectHomeKeyEventsOnDismiss;
+    private final boolean mIsHomeState;
 
     KeyboardQuickSwitch(
             LauncherInstrumentation launcher,
             LauncherInstrumentation.ContainerType startingContainerType,
-            boolean expectHomeKeyEventsOnDismiss) {
+            boolean isHomeState) {
         mLauncher = launcher;
         mStartingContainerType = startingContainerType;
-        mExpectHomeKeyEventsOnDismiss = expectHomeKeyEventsOnDismiss;
+        mIsHomeState = isHomeState;
     }
 
     /**
@@ -164,7 +164,7 @@
                 mLauncher.verifyContainerType(mStartingContainerType);
 
                 // Wait until the device has fully settled before unpressing the key code
-                if (mExpectHomeKeyEventsOnDismiss) {
+                if (mIsHomeState) {
                     mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_HOME_ALT_LEFT_UP);
                 }
                 mLauncher.unpressKeyCode(KeyEvent.KEYCODE_ALT_LEFT, 0);
@@ -204,7 +204,14 @@
                 "want to launch focused task: "
                         + (expectedPackageName == null ? "Overview" : expectedPackageName))) {
             mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_KQS_ALT_LEFT_UP);
-            mLauncher.unpressKeyCode(KeyEvent.KEYCODE_ALT_LEFT, 0);
+
+            if (expectedPackageName == null || !mIsHomeState) {
+                mLauncher.unpressKeyCode(KeyEvent.KEYCODE_ALT_LEFT, 0);
+            } else {
+                mLauncher.executeAndWaitForLauncherStop(
+                        () -> mLauncher.unpressKeyCode(KeyEvent.KEYCODE_ALT_LEFT, 0),
+                        "unpressing left alt");
+            }
 
             try (LauncherInstrumentation.Closable c2 = mLauncher.addContextLayer(
                     "un-pressed left alt")) {
diff --git a/tests/tapl/com/android/launcher3/tapl/Launchable.java b/tests/tapl/com/android/launcher3/tapl/Launchable.java
index fe927b3..ed47334 100644
--- a/tests/tapl/com/android/launcher3/tapl/Launchable.java
+++ b/tests/tapl/com/android/launcher3/tapl/Launchable.java
@@ -16,8 +16,6 @@
 
 package com.android.launcher3.tapl;
 
-import static android.view.accessibility.AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED;
-
 import static com.android.launcher3.testing.shared.TestProtocol.SPRING_LOADED_STATE_ORDINAL;
 
 import android.graphics.Point;
@@ -58,8 +56,8 @@
      */
     public LaunchedAppState launch(String expectedPackageName) {
         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
-            try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
-                    "want to launch an app from " + launchableType())) {
+            try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(String.format(
+                    "want to launch an app (%s) from %s", expectedPackageName, launchableType()))) {
                 LauncherInstrumentation.log("Launchable.launch before click "
                         + mObject.getVisibleCenter() + " in "
                         + mLauncher.getVisibleBounds(mObject));
@@ -97,12 +95,10 @@
             LauncherInstrumentation.log("Launchable.launch before click "
                     + mObject.getVisibleCenter() + " in " + mLauncher.getVisibleBounds(
                     mObject));
-            mLauncher.executeAndWaitForLauncherEvent(
+
+            mLauncher.executeAndWaitForLauncherStop(
                     () -> mLauncher.clickLauncherObject(mObject),
-                    accessibilityEvent ->
-                            accessibilityEvent.getEventType() == TYPE_WINDOW_STATE_CHANGED,
-                    () -> "Unable to click object to launch split",
-                    "Click launcher object to launch split");
+                    "clicking the launchable");
 
             try (LauncherInstrumentation.Closable c2 = mLauncher.addContextLayer("clicked")) {
                 mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, OverviewTask.SPLIT_START_EVENT);
diff --git a/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java b/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java
index 6d58a35..200f2ff 100644
--- a/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java
+++ b/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java
@@ -130,8 +130,11 @@
             int endX = startX;
             int endY = startY - taskbarFromNavThreshold;
 
-            mLauncher.linearGesture(startX, startY, endX, endY, 10, /* slowDown= */ true,
-                    LauncherInstrumentation.GestureScope.EXPECT_PILFER);
+            mLauncher.executeAndWaitForLauncherStop(
+                    () -> mLauncher.linearGesture(startX, startY, endX, endY, 10,
+                            /* slowDown= */ true,
+                            LauncherInstrumentation.GestureScope.EXPECT_PILFER),
+                    "swiping");
             LauncherInstrumentation.log("swipeUpToUnstashTaskbar: sent linear swipe up gesture");
 
             return new Taskbar(mLauncher);
@@ -340,4 +343,21 @@
             }
         }
     }
+
+    /** 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")) {
+            if (mLauncher.isLauncher3()) {
+                mLauncher.pressBackImpl();
+            } else {
+                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..f68e12c 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -21,6 +21,7 @@
 import static android.content.pm.PackageManager.MATCH_ALL;
 import static android.content.pm.PackageManager.MATCH_DISABLED_COMPONENTS;
 import static android.view.KeyEvent.ACTION_DOWN;
+import static android.view.MotionEvent.ACTION_SCROLL;
 import static android.view.MotionEvent.ACTION_UP;
 import static android.view.MotionEvent.AXIS_GESTURE_SWIPE_FINGER_COUNT;
 
@@ -277,21 +278,35 @@
         assertNotNull("Cannot find content provider for " + testProviderAuthority, pi);
         ComponentName cn = new ComponentName(pi.packageName, pi.name);
 
+        final int iterations = isLauncherTest ? 300 : 100;
+
         if (pm.getComponentEnabledSetting(cn) != COMPONENT_ENABLED_STATE_ENABLED) {
             if (TestHelpers.isInLauncherProcess()) {
                 pm.setComponentEnabledSetting(cn, COMPONENT_ENABLED_STATE_ENABLED, DONT_KILL_APP);
             } else {
                 try {
                     final int userId = getContext().getUserId();
+                    final String launcherPidCommand = "pidof " + pi.packageName;
+                    final String initialPid = mDevice.executeShellCommand(launcherPidCommand);
+
                     mDevice.executeShellCommand(
                             "pm enable --user " + userId + " " + cn.flattenToString());
+
+                    // Wait for Launcher restart after enabling test provider.
+                    for (int i = 0; i < iterations; ++i) {
+                        final String currentPid = mDevice.executeShellCommand(launcherPidCommand)
+                                .replaceAll("\\s", "");
+                        if (!currentPid.isEmpty() && !currentPid.equals(initialPid)) break;
+                        if (i == iterations - 1) {
+                            fail("Launcher didn't restart after enabling test provider");
+                        }
+                        SystemClock.sleep(100);
+                    }
                 } catch (IOException e) {
                     fail(e.toString());
                 }
             }
 
-            final int iterations = isLauncherTest ? 300 : 100;
-
             // Wait for Launcher content provider to become enabled.
             for (int i = 0; i < iterations; ++i) {
                 final ContentProviderClient testProvider = getContext().getContentResolver()
@@ -370,8 +385,8 @@
                 .getParcelable(TestProtocol.TEST_INFO_RESPONSE_FIELD);
     }
 
-    Insets getImeInsets() {
-        return getTestInfo(TestProtocol.REQUEST_IME_INSETS)
+    Insets getSystemGestureRegion() {
+        return getTestInfo(TestProtocol.REQUEST_SYSTEM_GESTURE_REGION)
                 .getParcelable(TestProtocol.TEST_INFO_RESPONSE_FIELD);
     }
 
@@ -405,6 +420,11 @@
                 .getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD);
     }
 
+    public int getOverviewCurrentPageIndex() {
+        return getTestInfo(TestProtocol.REQUEST_GET_OVERVIEW_CURRENT_PAGE_INDEX)
+                .getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD);
+    }
+
     float getExactScreenCenterX() {
         return getRealDisplaySize().x / 2f;
     }
@@ -1007,11 +1027,11 @@
             return;
         }
 
-        linearGesture(
-                displaySize.x / 2, displaySize.y - 1,
-                displaySize.x / 2, 0,
-                ZERO_BUTTON_STEPS_FROM_BACKGROUND_TO_HOME,
-                false, GestureScope.EXPECT_PILFER);
+        if (isLauncher3()) {
+            gestureToDismissPopup(displaySize);
+        } else {
+            runToState(() -> gestureToDismissPopup(displaySize), NORMAL_STATE_ORDINAL, "swiping");
+        }
 
         try (LauncherInstrumentation.Closable c1 = addContextLayer(
                 String.format("Swiped up from floating view %s to home", floatingRes.get()))) {
@@ -1020,6 +1040,14 @@
         }
     }
 
+    private void gestureToDismissPopup(Point displaySize) {
+        linearGesture(
+                displaySize.x / 2, displaySize.y - 1,
+                displaySize.x / 2, 0,
+                ZERO_BUTTON_STEPS_FROM_BACKGROUND_TO_HOME,
+                false, GestureScope.EXPECT_PILFER);
+    }
+
     /**
      * @return the Workspace object.
      * @deprecated use goHome().
@@ -1121,30 +1149,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);
             }
         }
     }
@@ -1525,7 +1557,8 @@
         }
     }
 
-    void runToState(Runnable command, int expectedState, String actionName) {
+    /** Run an action and wait for the specified Launcher state. */
+    public void runToState(Runnable command, int expectedState, String actionName) {
         final List<Integer> actualEvents = new ArrayList<>();
         executeAndWaitForLauncherEvent(
                 command,
@@ -1698,6 +1731,16 @@
                 "scrolling");
     }
 
+    void pointerScroll(float pointerX, float pointerY, Direction direction) {
+        executeAndWaitForLauncherEvent(
+                () -> injectEvent(getPointerMotionEvent(
+                        ACTION_SCROLL, pointerX, pointerY, direction)),
+                event -> TestProtocol.SCROLL_FINISHED_MESSAGE.equals(event.getClassName()),
+                () -> "Didn't receive a scroll end message: " + direction + " scroll from ("
+                        + pointerX + ", " + pointerY + ")",
+                "scrolling");
+    }
+
     // Inject a swipe gesture. Inject exactly 'steps' motion points, incrementing event time by a
     // fixed interval each time.
     public void linearGesture(int startX, int startY, int endX, int endY, int steps,
@@ -1706,32 +1749,38 @@
         final long downTime = SystemClock.uptimeMillis();
         final Point start = new Point(startX, startY);
         final Point end = new Point(endX, endY);
+        long endTime = downTime;
         sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN, start, gestureScope);
-        if (mTrackpadGestureType != TrackpadGestureType.NONE) {
-            sendPointer(downTime, downTime, getPointerAction(MotionEvent.ACTION_POINTER_DOWN, 1),
-                    start, gestureScope);
-            if (mTrackpadGestureType == TrackpadGestureType.THREE_FINGER
-                    || mTrackpadGestureType == TrackpadGestureType.FOUR_FINGER) {
+        try {
+
+            if (mTrackpadGestureType != TrackpadGestureType.NONE) {
                 sendPointer(downTime, downTime,
-                        getPointerAction(MotionEvent.ACTION_POINTER_DOWN, 2),
+                        getPointerAction(MotionEvent.ACTION_POINTER_DOWN, 1),
                         start, gestureScope);
-                if (mTrackpadGestureType == TrackpadGestureType.FOUR_FINGER) {
+                if (mTrackpadGestureType == TrackpadGestureType.THREE_FINGER
+                        || mTrackpadGestureType == TrackpadGestureType.FOUR_FINGER) {
                     sendPointer(downTime, downTime,
-                            getPointerAction(MotionEvent.ACTION_POINTER_DOWN, 3),
+                            getPointerAction(MotionEvent.ACTION_POINTER_DOWN, 2),
+                            start, gestureScope);
+                    if (mTrackpadGestureType == TrackpadGestureType.FOUR_FINGER) {
+                        sendPointer(downTime, downTime,
+                                getPointerAction(MotionEvent.ACTION_POINTER_DOWN, 3),
+                                start, gestureScope);
+                    }
+                }
+            }
+            endTime = movePointer(
+                    start, end, steps, false, downTime, downTime, slowDown, gestureScope);
+            if (mTrackpadGestureType != TrackpadGestureType.NONE) {
+                for (int i = mPointerCount; i >= 2; i--) {
+                    sendPointer(downTime, downTime,
+                            getPointerAction(MotionEvent.ACTION_POINTER_UP, i - 1),
                             start, gestureScope);
                 }
             }
+        } finally {
+            sendPointer(downTime, endTime, MotionEvent.ACTION_UP, end, gestureScope);
         }
-        final long endTime = movePointer(
-                start, end, steps, false, downTime, downTime, slowDown, gestureScope);
-        if (mTrackpadGestureType != TrackpadGestureType.NONE) {
-            for (int i = mPointerCount; i >= 2; i--) {
-                sendPointer(downTime, downTime,
-                        getPointerAction(MotionEvent.ACTION_POINTER_UP, i - 1),
-                        start, gestureScope);
-            }
-        }
-        sendPointer(downTime, endTime, MotionEvent.ACTION_UP, end, gestureScope);
     }
 
     private static int getPointerAction(int action, int index) {
@@ -1761,6 +1810,41 @@
         return getContext().getResources();
     }
 
+    private static MotionEvent getPointerMotionEvent(
+            int action, float x, float y, Direction direction) {
+        MotionEvent.PointerCoords[] coordinates = new MotionEvent.PointerCoords[1];
+        coordinates[0] = new MotionEvent.PointerCoords();
+        coordinates[0].x = x;
+        coordinates[0].y = y;
+        boolean isVertical = direction == Direction.UP || direction == Direction.DOWN;
+        boolean isForward = direction == Direction.RIGHT || direction == Direction.DOWN;
+        coordinates[0].setAxisValue(
+                isVertical ? MotionEvent.AXIS_VSCROLL : MotionEvent.AXIS_HSCROLL,
+                isForward ? 1f : -1f);
+
+        MotionEvent.PointerProperties[] properties = new MotionEvent.PointerProperties[1];
+        properties[0] = new MotionEvent.PointerProperties();
+        properties[0].id = 0;
+        properties[0].toolType = MotionEvent.TOOL_TYPE_MOUSE;
+
+        final long downTime = SystemClock.uptimeMillis();
+        return MotionEvent.obtain(
+                downTime,
+                downTime,
+                action,
+                /* pointerCount= */ 1,
+                properties,
+                coordinates,
+                /* metaState= */ 0,
+                /* buttonState= */ 0,
+                /* xPrecision= */ 1f,
+                /* yPrecision= */ 1f,
+                /* deviceId= */ 0,
+                /* edgeFlags= */ 0,
+                InputDevice.SOURCE_CLASS_POINTER,
+                /* flags= */ 0);
+    }
+
     private static MotionEvent getTrackpadMotionEvent(long downTime, long eventTime,
             int action, float x, float y, int pointerCount, TrackpadGestureType gestureType) {
         MotionEvent.PointerProperties[] pointerProperties =
@@ -2090,6 +2174,11 @@
         getTestInfo(TestProtocol.REQUEST_UNSTASH_TASKBAR_IF_STASHED);
     }
 
+    /** Shows the bubble bar if it is stashed, otherwise this does nothing. */
+    public void showBubbleBarIfHidden() {
+        getTestInfo(TestProtocol.REQUEST_UNSTASH_BUBBLE_BAR_IF_STASHED);
+    }
+
     /** Blocks the taskbar from automatically stashing based on time. */
     public void enableBlockTimeout(boolean enable) {
         getTestInfo(enable
@@ -2283,12 +2372,13 @@
                         : containerBounds.left - 1;
             }
             // If IME is visible and overlaps the container bounds, touch above it.
+            final Insets systemGestureRegion = getSystemGestureRegion();
             int bottomBound = Math.min(
                     containerBounds.bottom,
-                    getRealDisplaySize().y - getImeInsets().bottom);
-            int y = (bottomBound + containerBounds.top) / 2;
+                    getRealDisplaySize().y - systemGestureRegion.bottom);
+            int y = (bottomBound - containerBounds.top) / 2;
             // Do not tap in the status bar.
-            y = Math.max(y, getWindowInsets().top);
+            y = Math.max(y, systemGestureRegion.top);
 
             final long downTime = SystemClock.uptimeMillis();
             final Point tapTarget = new Point(x, y);
diff --git a/tests/tapl/com/android/launcher3/tapl/OverviewActions.java b/tests/tapl/com/android/launcher3/tapl/OverviewActions.java
index bd2c9c1..d7c40a0 100644
--- a/tests/tapl/com/android/launcher3/tapl/OverviewActions.java
+++ b/tests/tapl/com/android/launcher3/tapl/OverviewActions.java
@@ -89,8 +89,7 @@
     private SelectModeButtons getSelectModeButtons() {
         try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
                 "want to get select mode buttons")) {
-            UiObject2 selectModeButtons = mLauncher.waitForLauncherObject("select_mode_buttons");
-            return new SelectModeButtons(selectModeButtons, mLauncher);
+            return new SelectModeButtons(mLauncher);
         }
     }
 
diff --git a/tests/tapl/com/android/launcher3/tapl/OverviewTaskMenu.java b/tests/tapl/com/android/launcher3/tapl/OverviewTaskMenu.java
index 3d2914d..902ad5b 100644
--- a/tests/tapl/com/android/launcher3/tapl/OverviewTaskMenu.java
+++ b/tests/tapl/com/android/launcher3/tapl/OverviewTaskMenu.java
@@ -16,6 +16,9 @@
 
 package com.android.launcher3.tapl;
 
+import static com.android.launcher3.testing.shared.TestProtocol.OVERVIEW_MODAL_TASK_STATE_ORDINAL;
+import static com.android.launcher3.testing.shared.TestProtocol.OVERVIEW_SPLIT_SELECT_ORDINAL;
+
 import androidx.annotation.NonNull;
 import androidx.test.uiautomator.By;
 import androidx.test.uiautomator.UiObject2;
@@ -40,8 +43,11 @@
         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
              LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
                      "tap split menu item")) {
-            mLauncher.clickLauncherObject(
-                    mLauncher.findObjectInContainer(mMenu, By.textStartsWith("Split")));
+            mLauncher.runToState(() -> mLauncher.clickLauncherObject(
+                            mLauncher.findObjectInContainer(mMenu, By.textStartsWith("Split"))),
+                    OVERVIEW_SPLIT_SELECT_ORDINAL,
+                    "tapping split menu item"
+            );
 
             try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
                     "tapped split menu item")) {
@@ -72,6 +78,25 @@
         }
     }
 
+    /** Taps the select menu item from the overview task menu. */
+    @NonNull
+    public SelectModeButtons tapSelectMenuItem() {
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+             LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                     "before tapping the select menu item")) {
+
+            mLauncher.runToState(
+                    () -> mLauncher.clickLauncherObject(
+                            mLauncher.findObjectInContainer(mMenu, By.text("Select"))),
+                    OVERVIEW_MODAL_TASK_STATE_ORDINAL, "tapping select menu item");
+
+            try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
+                    "select menu item opened")) {
+                return new SelectModeButtons(mLauncher);
+            }
+        }
+    }
+
     /** Returns true if an item matching the given string is present in the menu. */
     public boolean hasMenuItem(String expectedMenuItemText) {
         UiObject2 menuItem = mLauncher.findObjectInContainer(mMenu, By.text(expectedMenuItemText));
diff --git a/tests/tapl/com/android/launcher3/tapl/Qsb.java b/tests/tapl/com/android/launcher3/tapl/Qsb.java
index fe2a63d..d67b8a3 100644
--- a/tests/tapl/com/android/launcher3/tapl/Qsb.java
+++ b/tests/tapl/com/android/launcher3/tapl/Qsb.java
@@ -118,9 +118,7 @@
         try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
                 "want to open search result page");
              LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
-            mLauncher.clickLauncherObject(waitForQsbObject());
-            // wait for the result rendering to complete
-            mLauncher.waitForIdle();
+            clickQsb();
             try (LauncherInstrumentation.Closable c2 = mLauncher.addContextLayer(
                     "clicked qsb to open search result page")) {
                 return createSearchResult();
@@ -128,6 +126,10 @@
         }
     }
 
+    protected void clickQsb() {
+        mLauncher.clickLauncherObject(waitForQsbObject());
+    }
+
     @Override
     public LauncherInstrumentation getLauncher() {
         return mLauncher;
diff --git a/tests/tapl/com/android/launcher3/tapl/SearchResultFromQsb.java b/tests/tapl/com/android/launcher3/tapl/SearchResultFromQsb.java
index f0a8aa2..8d3a631 100644
--- a/tests/tapl/com/android/launcher3/tapl/SearchResultFromQsb.java
+++ b/tests/tapl/com/android/launcher3/tapl/SearchResultFromQsb.java
@@ -15,6 +15,8 @@
  */
 package com.android.launcher3.tapl;
 
+import static com.android.launcher3.testing.shared.TestProtocol.NORMAL_STATE_ORDINAL;
+
 import android.widget.TextView;
 
 import androidx.test.uiautomator.By;
@@ -39,7 +41,7 @@
 
     /** Find the app from search results with app name. */
     public AppIcon findAppIcon(String appName) {
-        UiObject2 icon = mLauncher.waitForLauncherObject(By.clazz(TextView.class).text(appName));
+        UiObject2 icon = mLauncher.waitForLauncherObject(AppIcon.getAppIconSelector(appName));
         return createAppIcon(icon);
     }
 
@@ -87,7 +89,7 @@
                              + (tapRight ? "right" : "left"))) {
             final UiObject2 allAppsBottomSheet =
                     mLauncher.waitForLauncherObject(BOTTOM_SHEET_RES_ID);
-            mLauncher.touchOutsideContainer(allAppsBottomSheet, tapRight);
+            tapOutside(tapRight, allAppsBottomSheet);
             try (LauncherInstrumentation.Closable tapped = mLauncher.addContextLayer(
                     "tapped outside AllApps bottom sheet")) {
                 verifyVisibleContainerOnDismiss();
@@ -95,6 +97,13 @@
         }
     }
 
+    protected void tapOutside(boolean tapRight, UiObject2 allAppsBottomSheet) {
+        mLauncher.runToState(
+                () -> mLauncher.touchOutsideContainer(allAppsBottomSheet, tapRight),
+                NORMAL_STATE_ORDINAL,
+                "tappig outside");
+    }
+
     protected void verifyVisibleContainerOnDismiss() {
         mLauncher.getWorkspace();
     }
diff --git a/tests/tapl/com/android/launcher3/tapl/SearchResultFromTaskbarQsb.java b/tests/tapl/com/android/launcher3/tapl/SearchResultFromTaskbarQsb.java
index 00291a3..f4b4a91 100644
--- a/tests/tapl/com/android/launcher3/tapl/SearchResultFromTaskbarQsb.java
+++ b/tests/tapl/com/android/launcher3/tapl/SearchResultFromTaskbarQsb.java
@@ -50,4 +50,9 @@
     protected void verifyVisibleContainerOnDismiss() {
         mLauncher.getLaunchedAppState().assertTaskbarVisible();
     }
+
+    @Override
+    protected void tapOutside(boolean tapRight, UiObject2 allAppsBottomSheet) {
+        mLauncher.touchOutsideContainer(allAppsBottomSheet, tapRight);
+    }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/SelectModeButtons.java b/tests/tapl/com/android/launcher3/tapl/SelectModeButtons.java
index e1b73a4..e2bc17b 100644
--- a/tests/tapl/com/android/launcher3/tapl/SelectModeButtons.java
+++ b/tests/tapl/com/android/launcher3/tapl/SelectModeButtons.java
@@ -16,9 +16,17 @@
 
 package com.android.launcher3.tapl;
 
+import static android.view.KeyEvent.KEYCODE_ESCAPE;
+
+import static com.android.launcher3.testing.shared.TestProtocol.OVERVIEW_STATE_ORDINAL;
+
 import androidx.annotation.NonNull;
 import androidx.test.uiautomator.UiObject2;
 
+import com.android.launcher3.testing.shared.TestProtocol;
+
+import java.util.regex.Pattern;
+
 /**
  * View containing select mode buttons
  */
@@ -26,9 +34,14 @@
     private final UiObject2 mSelectModeButtons;
     private final LauncherInstrumentation mLauncher;
 
-    SelectModeButtons(UiObject2 selectModeButtons,
-            LauncherInstrumentation launcherInstrumentation) {
-        mSelectModeButtons = selectModeButtons;
+    private static final Pattern EVENT_ALT_ESC_DOWN = Pattern.compile(
+            "Key event: KeyEvent.*?action=ACTION_DOWN.*?keyCode=KEYCODE_ESCAPE.*?metaState=0");
+    private static final Pattern EVENT_ALT_ESC_UP = Pattern.compile(
+            "Key event: KeyEvent.*?action=ACTION_UP.*?keyCode=KEYCODE_ESCAPE.*?metaState=0");
+
+
+    SelectModeButtons(LauncherInstrumentation launcherInstrumentation) {
+        mSelectModeButtons = launcherInstrumentation.waitForLauncherObject("select_mode_buttons");
         mLauncher = launcherInstrumentation;
     }
 
@@ -48,4 +61,25 @@
             }
         }
     }
+
+    /**
+     * Close select mode when ESC key is pressed.
+     * @return The Overview
+     */
+    @NonNull
+    public Overview dismissByEscKey() {
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
+            mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_ALT_ESC_DOWN);
+            mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_ALT_ESC_UP);
+            mLauncher.runToState(
+                    () -> mLauncher.getDevice().pressKeyCode(KEYCODE_ESCAPE),
+                    OVERVIEW_STATE_ORDINAL,
+                    "pressing Esc");
+            try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                    "pressed esc key")) {
+                return new Overview(mLauncher);
+            }
+        }
+    }
+
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/Taskbar.java b/tests/tapl/com/android/launcher3/tapl/Taskbar.java
index a202c53..e6315f3 100644
--- a/tests/tapl/com/android/launcher3/tapl/Taskbar.java
+++ b/tests/tapl/com/android/launcher3/tapl/Taskbar.java
@@ -153,7 +153,7 @@
         return By.clazz(TextView.class).text("");
     }
 
-    private Rect getVisibleBounds() {
+    public Rect getVisibleBounds() {
         return mLauncher.waitForSystemLauncherObject(TASKBAR_RES_ID).getVisibleBounds();
     }
 
diff --git a/tests/tapl/com/android/launcher3/tapl/TaskbarAppIcon.java b/tests/tapl/com/android/launcher3/tapl/TaskbarAppIcon.java
index 064f80c..d05c112 100644
--- a/tests/tapl/com/android/launcher3/tapl/TaskbarAppIcon.java
+++ b/tests/tapl/com/android/launcher3/tapl/TaskbarAppIcon.java
@@ -68,6 +68,7 @@
 
     @Override
     protected boolean launcherStopsAfterLaunch() {
+        // false because if taskbar is showing then launcher is already stopped.
         return false;
     }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/Widgets.java b/tests/tapl/com/android/launcher3/tapl/Widgets.java
index 105bc3b..6387b05 100644
--- a/tests/tapl/com/android/launcher3/tapl/Widgets.java
+++ b/tests/tapl/com/android/launcher3/tapl/Widgets.java
@@ -19,6 +19,7 @@
 import static com.android.launcher3.tapl.LauncherInstrumentation.WAIT_TIME_MS;
 import static com.android.launcher3.tapl.LauncherInstrumentation.log;
 
+import android.annotation.Nullable;
 import android.graphics.Rect;
 
 import androidx.test.uiautomator.By;
@@ -114,7 +115,13 @@
                 .getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD);
     }
 
+    /** Get widget with supplied text. */
     public Widget getWidget(String labelText) {
+        return getWidget(labelText, null);
+    }
+
+    /** Get widget with supplied text and app package */
+    public Widget getWidget(String labelText, @Nullable String testAppWidgetPackage) {
         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
              LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
                      "getting widget " + labelText + " in widgets list")) {
@@ -124,7 +131,8 @@
             mLauncher.assertTrue("Widgets container didn't become scrollable",
                     fullWidgetsPicker.wait(Until.scrollable(true), WAIT_TIME_MS));
 
-            final UiObject2 widgetsContainer = findTestAppWidgetsTableContainer();
+            final UiObject2 widgetsContainer =
+                    findTestAppWidgetsTableContainer(testAppWidgetPackage);
             mLauncher.assertTrue("Can't locate widgets list for the test app: "
                             + mLauncher.getLauncherPackageName(),
                     widgetsContainer != null);
@@ -180,14 +188,22 @@
         return searchBar;
     }
 
-    /** Finds the widgets list of this test app from the collapsed full widgets picker. */
-    private UiObject2 findTestAppWidgetsTableContainer() {
+    /**
+     * Finds the widgets list of this test app or supplied test app package from the collapsed full
+     * widgets picker.
+     */
+    private UiObject2 findTestAppWidgetsTableContainer(@Nullable String testAppWidgetPackage) {
         final BySelector headerSelector = By.res(mLauncher.getLauncherPackageName(),
                 "widgets_list_header");
         final BySelector widgetPickerSelector = By.res(mLauncher.getLauncherPackageName(),
                 "container");
-        final BySelector targetAppSelector = By.clazz("android.widget.TextView").text(
-                mLauncher.getContext().getPackageName());
+
+        String packageName =  mLauncher.getContext().getPackageName();
+        final BySelector targetAppSelector = By
+                .clazz("android.widget.TextView")
+                .text((testAppWidgetPackage == null || testAppWidgetPackage.isEmpty())
+                                ? packageName
+                                : testAppWidgetPackage);
         final BySelector widgetsContainerSelector = By.res(mLauncher.getLauncherPackageName(),
                 "widgets_table");
 
diff --git a/tests/tapl/com/android/launcher3/tapl/Workspace.java b/tests/tapl/com/android/launcher3/tapl/Workspace.java
index f8fa00c..a911de4 100644
--- a/tests/tapl/com/android/launcher3/tapl/Workspace.java
+++ b/tests/tapl/com/android/launcher3/tapl/Workspace.java
@@ -121,7 +121,10 @@
              LauncherInstrumentation.Closable c =
                      mLauncher.addContextLayer("want to open all apps search")) {
             verifyActiveContainer();
-            mLauncher.getDevice().pressKeyCode(KEYCODE_META_RIGHT);
+            mLauncher.runToState(
+                    () -> mLauncher.getDevice().pressKeyCode(KEYCODE_META_RIGHT),
+                    ALL_APPS_STATE_ORDINAL,
+                    "pressing keyboard shortcut");
             try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
                     "pressed meta key")) {
                 return new HomeAllApps(mLauncher);
@@ -192,16 +195,24 @@
     }
 
     /**
-     * Ensures that workspace is scrollable. If it's not, drags an icon icons from hotseat to the
-     * second screen.
+     * Ensures that workspace is scrollable. If it's not, drags a chrome app icon from hotseat
+     * to the second screen.
      */
     public void ensureWorkspaceIsScrollable() {
+        ensureWorkspaceIsScrollable("Chrome");
+    }
+
+    /**
+     * Ensures that workspace is scrollable. If it's not, drags an icon of a given app name from
+     * hotseat to the second screen.
+     */
+    public void ensureWorkspaceIsScrollable(String appName) {
         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
             final UiObject2 workspace = verifyActiveContainer();
             if (!isWorkspaceScrollable(workspace)) {
                 try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
                         "dragging icon to a second page of workspace to make it scrollable")) {
-                    dragIcon(workspace, getHotseatAppIcon("Chrome"), pagesPerScreen());
+                    dragIcon(workspace, getHotseatAppIcon(appName), pagesPerScreen());
                     verifyActiveContainer();
                 }
             }
@@ -450,7 +461,12 @@
     }
 
     /** Returns the index of the current page */
-    private static int geCurrentPage(LauncherInstrumentation launcher) {
+    public int getCurrentPage() {
+        return getCurrentPage(mLauncher);
+    }
+
+    /** Returns the index of the current page */
+    private static int getCurrentPage(LauncherInstrumentation launcher) {
         return launcher.getTestInfo(TestProtocol.REQUEST_WORKSPACE_CURRENT_PAGE_INDEX).getInt(
                 TestProtocol.TEST_INFO_RESPONSE_FIELD);
     }
@@ -637,7 +653,7 @@
             Point currentPosition, int destinationWorkspaceIndex, int y) {
         final long downTime = SystemClock.uptimeMillis();
         int displayX = launcher.getRealDisplaySize().x;
-        int currentPage = Workspace.geCurrentPage(launcher);
+        int currentPage = Workspace.getCurrentPage(launcher);
         int counter = 0;
         while (currentPage != destinationWorkspaceIndex) {
             counter++;
@@ -656,7 +672,7 @@
                     () -> launcher.movePointer(finalDragStart, screenEdge, DEFAULT_DRAG_STEPS,
                             true, downTime, downTime, true,
                             LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER));
-            currentPage = Workspace.geCurrentPage(launcher);
+            currentPage = Workspace.getCurrentPage(launcher);
             currentPosition = screenEdge;
         }
         return currentPosition;
@@ -695,10 +711,9 @@
      */
     public void flingForward() {
         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
-            final UiObject2 workspace = verifyActiveContainer();
-            mLauncher.scroll(workspace, Direction.RIGHT,
-                    new Rect(0, 0, mLauncher.getEdgeSensitivityWidth() + 1, 0),
-                    FLING_STEPS, false);
+            Rect workspaceBounds = mLauncher.getVisibleBounds(verifyActiveContainer());
+            mLauncher.pointerScroll(
+                    workspaceBounds.centerX(), workspaceBounds.centerY(), Direction.RIGHT);
             verifyActiveContainer();
         }
     }
@@ -709,10 +724,9 @@
      */
     public void flingBackward() {
         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
-            final UiObject2 workspace = verifyActiveContainer();
-            mLauncher.scroll(workspace, Direction.LEFT,
-                    new Rect(mLauncher.getEdgeSensitivityWidth() + 1, 0, 0, 0),
-                    FLING_STEPS, false);
+            Rect workspaceBounds = mLauncher.getVisibleBounds(verifyActiveContainer());
+            mLauncher.pointerScroll(
+                    workspaceBounds.centerX(), workspaceBounds.centerY(), Direction.LEFT);
             verifyActiveContainer();
         }
     }