Merge "Remove ScreenRecord for a closed bug" into main
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
index 3dcf2b4..7d39bf8 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
@@ -342,10 +342,10 @@
             mBubbleBarViewController.showOverflow(update.showOverflow);
         }
 
-        BubbleBarBubble bubbleToSelect = null;
         if (update.addedBubble != null) {
             mBubbles.put(update.addedBubble.getKey(), update.addedBubble);
         }
+        BubbleBarBubble bubbleToSelect = null;
         if (update.selectedBubbleKey != null) {
             if (mSelectedBubble == null
                     || !update.selectedBubbleKey.equals(mSelectedBubble.getKey())) {
@@ -363,7 +363,7 @@
                 && update.removedBubbles.isEmpty()
                 && !mBubbles.isEmpty()) {
             // A bubble was added from the overflow (& now it's empty / not showing)
-            mBubbleBarViewController.removeOverflowAndAddBubble(update.addedBubble);
+            mBubbleBarViewController.removeOverflowAndAddBubble(update.addedBubble, bubbleToSelect);
         } else if (update.addedBubble != null && update.removedBubbles.size() == 1) {
             // we're adding and removing a bubble at the same time. handle this as a single update.
             RemovedBubble removedBubble = update.removedBubbles.get(0);
@@ -371,7 +371,8 @@
             boolean showOverflow = update.showOverflowChanged && update.showOverflow;
             if (bubbleToRemove != null) {
                 mBubbleBarViewController.addBubbleAndRemoveBubble(update.addedBubble,
-                        bubbleToRemove, isExpanding, suppressAnimation, showOverflow);
+                        bubbleToRemove, bubbleToSelect, isExpanding, suppressAnimation,
+                        showOverflow);
             } else {
                 mBubbleBarViewController.addBubble(update.addedBubble, isExpanding,
                         suppressAnimation, bubbleToSelect);
@@ -387,7 +388,7 @@
                     if (bubble != null && overflowNeedsToBeAdded) {
                         // First removal, show the overflow
                         overflowNeedsToBeAdded = false;
-                        mBubbleBarViewController.addOverflowAndRemoveBubble(bubble);
+                        mBubbleBarViewController.addOverflowAndRemoveBubble(bubble, bubbleToSelect);
                     } else if (bubble != null) {
                         mBubbleBarViewController.removeBubble(bubble);
                     } else {
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
index aa6ad25..2d4d279 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
@@ -741,35 +741,32 @@
 
     /** Add a new bubble and remove an old bubble from the bubble bar. */
     public void addBubbleAndRemoveBubble(BubbleView addedBubble, BubbleView removedBubble,
-            Runnable onEndRunnable) {
+            @Nullable BubbleView bubbleToSelect, Runnable onEndRunnable) {
         FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams((int) mIconSize, (int) mIconSize,
                 Gravity.LEFT);
-        boolean isOverflowSelected =
-                mSelectedBubbleView != null && mSelectedBubbleView.isOverflow();
-        boolean removingOverflow = removedBubble.isOverflow();
-        boolean addingOverflow = addedBubble.isOverflow();
-
+        int addedIndex = addedBubble.isOverflow() ? getChildCount() : 0;
         if (!isExpanded()) {
             removeView(removedBubble);
-            int index = addingOverflow ? getChildCount() : 0;
-            addView(addedBubble, index, lp);
+            addView(addedBubble, addedIndex, lp);
             if (onEndRunnable != null) {
                 onEndRunnable.run();
             }
             return;
         }
-        int index = addingOverflow ? getChildCount() : 0;
         addedBubble.setScaleX(0f);
         addedBubble.setScaleY(0f);
-        addView(addedBubble, index, lp);
-
-        if (isOverflowSelected && removingOverflow) {
-            // The added bubble will be selected
-            mSelectedBubbleView = addedBubble;
-        }
-        int indexOfSelectedBubble = indexOfChild(mSelectedBubbleView);
+        addView(addedBubble, addedIndex, lp);
+        int indexOfCurrentSelectedBubble = indexOfChild(mSelectedBubbleView);
         int indexOfBubbleToRemove = indexOfChild(removedBubble);
-
+        int indexOfNewlySelectedBubble = bubbleToSelect == null
+                ? indexOfCurrentSelectedBubble : indexOfChild(bubbleToSelect);
+        // Since removed bubble is kept till the end of the animation we should check if there are
+        // more than one bubble. In such a case the bar will remain open without the selected bubble
+        if (mSelectedBubbleView == removedBubble
+                && bubbleToSelect == null
+                && getBubbleChildCount() > 1) {
+            Log.w(TAG, "Remove the currently selected bubble without selecting a new one.");
+        }
         mBubbleAnimator = new BubbleAnimator(mIconSize, mExpandedBarIconsSpacing,
                 getChildCount(), mBubbleBarLocation.isOnLeft(isLayoutRtl()));
         BubbleAnimator.Listener listener = new BubbleAnimator.Listener() {
@@ -802,8 +799,8 @@
                 invalidate();
             }
         };
-        mBubbleAnimator.animateNewAndRemoveOld(indexOfSelectedBubble, indexOfBubbleToRemove,
-                listener);
+        mBubbleAnimator.animateNewAndRemoveOld(indexOfCurrentSelectedBubble,
+                indexOfNewlySelectedBubble, indexOfBubbleToRemove, addedIndex, listener);
     }
 
     @Override
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
index 84243f5..0b627d2 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -843,11 +843,12 @@
     }
 
     /** Adds a new bubble and removes an old bubble at the same time. */
-    public void addBubbleAndRemoveBubble(BubbleBarBubble addedBubble,
-            BubbleBarBubble removedBubble, boolean isExpanding, boolean suppressAnimation,
-            boolean addOverflowToo) {
+    public void addBubbleAndRemoveBubble(BubbleBarBubble addedBubble, BubbleBarBubble removedBubble,
+            @Nullable BubbleBarBubble bubbleToSelect, boolean isExpanding,
+            boolean suppressAnimation, boolean addOverflowToo) {
+        BubbleView bubbleToSelectView = bubbleToSelect == null ? null : bubbleToSelect.getView();
         mBarView.addBubbleAndRemoveBubble(addedBubble.getView(), removedBubble.getView(),
-                addOverflowToo ? () -> showOverflow(true) : null);
+                bubbleToSelectView, addOverflowToo ? () -> showOverflow(true) : null);
         addedBubble.getView().setOnClickListener(mBubbleClickListener);
         addedBubble.getView().setController(mBubbleViewController);
         removedBubble.getView().setController(null);
@@ -878,22 +879,26 @@
     }
 
     /** Adds the overflow view to the bubble bar while animating a view away. */
-    public void addOverflowAndRemoveBubble(BubbleBarBubble removedBubble) {
+    public void addOverflowAndRemoveBubble(BubbleBarBubble removedBubble,
+            @Nullable BubbleBarBubble bubbleToSelect) {
         if (mOverflowAdded) return;
         mOverflowAdded = true;
+        BubbleView bubbleToSelectView = bubbleToSelect == null ? null : bubbleToSelect.getView();
         mBarView.addBubbleAndRemoveBubble(mOverflowBubble.getView(), removedBubble.getView(),
-                null /* onEndRunnable */);
+                bubbleToSelectView, null /* onEndRunnable */);
         mOverflowBubble.getView().setOnClickListener(mBubbleClickListener);
         mOverflowBubble.getView().setController(mBubbleViewController);
         removedBubble.getView().setController(null);
     }
 
     /** Removes the overflow view to the bubble bar while animating a view in. */
-    public void removeOverflowAndAddBubble(BubbleBarBubble addedBubble) {
+    public void removeOverflowAndAddBubble(BubbleBarBubble addedBubble,
+            @Nullable BubbleBarBubble bubbleToSelect) {
         if (!mOverflowAdded) return;
         mOverflowAdded = false;
+        BubbleView bubbleToSelectView = bubbleToSelect == null ? null : bubbleToSelect.getView();
         mBarView.addBubbleAndRemoveBubble(addedBubble.getView(), mOverflowBubble.getView(),
-                null /* onEndRunnable */);
+                bubbleToSelectView, null /* onEndRunnable */);
         addedBubble.getView().setOnClickListener(mBubbleClickListener);
         addedBubble.getView().setController(mBubbleViewController);
         mOverflowBubble.getView().setController(null);
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimator.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimator.kt
index 944e806..26d6ccc 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimator.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimator.kt
@@ -18,6 +18,8 @@
 
 import androidx.core.animation.Animator
 import androidx.core.animation.ValueAnimator
+import kotlin.math.max
+import kotlin.math.min
 
 /**
  * Animates individual bubbles within the bubble bar while the bubble bar is expanded.
@@ -70,14 +72,18 @@
 
     fun animateNewAndRemoveOld(
         selectedBubbleIndex: Int,
+        newlySelectedBubbleIndex: Int,
         removedBubbleIndex: Int,
+        addedBubbleIndex: Int,
         listener: Listener,
     ) {
         animator = createAnimator(listener)
         state =
             State.AddingAndRemoving(
                 selectedBubbleIndex = selectedBubbleIndex,
+                newlySelectedBubbleIndex = newlySelectedBubbleIndex,
                 removedBubbleIndex = removedBubbleIndex,
+                addedBubbleIndex = addedBubbleIndex,
             )
         animator.start()
     }
@@ -137,6 +143,7 @@
                 getBubbleTranslationXWhileAddingBubbleAtLimit(
                     bubbleIndex = bubbleIndex,
                     removedBubbleIndex = state.removedBubbleIndex,
+                    addedBubbleIndex = state.addedBubbleIndex,
                     addedBubbleScale = animator.animatedFraction,
                     removedBubbleScale = 1 - animator.animatedFraction,
                 )
@@ -187,34 +194,25 @@
             State.Idle -> 0f
             is State.AddingBubble -> getArrowPositionWhenAddingBubble(state)
             is State.RemovingBubble -> getArrowPositionWhenRemovingBubble(state)
-            is State.AddingAndRemoving -> {
-                // we never remove the selected bubble, so the arrow stays pointing to its center
-                val tx =
-                    getBubbleTranslationXWhileAddingBubbleAtLimit(
-                        bubbleIndex = state.selectedBubbleIndex,
-                        removedBubbleIndex = state.removedBubbleIndex,
-                        addedBubbleScale = animator.animatedFraction,
-                        removedBubbleScale = 1 - animator.animatedFraction,
-                    )
-                tx + iconSize / 2f
-            }
+            is State.AddingAndRemoving -> getArrowPositionWhenAddingAndRemovingBubble(state)
         }
     }
 
     private fun getArrowPositionWhenAddingBubble(state: State.AddingBubble): Float {
         val scale = animator.animatedFraction
-        var tx = getBubbleTranslationXWhileScalingBubble(
-            bubbleIndex = state.selectedBubbleIndex,
-            scalingBubbleIndex = 0,
-            bubbleScale = scale
-        ) + iconSize / 2f
+        var tx =
+            getBubbleTranslationXWhileScalingBubble(
+                bubbleIndex = state.selectedBubbleIndex,
+                scalingBubbleIndex = 0,
+                bubbleScale = scale,
+            ) + iconSize / 2f
         if (state.newlySelectedBubbleIndex != null) {
             val selectedBubbleScale = if (state.newlySelectedBubbleIndex == 0) scale else 1f
             val finalTx =
                 getBubbleTranslationXWhileScalingBubble(
                     bubbleIndex = state.newlySelectedBubbleIndex,
                     scalingBubbleIndex = 0,
-                    bubbleScale = 1f,
+                    bubbleScale = scale,
                 ) + iconSize * selectedBubbleScale / 2f
             tx += (finalTx - tx) * animator.animatedFraction
         }
@@ -266,6 +264,46 @@
             }
         }
 
+    private fun getArrowPositionWhenAddingAndRemovingBubble(state: State.AddingAndRemoving): Float {
+        // The bubble bar keeps constant width while adding and removing bubble. So we just need to
+        // find selected bubble arrow position on the animation start and newly selected bubble
+        // arrow position on the animation end interpolating the arrow between these positions
+        // during the animation.
+        // The indexes in the state are provided for the bubble bar containing all bubbles. So for
+        // certain circumstances indexes should be adjusted.
+        // When animation is started added bubble has zero scale as well as removed bubble when the
+        // animation is ended, so for both cases we should compute translation as it is one less
+        // bubble.
+        val bubbleCountOnEnd = bubbleCount - 1
+        var selectedIndex = state.selectedBubbleIndex
+        // We only need to adjust the selected index if added bubble was added before the selected.
+        if (selectedIndex > state.addedBubbleIndex) {
+            // If the selectedIndex is higher index than the added bubble index, we need to reduce
+            // selectedIndex by one because the added bubble  has zero scale when animation is
+            // started.
+            selectedIndex--
+        }
+        var newlySelectedIndex = state.newlySelectedBubbleIndex
+        // We only need to adjust newlySelectedIndex if removed bubble was removed before the newly
+        // selected bubble.
+        if (newlySelectedIndex > state.removedBubbleIndex) {
+            // If the newlySelectedIndex is higher index than the removed bubble index, we need to
+            // reduce newlySelectedIndex by one because the removed bubble has zero scale when
+            // animation is ended.
+            newlySelectedIndex--
+        }
+        val iconAndSpacing: Float = iconSize + expandedBarIconSpacing
+        val startTx = getBubblesToTheLeft(selectedIndex, bubbleCountOnEnd) * iconAndSpacing
+        val endTx = getBubblesToTheLeft(newlySelectedIndex, bubbleCountOnEnd) * iconAndSpacing
+        val tx = startTx + (endTx - startTx) * animator.animatedFraction
+        return tx + iconSize / 2f
+    }
+
+    private fun getBubblesToTheLeft(bubbleIndex: Int, bubbleCount: Int = this.bubbleCount): Int =
+        // when bar is on left the index - 0 corresponds to the right - most bubble and when the
+        // bubble bar is on the right - 0 corresponds to the left - most bubble.
+        if (onLeft) bubbleCount - bubbleIndex - 1 else bubbleIndex
+
     /**
      * Returns the translation X for the bubble at index {@code bubbleIndex} when the bubble bar is
      * expanded and a bubble is animating in or out.
@@ -290,6 +328,7 @@
                     // the bar is on the left and the current bubble is to the right of the scaling
                     // bubble so account for its scale
                     (bubbleCount - bubbleIndex - 2 + bubbleScale) * iconAndSpacing
+
                 bubbleIndex == scalingBubbleIndex -> {
                     // the bar is on the left and this is the scaling bubble
                     val totalIconSize = (bubbleCount - bubbleIndex - 1) * iconSize
@@ -299,6 +338,7 @@
                     val scaledSpace = bubbleScale * expandedBarIconSpacing
                     totalIconSize + totalSpacing + scaledSpace + pivotAdjustment
                 }
+
                 else ->
                     // the bar is on the left and the scaling bubble is on the right. the current
                     // bubble is unaffected by the scaling bubble
@@ -310,10 +350,12 @@
                     // the bar is on the right and the scaling bubble is on the right. the current
                     // bubble is unaffected by the scaling bubble
                     iconAndSpacing * bubbleIndex
+
                 bubbleIndex == scalingBubbleIndex ->
                     // the bar is on the right, and this is the animating bubble. it only needs to
                     // be adjusted for the scaling pivot.
                     iconAndSpacing * bubbleIndex + pivotAdjustment
+
                 else ->
                     // the bar is on the right and the scaling bubble is on the left so account for
                     // its scale
@@ -325,61 +367,67 @@
     private fun getBubbleTranslationXWhileAddingBubbleAtLimit(
         bubbleIndex: Int,
         removedBubbleIndex: Int,
+        addedBubbleIndex: Int,
         addedBubbleScale: Float,
         removedBubbleScale: Float,
     ): Float {
         val iconAndSpacing = iconSize + expandedBarIconSpacing
         // the bubbles are scaling from the center, so we need to adjust their translation so
         // that the distance to the adjacent bubble scales at the same rate.
-        val addedBubblePivotAdjustment = -(1 - addedBubbleScale) * iconSize / 2f
-        val removedBubblePivotAdjustment = -(1 - removedBubbleScale) * iconSize / 2f
+        val addedBubblePivotAdjustment = (addedBubbleScale - 1) * iconSize / 2f
+        val removedBubblePivotAdjustment = (removedBubbleScale - 1) * iconSize / 2f
 
-        return if (onLeft) {
-            // this is how many bubbles there are to the left of the current bubble.
-            // when the bubble bar is on the right the added bubble is the right-most bubble so it
-            // doesn't affect the translation of any other bubble.
-            // when the removed bubble is to the left of the current bubble, we need to subtract it
-            // from bubblesToLeft and use removedBubbleScale instead when calculating the
-            // translation.
-            val bubblesToLeft = bubbleCount - bubbleIndex - 1
-            when {
-                bubbleIndex == 0 ->
-                    // this is the added bubble and it's the right-most bubble. account for all the
-                    // other bubbles -- including the removed bubble -- and adjust for the added
-                    // bubble pivot.
-                    (bubblesToLeft - 1 + removedBubbleScale) * iconAndSpacing +
-                        addedBubblePivotAdjustment
-                bubbleIndex < removedBubbleIndex ->
+        val minAddedRemovedIndex = min(addedBubbleIndex, removedBubbleIndex)
+        val maxAddedRemovedIndex = max(addedBubbleIndex, removedBubbleIndex)
+        val isBetweenAddedAndRemoved =
+            bubbleIndex in (minAddedRemovedIndex + 1)..<maxAddedRemovedIndex
+        val isRemovedBubbleToLeftOfAddedBubble = onLeft == addedBubbleIndex < removedBubbleIndex
+        val bubblesToLeft = getBubblesToTheLeft(bubbleIndex)
+        return when {
+            isBetweenAddedAndRemoved -> {
+                if (isRemovedBubbleToLeftOfAddedBubble) {
                     // the removed bubble is to the left so account for it
                     (bubblesToLeft - 1 + removedBubbleScale) * iconAndSpacing
-                bubbleIndex == removedBubbleIndex -> {
-                    // this is the removed bubble. all the bubbles to the left are at full scale
-                    // but we need to scale the spacing between the removed bubble and the bubble to
-                    // its left because the removed bubble disappears towards the left side
+                } else {
+                    // the added bubble is to the left so account for it
+                    (bubblesToLeft - 1 + addedBubbleScale) * iconAndSpacing
+                }
+            }
+
+            bubbleIndex == addedBubbleIndex -> {
+                if (isRemovedBubbleToLeftOfAddedBubble) {
+                    // the removed bubble is to the left so account for it
+                    (bubblesToLeft - 1 + removedBubbleScale) * iconAndSpacing
+                } else {
+                    // it's the left-most scaling bubble, all bubbles on the left are at full scale
+                    bubblesToLeft * iconAndSpacing
+                } + addedBubblePivotAdjustment
+            }
+
+            bubbleIndex == removedBubbleIndex -> {
+                if (isRemovedBubbleToLeftOfAddedBubble) {
+                    // All the bubbles to the left are at full scale, but we need to scale the
+                    // spacing between the removed bubble and the bubble next to it
                     val totalIconSize = bubblesToLeft * iconSize
                     val totalSpacing =
                         (bubblesToLeft - 1 + removedBubbleScale) * expandedBarIconSpacing
-                    totalIconSize + totalSpacing + removedBubblePivotAdjustment
-                }
-                else ->
-                    // both added and removed bubbles are to the right so they don't affect the tx
-                    bubblesToLeft * iconAndSpacing
+                    totalIconSize + totalSpacing
+                } else {
+                    // The added bubble is to the left, so account for it
+                    (bubblesToLeft - 1 + addedBubbleScale) * iconAndSpacing
+                } + removedBubblePivotAdjustment
             }
-        } else {
-            when {
-                bubbleIndex == 0 -> addedBubblePivotAdjustment // we always add bubbles at index 0
-                bubbleIndex < removedBubbleIndex ->
-                    // the bar is on the right and the removed bubble is on the right. the current
-                    // bubble is unaffected by the removed bubble. only need to factor in the added
-                    // bubble's scale.
-                    iconAndSpacing * (bubbleIndex - 1 + addedBubbleScale)
-                bubbleIndex == removedBubbleIndex ->
-                    // the bar is on the right, and this is the animating bubble.
-                    iconAndSpacing * (bubbleIndex - 1 + addedBubbleScale) +
-                        removedBubblePivotAdjustment
-                else ->
-                    // both the added and the removed bubbles are to the left of the current bubble
-                    iconAndSpacing * (bubbleIndex - 2 + addedBubbleScale + removedBubbleScale)
+
+            else -> {
+                // if bubble index is on the right side of the animated bubbles, we need to deduct
+                // one, since both the added and the removed bubbles takes a single place
+                val onTheRightOfAnimatedBubbles =
+                    if (onLeft) {
+                        bubbleIndex < minAddedRemovedIndex
+                    } else {
+                        bubbleIndex > maxAddedRemovedIndex
+                    }
+                (bubblesToLeft - if (onTheRightOfAnimatedBubbles) 1 else 0) * iconAndSpacing
             }
         }
     }
@@ -413,10 +461,17 @@
             val removingLastRemainingBubble: Boolean,
         ) : State
 
-        // TODO add index where bubble is being added, and index for newly selected bubble
         /** A new bubble is being added and an old bubble is being removed from the bubble bar. */
-        data class AddingAndRemoving(val selectedBubbleIndex: Int, val removedBubbleIndex: Int) :
-            State
+        data class AddingAndRemoving(
+            /** The index of the selected bubble. */
+            val selectedBubbleIndex: Int,
+            /** The index of the newly selected bubble. */
+            val newlySelectedBubbleIndex: Int,
+            /** The index of the bubble being removed. */
+            val removedBubbleIndex: Int,
+            /** The index of the added bubble. */
+            val addedBubbleIndex: Int,
+        ) : State
     }
 
     /** Callbacks for the animation. */
diff --git a/quickstep/src/com/android/launcher3/uioverrides/flags/DevOptionsUiHelper.kt b/quickstep/src/com/android/launcher3/uioverrides/flags/DevOptionsUiHelper.kt
index 417bb74..7c09e9a 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/flags/DevOptionsUiHelper.kt
+++ b/quickstep/src/com/android/launcher3/uioverrides/flags/DevOptionsUiHelper.kt
@@ -17,8 +17,6 @@
 package com.android.launcher3.uioverrides.flags
 
 import android.app.PendingIntent
-import android.app.blob.BlobHandle.createWithSha256
-import android.app.blob.BlobStoreManager
 import android.content.Context
 import android.content.IIntentReceiver
 import android.content.IIntentSender.Stub
@@ -29,10 +27,8 @@
 import android.net.Uri
 import android.os.Bundle
 import android.os.IBinder
-import android.os.ParcelFileDescriptor.AutoCloseOutputStream
 import android.provider.DeviceConfig
 import android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS
-import android.provider.Settings.Secure
 import android.text.Html
 import android.util.AttributeSet
 import android.view.inputmethod.EditorInfo
@@ -44,33 +40,16 @@
 import androidx.preference.PreferenceGroup
 import androidx.preference.PreferenceViewHolder
 import androidx.preference.SwitchPreference
-import com.android.launcher3.AutoInstallsLayout
 import com.android.launcher3.ExtendedEditText
 import com.android.launcher3.LauncherAppState
 import com.android.launcher3.LauncherPrefs
-import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP
-import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT
-import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION
-import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET
-import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT
-import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_FOLDER
-import com.android.launcher3.LauncherSettings.Settings.LAYOUT_DIGEST_LABEL
-import com.android.launcher3.LauncherSettings.Settings.LAYOUT_DIGEST_TAG
-import com.android.launcher3.LauncherSettings.Settings.LAYOUT_PROVIDER_KEY
-import com.android.launcher3.LauncherSettings.Settings.createBlobProviderKey
 import com.android.launcher3.R
-import com.android.launcher3.model.data.FolderInfo
-import com.android.launcher3.model.data.ItemInfo
-import com.android.launcher3.model.data.LauncherAppWidgetInfo
-import com.android.launcher3.pm.UserCache
 import com.android.launcher3.proxy.ProxyActivityStarter
 import com.android.launcher3.secondarydisplay.SecondaryDisplayLauncher
-import com.android.launcher3.shortcuts.ShortcutKey
 import com.android.launcher3.uioverrides.plugins.PluginManagerWrapperImpl
 import com.android.launcher3.util.Executors.MAIN_EXECUTOR
-import com.android.launcher3.util.Executors.MODEL_EXECUTOR
 import com.android.launcher3.util.Executors.ORDERED_BG_EXECUTOR
-import com.android.launcher3.util.LauncherLayoutBuilder
+import com.android.launcher3.util.LayoutImportExportHelper
 import com.android.launcher3.util.OnboardingPrefs.ALL_APPS_VISITED_COUNT
 import com.android.launcher3.util.OnboardingPrefs.HOME_BOUNCE_COUNT
 import com.android.launcher3.util.OnboardingPrefs.HOME_BOUNCE_SEEN
@@ -80,14 +59,12 @@
 import com.android.launcher3.util.OnboardingPrefs.TASKBAR_SEARCH_EDU_SEEN
 import com.android.launcher3.util.PluginManagerWrapper
 import com.android.launcher3.util.StartActivityParams
-import com.android.launcher3.util.UserIconInfo
 import com.android.quickstep.util.DeviceConfigHelper
 import com.android.quickstep.util.DeviceConfigHelper.Companion.NAMESPACE_LAUNCHER
 import com.android.quickstep.util.DeviceConfigHelper.DebugInfo
 import com.android.systemui.shared.plugins.PluginEnabler
 import com.android.systemui.shared.plugins.PluginPrefs
-import java.io.OutputStreamWriter
-import java.security.MessageDigest
+import java.nio.charset.StandardCharsets
 import java.util.Locale
 import java.util.concurrent.Executor
 
@@ -421,26 +398,12 @@
             title = "Export"
             intent =
                 createUriPickerIntent(ACTION_CREATE_DOCUMENT, MAIN_EXECUTOR) { uri ->
-                    model.enqueueModelUpdateTask { _, dataModel, _ ->
-                        val builder = LauncherLayoutBuilder()
-                        dataModel.workspaceItems.forEach { info ->
-                            val loc =
-                                when (info.container) {
-                                    CONTAINER_DESKTOP ->
-                                        builder.atWorkspace(info.cellX, info.cellY, info.screenId)
-                                    CONTAINER_HOTSEAT -> builder.atHotseat(info.screenId)
-                                    else -> return@forEach
-                                }
-                            loc.addItem(info)
-                        }
-                        dataModel.appWidgets.forEach { info ->
-                            builder.atWorkspace(info.cellX, info.cellY, info.screenId).addItem(info)
-                        }
-
+                    LayoutImportExportHelper.exportModelDbAsXml(context) { layoutXml ->
                         context.contentResolver.openOutputStream(uri).use { os ->
-                            builder.build(OutputStreamWriter(os))
+                            val bytes: ByteArray =
+                                layoutXml.toByteArray(StandardCharsets.UTF_8) // Encode to UTF-8
+                            os?.write(bytes)
                         }
-
                         MAIN_EXECUTOR.execute {
                             Toast.makeText(context, "File saved", Toast.LENGTH_LONG).show()
                         }
@@ -458,66 +421,12 @@
                         resolver.openInputStream(uri).use { stream ->
                             stream?.readAllBytes() ?: return@createUriPickerIntent
                         }
-
-                    val digest = MessageDigest.getInstance("SHA-256").digest(data)
-                    val handle = createWithSha256(digest, LAYOUT_DIGEST_LABEL, 0, LAYOUT_DIGEST_TAG)
-                    val blobManager = context.getSystemService(BlobStoreManager::class.java)!!
-
-                    blobManager.openSession(blobManager.createSession(handle)).use { session ->
-                        AutoCloseOutputStream(session.openWrite(0, -1)).use { it.write(data) }
-                        session.allowPublicAccess()
-
-                        session.commit(ORDERED_BG_EXECUTOR) {
-                            Secure.putString(
-                                resolver,
-                                LAYOUT_PROVIDER_KEY,
-                                createBlobProviderKey(digest),
-                            )
-
-                            MODEL_EXECUTOR.submit { model.modelDbController.createEmptyDB() }.get()
-                            MAIN_EXECUTOR.submit { model.forceReload() }.get()
-                            MODEL_EXECUTOR.submit {}.get()
-                            Secure.putString(resolver, LAYOUT_PROVIDER_KEY, null)
-                        }
-                    }
+                    LayoutImportExportHelper.importModelFromXml(context, data)
                 }
             category.addPreference(this)
         }
     }
 
-    private fun LauncherLayoutBuilder.ItemTarget.addItem(info: ItemInfo) {
-        val userType: String? =
-            when (UserCache.INSTANCE.get(context).getUserInfo(info.user).type) {
-                UserIconInfo.TYPE_WORK -> AutoInstallsLayout.USER_TYPE_WORK
-                UserIconInfo.TYPE_CLONED -> AutoInstallsLayout.USER_TYPE_CLONED
-                else -> null
-            }
-        when (info.itemType) {
-            ITEM_TYPE_APPLICATION ->
-                info.targetComponent?.let { c -> putApp(c.packageName, c.className, userType) }
-            ITEM_TYPE_DEEP_SHORTCUT ->
-                ShortcutKey.fromItemInfo(info).let { key ->
-                    putShortcut(key.packageName, key.id, userType)
-                }
-            ITEM_TYPE_FOLDER ->
-                (info as FolderInfo).let { folderInfo ->
-                    putFolder(folderInfo.title?.toString() ?: "").also { folderBuilder ->
-                        folderInfo.getContents().forEach { folderContent ->
-                            folderBuilder.addItem(folderContent)
-                        }
-                    }
-                }
-            ITEM_TYPE_APPWIDGET ->
-                putWidget(
-                    (info as LauncherAppWidgetInfo).providerName.packageName,
-                    info.providerName.className,
-                    info.spanX,
-                    info.spanY,
-                    userType,
-                )
-        }
-    }
-
     private fun createUriPickerIntent(
         action: String,
         executor: Executor,
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index e27b607..124be41 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -100,7 +100,6 @@
 import android.widget.Toast;
 import android.window.DesktopModeFlags;
 import android.window.PictureInPictureSurfaceTransaction;
-import android.window.TransitionInfo;
 import android.window.WindowAnimationState;
 
 import androidx.annotation.NonNull;
@@ -957,10 +956,10 @@
 
     @Override
     public void onRecentsAnimationStart(RecentsAnimationController controller,
-            RecentsAnimationTargets targets, TransitionInfo transitionInfo) {
-        super.onRecentsAnimationStart(controller, targets, transitionInfo);
+            RecentsAnimationTargets targets) {
+        super.onRecentsAnimationStart(controller, targets);
         if (targets.hasDesktopTasks(mContext)) {
-            mRemoteTargetHandles = mTargetGluer.assignTargetsForDesktop(targets, transitionInfo);
+            mRemoteTargetHandles = mTargetGluer.assignTargetsForDesktop(targets);
         } else {
             int untrimmedAppCount = mRemoteTargetHandles.length;
             mRemoteTargetHandles = mTargetGluer.assignTargetsForSplitScreen(targets);
diff --git a/quickstep/src/com/android/quickstep/GestureState.java b/quickstep/src/com/android/quickstep/GestureState.java
index e0fa77a..cfbcf0a 100644
--- a/quickstep/src/com/android/quickstep/GestureState.java
+++ b/quickstep/src/com/android/quickstep/GestureState.java
@@ -33,7 +33,6 @@
 import android.os.SystemClock;
 import android.view.MotionEvent;
 import android.view.RemoteAnimationTarget;
-import android.window.TransitionInfo;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -492,7 +491,7 @@
 
     @Override
     public void onRecentsAnimationStart(RecentsAnimationController controller,
-            RecentsAnimationTargets targets, TransitionInfo info) {
+            RecentsAnimationTargets targets) {
         mStateCallback.setState(STATE_RECENTS_ANIMATION_STARTED);
     }
 
diff --git a/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt b/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt
index 66f307c..089706f 100644
--- a/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt
+++ b/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt
@@ -23,7 +23,6 @@
 import android.os.Trace
 import android.util.Log
 import android.view.View
-import android.window.TransitionInfo
 import androidx.annotation.BinderThread
 import androidx.annotation.UiThread
 import androidx.annotation.VisibleForTesting
@@ -370,7 +369,6 @@
                 override fun onRecentsAnimationStart(
                     controller: RecentsAnimationController,
                     targets: RecentsAnimationTargets,
-                    transitionInfo: TransitionInfo,
                 ) {
                     Log.d(TAG, "recents animation started: $command")
                     updateRecentsViewFocus(command)
diff --git a/quickstep/src/com/android/quickstep/OverviewComponentObserver.java b/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
index 64c1129..1f95c41 100644
--- a/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
+++ b/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
@@ -268,7 +268,7 @@
      *
      * @return the overview intent
      */
-    Intent getOverviewIntentIgnoreSysUiState() {
+    public Intent getOverviewIntentIgnoreSysUiState() {
         return mIsDefaultHome ? mMyHomeIntent : mOverviewIntent;
     }
 
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java b/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java
index 87bf81c..8fc1a78 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java
@@ -26,7 +26,6 @@
 import android.os.Bundle;
 import android.util.ArraySet;
 import android.view.RemoteAnimationTarget;
-import android.window.TransitionInfo;
 
 import androidx.annotation.BinderThread;
 import androidx.annotation.NonNull;
@@ -94,7 +93,7 @@
             RemoteAnimationTarget[] appTargets, Rect homeContentInsets,
             Rect minimizedHomeBounds, Bundle extras) {
         onAnimationStart(controller, appTargets, new RemoteAnimationTarget[0],
-                homeContentInsets, minimizedHomeBounds, extras, /* transitionInfo= */ null);
+                homeContentInsets, minimizedHomeBounds, extras);
     }
 
     // Called only in R+ platform
@@ -102,8 +101,7 @@
     public final void onAnimationStart(RecentsAnimationControllerCompat animationController,
             RemoteAnimationTarget[] appTargets,
             RemoteAnimationTarget[] wallpaperTargets,
-            Rect homeContentInsets, Rect minimizedHomeBounds, Bundle extras,
-            TransitionInfo transitionInfo) {
+            Rect homeContentInsets, Rect minimizedHomeBounds, Bundle extras) {
         long appCount = Arrays.stream(appTargets)
                 .filter(app -> app.mode == MODE_CLOSING)
                 .count();
@@ -143,7 +141,7 @@
             Utilities.postAsyncCallback(MAIN_EXECUTOR.getHandler(), () -> {
                 ActiveGestureProtoLogProxy.logOnRecentsAnimationStart(targets.apps.length);
                 for (RecentsAnimationListener listener : getListeners()) {
-                    listener.onRecentsAnimationStart(mController, targets, transitionInfo);
+                    listener.onRecentsAnimationStart(mController, targets);
                 }
             });
         }
@@ -207,7 +205,7 @@
      */
     public interface RecentsAnimationListener {
         default void onRecentsAnimationStart(RecentsAnimationController controller,
-                RecentsAnimationTargets targets, TransitionInfo transitionInfo) {}
+                RecentsAnimationTargets targets) {}
 
         /**
          * Callback from the system when the recents animation is canceled. {@param thumbnailData}
diff --git a/quickstep/src/com/android/quickstep/RemoteTargetGluer.java b/quickstep/src/com/android/quickstep/RemoteTargetGluer.java
index 8edbacb..89337e5 100644
--- a/quickstep/src/com/android/quickstep/RemoteTargetGluer.java
+++ b/quickstep/src/com/android/quickstep/RemoteTargetGluer.java
@@ -24,7 +24,6 @@
 import android.graphics.Rect;
 import android.util.Log;
 import android.view.RemoteAnimationTarget;
-import android.window.TransitionInfo;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -214,8 +213,7 @@
      * Similar to {@link #assignTargets(RemoteAnimationTargets)}, except this creates distinct
      * transform params per app in {@code targets.apps} list.
      */
-    public RemoteTargetHandle[] assignTargetsForDesktop(
-            RemoteAnimationTargets targets, TransitionInfo transitionInfo) {
+    public RemoteTargetHandle[] assignTargetsForDesktop(RemoteAnimationTargets targets) {
         resizeRemoteTargetHandles(targets);
 
         for (int i = 0; i < mRemoteTargetHandles.length; i++) {
@@ -224,7 +222,6 @@
                     .filter(target -> target.taskId != primaryTaskTarget.taskId).toList();
             mRemoteTargetHandles[i].mTransformParams.setTargetSet(
                     createRemoteAnimationTargetsForTarget(targets, excludeTargets));
-            mRemoteTargetHandles[i].mTransformParams.setTransitionInfo(transitionInfo);
             mRemoteTargetHandles[i].mTaskViewSimulator.setPreview(primaryTaskTarget, null);
         }
         return mRemoteTargetHandles;
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.kt b/quickstep/src/com/android/quickstep/SystemUiProxy.kt
index a7405a0..75694af 100644
--- a/quickstep/src/com/android/quickstep/SystemUiProxy.kt
+++ b/quickstep/src/com/android/quickstep/SystemUiProxy.kt
@@ -45,7 +45,6 @@
 import android.window.RemoteTransition
 import android.window.TaskSnapshot
 import android.window.TransitionFilter
-import android.window.TransitionInfo
 import androidx.annotation.MainThread
 import androidx.annotation.VisibleForTesting
 import androidx.annotation.WorkerThread
@@ -1175,7 +1174,6 @@
             homeContentInsets: Rect?,
             minimizedHomeBounds: Rect?,
             extras: Bundle?,
-            transitionInfo: TransitionInfo?,
         ) =
             listener.onAnimationStart(
                 RecentsAnimationControllerCompat(controller),
@@ -1188,7 +1186,6 @@
                     // https://developer.android.com/guide/components/aidl#Bundles
                     classLoader = SplitBounds::class.java.classLoader
                 },
-                transitionInfo,
             )
 
         override fun onAnimationCanceled(taskIds: IntArray?, taskSnapshots: Array<TaskSnapshot>?) =
diff --git a/quickstep/src/com/android/quickstep/TaskAnimationManager.java b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
index cea7931..731c256 100644
--- a/quickstep/src/com/android/quickstep/TaskAnimationManager.java
+++ b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
@@ -20,7 +20,6 @@
 import static com.android.launcher3.Flags.enableHandleDelayedGestureCallbacks;
 import static com.android.launcher3.Flags.enableScalingRevealHomeAnimation;
 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.NavigationMode.NO_BUTTON;
 import static com.android.quickstep.GestureState.GestureEndTarget.RECENTS;
 import static com.android.quickstep.GestureState.STATE_END_TARGET_ANIMATION_FINISHED;
@@ -36,7 +35,6 @@
 import android.os.SystemProperties;
 import android.util.Log;
 import android.view.RemoteAnimationTarget;
-import android.window.TransitionInfo;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -54,7 +52,6 @@
 import com.android.quickstep.util.SystemUiFlagUtils;
 import com.android.quickstep.views.RecentsView;
 import com.android.systemui.shared.recents.model.ThumbnailData;
-import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.QuickStepContract;
 import com.android.systemui.shared.system.TaskStackChangeListener;
 import com.android.systemui.shared.system.TaskStackChangeListeners;
@@ -70,7 +67,6 @@
     private RecentsAnimationController mController;
     private RecentsAnimationCallbacks mCallbacks;
     private RecentsAnimationTargets mTargets;
-    private TransitionInfo mTransitionInfo;
     private RecentsAnimationDeviceState mDeviceState;
 
     // Temporary until we can hook into gesture state events
@@ -112,16 +108,6 @@
         return SystemUiProxy.INSTANCE.get(mCtx);
     }
 
-    /**
-     * Preloads the recents animation.
-     */
-    public void preloadRecentsAnimation(Intent intent) {
-        // Pass null animation handler to indicate this start is for preloading
-        UI_HELPER_EXECUTOR.execute(() -> {
-            ActivityManagerWrapper.getInstance().preloadRecentsActivity(intent);
-        });
-    }
-
     boolean shouldIgnoreMotionEvents() {
         return mShouldIgnoreMotionEvents;
     }
@@ -168,7 +154,7 @@
         mCallbacks.addListener(new RecentsAnimationCallbacks.RecentsAnimationListener() {
             @Override
             public void onRecentsAnimationStart(RecentsAnimationController controller,
-                    RecentsAnimationTargets targets, TransitionInfo transitionInfo) {
+                    RecentsAnimationTargets targets) {
                 if (enableHandleDelayedGestureCallbacks() && mRecentsAnimationStartPending) {
                     ActiveGestureProtoLogProxy.logStartRecentsAnimationCallback(
                             "onRecentsAnimationStart");
@@ -182,7 +168,6 @@
                 }
                 mController = controller;
                 mTargets = targets;
-                mTransitionInfo = transitionInfo;
                 // TODO(b/236226779): We can probably get away w/ setting mLastAppearedTaskTargets
                 //  to all appeared targets directly vs just looking at running ones
                 int[] runningTaskIds = mLastGestureState.getRunningTaskIds(targets.apps.length > 1);
@@ -451,7 +436,7 @@
     public void notifyRecentsAnimationState(
             RecentsAnimationCallbacks.RecentsAnimationListener listener) {
         if (isRecentsAnimationRunning()) {
-            listener.onRecentsAnimationStart(mController, mTargets, mTransitionInfo);
+            listener.onRecentsAnimationStart(mController, mTargets);
         }
         // TODO: Do we actually need to report canceled/finished?
     }
@@ -491,7 +476,6 @@
         mController = null;
         mCallbacks = null;
         mTargets = null;
-        mTransitionInfo = null;
         mLastGestureState = null;
         mLastAppearedTaskTargets = null;
     }
diff --git a/quickstep/src/com/android/quickstep/TaskViewUtils.java b/quickstep/src/com/android/quickstep/TaskViewUtils.java
index 3e90374..dec36cf 100644
--- a/quickstep/src/com/android/quickstep/TaskViewUtils.java
+++ b/quickstep/src/com/android/quickstep/TaskViewUtils.java
@@ -189,8 +189,7 @@
             RemoteTargetGluer gluer = new RemoteTargetGluer(v.getContext(),
                     recentsView.getSizeStrategy(), targets, forDesktop);
             if (forDesktop) {
-                remoteTargetHandles =
-                        gluer.assignTargetsForDesktop(targets, /* transitionInfo=*/ null);
+                remoteTargetHandles = gluer.assignTargetsForDesktop(targets);
             } else if (v.containsMultipleTasks()) {
                 remoteTargetHandles = gluer.assignTargetsForSplitScreen(targets,
                         ((GroupedTaskView) v).getSplitBoundsConfig());
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index a06029b..efd9a56 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -49,7 +49,6 @@
 import android.os.IBinder;
 import android.os.Looper;
 import android.os.SystemClock;
-import android.os.Trace;
 import android.util.Log;
 import android.view.Choreographer;
 import android.view.InputDevice;
@@ -68,7 +67,6 @@
 import com.android.launcher3.LauncherPrefs;
 import com.android.launcher3.anim.AnimatedFloat;
 import com.android.launcher3.desktop.DesktopAppLaunchTransitionManager;
-import com.android.launcher3.provider.RestoreDbTask;
 import com.android.launcher3.statehandlers.DesktopVisibilityController;
 import com.android.launcher3.statemanager.StatefulActivity;
 import com.android.launcher3.taskbar.TaskbarActivityContext;
@@ -96,6 +94,7 @@
 import com.android.quickstep.util.ActiveGestureLog.CompoundString;
 import com.android.quickstep.util.ActiveGestureProtoLogProxy;
 import com.android.quickstep.util.ActiveTrackpadList;
+import com.android.quickstep.util.ActivityPreloadUtil;
 import com.android.quickstep.util.ContextualSearchInvoker;
 import com.android.quickstep.util.ContextualSearchStateManager;
 import com.android.quickstep.views.RecentsViewContainer;
@@ -183,7 +182,7 @@
                         recentTasks, launcherUnlockAnimationController, backAnimation, desktopMode,
                         unfoldTransition, dragAndDrop);
                 tis.initInputMonitor("TISBinder#onInitialize()");
-                tis.preloadOverview(true /* fromInit */);
+                ActivityPreloadUtil.preloadOverviewForTIS(tis, true /* fromInit */);
             }));
         }
 
@@ -350,16 +349,6 @@
             ));
         }
 
-        /**
-         * Preloads the Overview activity.
-         * <p>
-         * This method should only be used when the All Set page of the SUW is reached to safely
-         * preload the Launcher for the SUW first reveal.
-         */
-        public void preloadOverviewForSUWAllSet() {
-            executeForTouchInteractionService(tis -> tis.preloadOverview(false, true));
-        }
-
         @Override
         public void onRotationProposal(int rotation, boolean isValid) {
             executeForTaskbarManager(taskbarManager ->
@@ -1035,47 +1024,6 @@
         }
     }
 
-    private void preloadOverview(boolean fromInit) {
-        Trace.beginSection("preloadOverview(fromInit=" + fromInit + ")");
-        preloadOverview(fromInit, false);
-        Trace.endSection();
-    }
-
-    private void preloadOverview(boolean fromInit, boolean forSUWAllSet) {
-        if (!LockedUserState.get(this).isUserUnlocked()) {
-            return;
-        }
-
-        if (mDeviceState.isButtonNavMode() && !mOverviewComponentObserver.isHomeAndOverviewSame()) {
-            // Prevent the overview from being started before the real home on first boot.
-            return;
-        }
-
-        if ((RestoreDbTask.isPending(this) && !forSUWAllSet)
-                || !mDeviceState.isUserSetupComplete()) {
-            // Preloading while a restore is pending may cause launcher to start the restore
-            // too early.
-            return;
-        }
-
-        final BaseContainerInterface containerInterface =
-                mOverviewComponentObserver.getContainerInterface();
-        final Intent overviewIntent = new Intent(
-                mOverviewComponentObserver.getOverviewIntentIgnoreSysUiState());
-        if (containerInterface.getCreatedContainer() != null && fromInit) {
-            // The activity has been created before the initialization of overview service. It is
-            // usually happens when booting or launcher is the top activity, so we should already
-            // have the latest state.
-            return;
-        }
-
-        // TODO(b/258022658): Remove temporary logging.
-        Log.i(TAG, "preloadOverview: forSUWAllSet=" + forSUWAllSet
-                + ", isHomeAndOverviewSame=" + mOverviewComponentObserver.isHomeAndOverviewSame());
-        ActiveGestureProtoLogProxy.logPreloadRecentsAnimation();
-        mTaskAnimationManager.preloadRecentsAnimation(overviewIntent);
-    }
-
     @Override
     public void onConfigurationChanged(Configuration newConfig) {
         if (!LockedUserState.get(this).isUserUnlocked()) {
@@ -1103,7 +1051,7 @@
             return;
         }
 
-        preloadOverview(false /* fromInit */);
+        ActivityPreloadUtil.preloadOverviewForTIS(this, false /* fromInit */);
     }
 
     private static boolean isTablet(Configuration config) {
diff --git a/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowSwipeHandler.java b/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowSwipeHandler.java
index 973fb2f..12bae53 100644
--- a/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowSwipeHandler.java
+++ b/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowSwipeHandler.java
@@ -49,7 +49,6 @@
 import android.view.SurfaceControl;
 import android.view.SurfaceControl.Transaction;
 import android.view.animation.Interpolator;
-import android.window.TransitionInfo;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -125,8 +124,8 @@
 
     @Override
     public void onRecentsAnimationStart(RecentsAnimationController controller,
-            RecentsAnimationTargets targets, TransitionInfo transitionInfo) {
-        super.onRecentsAnimationStart(controller, targets, transitionInfo);
+            RecentsAnimationTargets targets) {
+        super.onRecentsAnimationStart(controller, targets);
         initTransformParams();
     }
 
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java
index 503b900..01f5522 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java
@@ -37,7 +37,6 @@
 import android.view.MotionEvent;
 import android.view.RemoteAnimationTarget;
 import android.view.VelocityTracker;
-import android.window.TransitionInfo;
 
 import com.android.app.animation.Interpolators;
 import com.android.launcher3.R;
@@ -250,7 +249,7 @@
 
     @Override
     public void onRecentsAnimationStart(RecentsAnimationController controller,
-            RecentsAnimationTargets targets, TransitionInfo transitionInfo) {
+            RecentsAnimationTargets targets) {
         mRecentsAnimationController = controller;
         mTransformParams.setTargetSet(targets);
         applyTransform();
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/ProgressDelegateInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/ProgressDelegateInputConsumer.java
index c91bebe..6dcb7bc 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/ProgressDelegateInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/ProgressDelegateInputConsumer.java
@@ -28,7 +28,6 @@
 import android.content.Intent;
 import android.graphics.Point;
 import android.view.MotionEvent;
-import android.window.TransitionInfo;
 
 import com.android.launcher3.anim.AnimatedFloat;
 import com.android.launcher3.anim.AnimatorListeners;
@@ -173,7 +172,7 @@
 
     @Override
     public void onRecentsAnimationStart(RecentsAnimationController controller,
-            RecentsAnimationTargets targets, TransitionInfo transitionInfo) {
+            RecentsAnimationTargets targets) {
         mRecentsAnimationController = controller;
         mStateCallback.setState(STATE_TARGET_RECEIVED);
     }
diff --git a/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java b/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java
index 4995e77..c986b88 100644
--- a/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java
+++ b/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java
@@ -73,6 +73,7 @@
 import com.android.quickstep.OverviewComponentObserver;
 import com.android.quickstep.OverviewComponentObserver.OverviewChangeListener;
 import com.android.quickstep.TouchInteractionService.TISBinder;
+import com.android.quickstep.util.ActivityPreloadUtil;
 import com.android.quickstep.util.LottieAnimationColorUtils;
 import com.android.quickstep.util.TISBindHelper;
 
@@ -202,6 +203,7 @@
 
         OverviewComponentObserver.INSTANCE.get(this)
                 .addOverviewChangeListener(mOverviewChangeListener);
+        ActivityPreloadUtil.preloadOverviewForSUWAllSet(this);
     }
 
     private InvariantDeviceProfile getIDP() {
@@ -291,7 +293,6 @@
     private void onTISConnected(TISBinder binder) {
         setSetupUIVisible(isResumed());
         binder.setSwipeUpProxy(isResumed() ? this::createSwipeUpProxy : null);
-        binder.preloadOverviewForSUWAllSet();
         TaskbarManager taskbarManager = binder.getTaskbarManager();
         if (taskbarManager != null) {
             mLauncherStartAnim = taskbarManager.createLauncherStartFromSuwAnim(MAX_SWIPE_DURATION);
@@ -299,10 +300,7 @@
     }
 
     private void onOverviewTargetChange(boolean isHomeAndOverviewSame) {
-        TISBinder binder = mTISBindHelper.getBinder();
-        if (binder != null) {
-            binder.preloadOverviewForSUWAllSet();
-        }
+        ActivityPreloadUtil.preloadOverviewForSUWAllSet(this);
     }
 
     @Override
diff --git a/quickstep/src/com/android/quickstep/util/ActivityPreloadUtil.kt b/quickstep/src/com/android/quickstep/util/ActivityPreloadUtil.kt
new file mode 100644
index 0000000..47b39db
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/ActivityPreloadUtil.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.util
+
+import android.content.Context
+import android.content.Intent
+import android.os.Trace
+import com.android.launcher3.provider.RestoreDbTask
+import com.android.launcher3.util.Executors
+import com.android.launcher3.util.LockedUserState
+import com.android.quickstep.OverviewComponentObserver
+import com.android.quickstep.RecentsAnimationDeviceState
+import com.android.systemui.shared.system.ActivityManagerWrapper
+
+/** Utility class for preloading overview */
+object ActivityPreloadUtil {
+
+    @JvmStatic
+    fun preloadOverviewForSUWAllSet(ctx: Context) {
+        preloadOverview(ctx, fromInit = false, forSUWAllSet = true)
+    }
+
+    @JvmStatic
+    fun preloadOverviewForTIS(ctx: Context, fromInit: Boolean) {
+        preloadOverview(ctx, fromInit = fromInit, forSUWAllSet = false)
+    }
+
+    private fun preloadOverview(ctx: Context, fromInit: Boolean, forSUWAllSet: Boolean) {
+        Trace.beginSection("preloadOverview(fromInit=$fromInit, forSUWAllSet=$forSUWAllSet)")
+
+        try {
+            if (!LockedUserState.get(ctx).isUserUnlocked) return
+
+            val deviceState = RecentsAnimationDeviceState.INSTANCE[ctx]
+            val overviewCompObserver = OverviewComponentObserver.INSTANCE[ctx]
+
+            // Prevent the overview from being started before the real home on first boot
+            if (deviceState.isButtonNavMode && !overviewCompObserver.isHomeAndOverviewSame) return
+
+            // Preloading while a restore is pending may cause launcher to start the restore too
+            // early
+            if ((RestoreDbTask.isPending(ctx) && !forSUWAllSet) || !deviceState.isUserSetupComplete)
+                return
+
+            // The activity has been created before the initialization of overview service. It is
+            // usually happens when booting or launcher is the top activity, so we should already
+            // have the latest state.
+            if (fromInit && overviewCompObserver.containerInterface.createdContainer != null) return
+
+            ActiveGestureProtoLogProxy.logPreloadRecentsAnimation()
+            val overviewIntent = Intent(overviewCompObserver.overviewIntentIgnoreSysUiState)
+            Executors.UI_HELPER_EXECUTOR.execute {
+                ActivityManagerWrapper.getInstance().preloadRecentsActivity(overviewIntent)
+            }
+        } finally {
+            Trace.endSection()
+        }
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
index 202574b..c524286 100644
--- a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
+++ b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
@@ -678,7 +678,7 @@
         }
 
         @Override
-        public void startAnimation(IBinder transition, TransitionInfo transitionInfo,
+        public void startAnimation(IBinder transition, TransitionInfo info,
                 SurfaceControl.Transaction t,
                 IRemoteTransitionFinishedCallback finishedCallback) {
             final Runnable finishAdapter = () ->  {
@@ -708,7 +708,7 @@
                         null /* nonApps */,
                         mStateManager,
                         mDepthController,
-                        transitionInfo, t, () -> {
+                        info, t, () -> {
                             finishAdapter.run();
                             cleanup(true /*success*/);
                         },
@@ -920,7 +920,7 @@
 
             @Override
             public void onRecentsAnimationStart(RecentsAnimationController controller,
-                    RecentsAnimationTargets targets, TransitionInfo transitionInfo) {
+                    RecentsAnimationTargets targets) {
                 StatsLogManager.LauncherEvent launcherDesktopSplitEvent =
                         mSplitPosition == STAGE_POSITION_BOTTOM_OR_RIGHT ?
                         LAUNCHER_DESKTOP_MODE_SPLIT_RIGHT_BOTTOM :
diff --git a/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java b/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java
index 32c4b90..425c4fe 100644
--- a/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java
+++ b/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java
@@ -30,7 +30,6 @@
 import android.app.ActivityOptions;
 import android.graphics.Rect;
 import android.graphics.RectF;
-import android.window.TransitionInfo;
 
 import androidx.annotation.BinderThread;
 
@@ -115,7 +114,7 @@
 
         @Override
         public void onRecentsAnimationStart(RecentsAnimationController controller,
-                RecentsAnimationTargets targets, TransitionInfo transitionInfo) {
+                RecentsAnimationTargets targets) {
             mController.setInitialTaskSelect(mRunningTaskInfo,
                     mLeftOrTop ? STAGE_POSITION_TOP_OR_LEFT : STAGE_POSITION_BOTTOM_OR_RIGHT,
                     null /* itemInfo */,
diff --git a/quickstep/src/com/android/quickstep/util/TransformParams.java b/quickstep/src/com/android/quickstep/util/TransformParams.java
index bb88818..401eccc 100644
--- a/quickstep/src/com/android/quickstep/util/TransformParams.java
+++ b/quickstep/src/com/android/quickstep/util/TransformParams.java
@@ -19,12 +19,9 @@
 
 import android.util.FloatProperty;
 import android.view.RemoteAnimationTarget;
-import android.view.SurfaceControl;
-import android.window.TransitionInfo;
 
 import com.android.quickstep.RemoteAnimationTargets;
 import com.android.quickstep.util.SurfaceTransaction.SurfaceProperties;
-import com.android.window.flags.Flags;
 
 public class TransformParams {
 
@@ -59,7 +56,6 @@
     private float mTargetAlpha;
     private float mCornerRadius;
     private RemoteAnimationTargets mTargetSet;
-    private TransitionInfo mTransitionInfo;
     private SurfaceTransactionApplier mSyncTransactionApplier;
 
     private BuilderProxy mHomeBuilderProxy = BuilderProxy.ALWAYS_VISIBLE;
@@ -111,14 +107,6 @@
     }
 
     /**
-     * Provides the {@code TransitionInfo} of the transition that this transformation stems from.
-     */
-    public TransformParams setTransitionInfo(TransitionInfo transitionInfo) {
-        mTransitionInfo = transitionInfo;
-        return this;
-    }
-
-    /**
      * Sets the SyncRtSurfaceTransactionApplierCompat that will apply the SurfaceParams that
      * are computed based on these TransformParams.
      */
@@ -164,9 +152,6 @@
                 builder.setAlpha(getTargetAlpha());
             }
             targetProxy.onBuildTargetParams(builder, app, this);
-            // Override the corner radius for {@code app} with the leash used by Shell, so that it
-            // doesn't interfere with the window clip and corner radius applied here.
-            overrideChangeLeashCornerRadiusToZero(app, transaction.getTransaction());
         }
 
         // always put wallpaper layer to bottom.
@@ -178,28 +163,6 @@
         return transaction;
     }
 
-    private void overrideChangeLeashCornerRadiusToZero(
-            RemoteAnimationTarget app, SurfaceControl.Transaction transaction) {
-        if (!Flags.enableDesktopRecentsTransitionsCornersBugfix()) {
-            return;
-        }
-        SurfaceControl changeLeash = getChangeLeashForApp(app);
-        if (changeLeash != null) {
-            transaction.setCornerRadius(changeLeash, 0);
-        }
-    }
-
-    private SurfaceControl getChangeLeashForApp(RemoteAnimationTarget app) {
-        if (mTransitionInfo == null) return null;
-        for (TransitionInfo.Change change : mTransitionInfo.getChanges()) {
-            if (change.getTaskInfo() == null) continue;
-            if (change.getTaskInfo().taskId == app.taskId) {
-                return change.getLeash();
-            }
-        }
-        return null;
-    }
-
     // Pubic getters so outside packages can read the values.
 
     public float getProgress() {
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 9a4933a..045b823 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -5870,8 +5870,7 @@
         if (recentsAnimationTargets.hasDesktopTasks(mContext)) {
             gluer = new RemoteTargetGluer(getContext(), getSizeStrategy(), recentsAnimationTargets,
                     true /* forDesktop */);
-            mRemoteTargetHandles = gluer.assignTargetsForDesktop(
-                    recentsAnimationTargets, /* transitionInfo= */ null);
+            mRemoteTargetHandles = gluer.assignTargetsForDesktop(recentsAnimationTargets);
         } else {
             gluer = new RemoteTargetGluer(getContext(), getSizeStrategy(), recentsAnimationTargets,
                     false);
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimatorTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimatorTest.kt
index 3ca36ec..da362bd 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimatorTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimatorTest.kt
@@ -94,7 +94,9 @@
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
             bubbleAnimator.animateNewAndRemoveOld(
                 selectedBubbleIndex = 3,
-                removedBubbleIndex = 2,
+                newlySelectedBubbleIndex = 2,
+                removedBubbleIndex = 1,
+                addedBubbleIndex = 3,
                 listener,
             )
         }
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/AbsSwipeUpHandlerTestCase.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/AbsSwipeUpHandlerTestCase.java
index 542eb64..f16e193 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/AbsSwipeUpHandlerTestCase.java
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/AbsSwipeUpHandlerTestCase.java
@@ -364,7 +364,7 @@
 
     private void onRecentsAnimationStart(SWIPE_HANDLER absSwipeUpHandler) {
         runOnMainSync(() -> absSwipeUpHandler.onRecentsAnimationStart(
-                mRecentsAnimationController, mRecentsAnimationTargets, /* transitionInfo= */null));
+                mRecentsAnimationController, mRecentsAnimationTargets));
     }
 
     protected static void runOnMainSync(Runnable runnable) {
diff --git a/quickstep/tests/src/com/android/quickstep/TaplPrivateSpaceTest.java b/quickstep/tests/src/com/android/quickstep/TaplPrivateSpaceTest.java
index e065dba..88be752 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplPrivateSpaceTest.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplPrivateSpaceTest.java
@@ -165,7 +165,6 @@
     }
 
     @Test
-    @ScreenRecordRule.ScreenRecord // b/355466672
     public void testPrivateSpaceLockingBehaviour() throws IOException {
         assumeFalse(mLauncher.isTablet()); // b/367258373
         // Scroll to the bottom of All Apps
diff --git a/src/com/android/launcher3/LauncherProvider.java b/src/com/android/launcher3/LauncherProvider.java
index 6e2d357..a526b89 100644
--- a/src/com/android/launcher3/LauncherProvider.java
+++ b/src/com/android/launcher3/LauncherProvider.java
@@ -24,25 +24,38 @@
 import android.content.ContentProvider;
 import android.content.ContentUris;
 import android.content.ContentValues;
+import android.content.pm.PackageManager;
 import android.database.Cursor;
 import android.database.sqlite.SQLiteQueryBuilder;
 import android.net.Uri;
 import android.os.Binder;
+import android.os.Bundle;
 import android.os.Process;
 import android.text.TextUtils;
 import android.util.Log;
 
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.model.ModelDbController;
+import com.android.launcher3.util.LayoutImportExportHelper;
 import com.android.launcher3.widget.LauncherWidgetHolder;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
 import java.util.function.ToIntFunction;
 
 public class LauncherProvider extends ContentProvider {
     private static final String TAG = "LauncherProvider";
 
+    // Method API For Provider#call method.
+    private static final String METHOD_EXPORT_LAYOUT_XML = "EXPORT_LAYOUT_XML";
+    private static final String METHOD_IMPORT_LAYOUT_XML = "IMPORT_LAYOUT_XML";
+    private static final String KEY_RESULT = "KEY_RESULT";
+    private static final String KEY_LAYOUT = "KEY_LAYOUT";
+    private static final String SUCCESS = "success";
+    private static final String FAILURE = "failure";
+
     /**
      * $ adb shell dumpsys activity provider com.android.launcher3
      */
@@ -142,6 +155,43 @@
         return executeControllerTask(c -> c.update(args.table, values, args.where, args.args));
     }
 
+    @Override
+    public Bundle call(String method, String arg, Bundle extras) {
+        Bundle b = new Bundle();
+
+        // The caller must have the read or write permission for this content provider to
+        // access the "call" method at all. We also enforce the appropriate per-method permissions.
+        switch(method) {
+            case METHOD_EXPORT_LAYOUT_XML:
+                if (getContext().checkCallingOrSelfPermission(getReadPermission())
+                        != PackageManager.PERMISSION_GRANTED) {
+                    throw new SecurityException("Caller doesn't have read permission");
+                }
+
+                CompletableFuture<String> resultFuture = LayoutImportExportHelper.INSTANCE
+                        .exportModelDbAsXmlFuture(getContext());
+                try {
+                    b.putString(KEY_LAYOUT, resultFuture.get());
+                    b.putString(KEY_RESULT, SUCCESS);
+                } catch (ExecutionException | InterruptedException e) {
+                    b.putString(KEY_RESULT, FAILURE);
+                }
+                return b;
+
+            case METHOD_IMPORT_LAYOUT_XML:
+                if (getContext().checkCallingOrSelfPermission(getWritePermission())
+                        != PackageManager.PERMISSION_GRANTED) {
+                    throw new SecurityException("Caller doesn't have write permission");
+                }
+
+                LayoutImportExportHelper.INSTANCE.importModelFromXml(getContext(), arg);
+                b.putString(KEY_RESULT, SUCCESS);
+                return b;
+            default:
+                return null;
+        }
+    }
+
     private int executeControllerTask(ToIntFunction<ModelDbController> task) {
         if (Binder.getCallingPid() == Process.myPid()) {
             throw new IllegalArgumentException("Same process should call model directly");
diff --git a/src/com/android/launcher3/util/LayoutImportExportHelper.kt b/src/com/android/launcher3/util/LayoutImportExportHelper.kt
new file mode 100644
index 0000000..4033f60
--- /dev/null
+++ b/src/com/android/launcher3/util/LayoutImportExportHelper.kt
@@ -0,0 +1,139 @@
+/*
+ * 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.util
+
+import android.app.blob.BlobHandle.createWithSha256
+import android.app.blob.BlobStoreManager
+import android.content.Context
+import android.os.ParcelFileDescriptor.AutoCloseOutputStream
+import android.provider.Settings.Secure
+import com.android.launcher3.AutoInstallsLayout
+import com.android.launcher3.LauncherAppState
+import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP
+import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT
+import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION
+import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET
+import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT
+import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_FOLDER
+import com.android.launcher3.LauncherSettings.Settings.LAYOUT_DIGEST_LABEL
+import com.android.launcher3.LauncherSettings.Settings.LAYOUT_DIGEST_TAG
+import com.android.launcher3.LauncherSettings.Settings.LAYOUT_PROVIDER_KEY
+import com.android.launcher3.LauncherSettings.Settings.createBlobProviderKey
+import com.android.launcher3.model.data.FolderInfo
+import com.android.launcher3.model.data.ItemInfo
+import com.android.launcher3.model.data.LauncherAppWidgetInfo
+import com.android.launcher3.pm.UserCache
+import com.android.launcher3.shortcuts.ShortcutKey
+import com.android.launcher3.util.Executors.MAIN_EXECUTOR
+import com.android.launcher3.util.Executors.MODEL_EXECUTOR
+import com.android.launcher3.util.Executors.ORDERED_BG_EXECUTOR
+import java.nio.charset.StandardCharsets
+import java.security.MessageDigest
+import java.util.concurrent.CompletableFuture
+
+object LayoutImportExportHelper {
+    fun exportModelDbAsXmlFuture(context: Context): CompletableFuture<String> {
+        val future = CompletableFuture<String>()
+        exportModelDbAsXml(context) { xmlString -> future.complete(xmlString) }
+        return future
+    }
+
+    fun exportModelDbAsXml(context: Context, callback: (String) -> Unit) {
+        val model = LauncherAppState.getInstance(context).model
+
+        model.enqueueModelUpdateTask { _, dataModel, _ ->
+            val builder = LauncherLayoutBuilder()
+            dataModel.workspaceItems.forEach { info ->
+                val loc =
+                    when (info.container) {
+                        CONTAINER_DESKTOP ->
+                            builder.atWorkspace(info.cellX, info.cellY, info.screenId)
+
+                        CONTAINER_HOTSEAT -> builder.atHotseat(info.screenId)
+                        else -> return@forEach
+                    }
+                loc.addItem(context, info)
+            }
+            dataModel.appWidgets.forEach { info ->
+                builder.atWorkspace(info.cellX, info.cellY, info.screenId).addItem(context, info)
+            }
+
+            val layoutXml = builder.build()
+            callback(layoutXml)
+        }
+    }
+
+    fun importModelFromXml(context: Context, xmlString: String) {
+        importModelFromXml(context, xmlString.toByteArray(StandardCharsets.UTF_8))
+    }
+
+    fun importModelFromXml(context: Context, data: ByteArray) {
+        val model = LauncherAppState.getInstance(context).model
+
+        val digest = MessageDigest.getInstance("SHA-256").digest(data)
+        val handle = createWithSha256(digest, LAYOUT_DIGEST_LABEL, 0, LAYOUT_DIGEST_TAG)
+        val blobManager = context.getSystemService(BlobStoreManager::class.java)!!
+
+        val resolver = context.contentResolver
+
+        blobManager.openSession(blobManager.createSession(handle)).use { session ->
+            AutoCloseOutputStream(session.openWrite(0, -1)).use { it.write(data) }
+            session.allowPublicAccess()
+
+            session.commit(ORDERED_BG_EXECUTOR) {
+                Secure.putString(resolver, LAYOUT_PROVIDER_KEY, createBlobProviderKey(digest))
+
+                MODEL_EXECUTOR.submit { model.modelDbController.createEmptyDB() }.get()
+                MAIN_EXECUTOR.submit { model.forceReload() }.get()
+                MODEL_EXECUTOR.submit {}.get()
+                Secure.putString(resolver, LAYOUT_PROVIDER_KEY, null)
+            }
+        }
+    }
+
+    private fun LauncherLayoutBuilder.ItemTarget.addItem(context: Context, info: ItemInfo) {
+        val userType: String? =
+            when (UserCache.INSTANCE.get(context).getUserInfo(info.user).type) {
+                UserIconInfo.TYPE_WORK -> AutoInstallsLayout.USER_TYPE_WORK
+                UserIconInfo.TYPE_CLONED -> AutoInstallsLayout.USER_TYPE_CLONED
+                else -> null
+            }
+        when (info.itemType) {
+            ITEM_TYPE_APPLICATION ->
+                info.targetComponent?.let { c -> putApp(c.packageName, c.className, userType) }
+            ITEM_TYPE_DEEP_SHORTCUT ->
+                ShortcutKey.fromItemInfo(info).let { key ->
+                    putShortcut(key.packageName, key.id, userType)
+                }
+            ITEM_TYPE_FOLDER ->
+                (info as FolderInfo).let { folderInfo ->
+                    putFolder(folderInfo.title?.toString() ?: "").also { folderBuilder ->
+                        folderInfo.getContents().forEach { folderContent ->
+                            folderBuilder.addItem(context, folderContent)
+                        }
+                    }
+                }
+            ITEM_TYPE_APPWIDGET ->
+                putWidget(
+                    (info as LauncherAppWidgetInfo).providerName.packageName,
+                    info.providerName.className,
+                    info.spanX,
+                    info.spanY,
+                    userType,
+                )
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/src/com/android/launcher3/tablet/TaplIsTabletTest.kt b/tests/src/com/android/launcher3/tablet/TaplIsTabletTest.kt
deleted file mode 100644
index a6de607..0000000
--- a/tests/src/com/android/launcher3/tablet/TaplIsTabletTest.kt
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * 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.tablet
-
-import android.platform.test.rule.AllowedDevices
-import android.platform.test.rule.DeviceProduct
-import com.android.launcher3.Launcher
-import com.android.launcher3.ui.AbstractLauncherUiTest
-import junit.framework.TestCase.assertFalse
-import junit.framework.TestCase.assertTrue
-import org.junit.Test
-
-class TaplIsTabletTest : AbstractLauncherUiTest<Launcher>() {
-
-    /** Investigating b/366237798 by isolating and seeing flake rate of mLauncher.isTablet */
-    @Test
-    @AllowedDevices(
-        DeviceProduct.CF_FOLDABLE,
-        DeviceProduct.CF_TABLET,
-        DeviceProduct.TANGORPRO,
-        DeviceProduct.FELIX,
-        DeviceProduct.COMET,
-    )
-    fun isTabletShouldBeTrue() {
-        assertTrue(mLauncher.isTablet)
-    }
-
-    /** Investigating b/366237798 by isolating and seeing flake rate of mLauncher.isTablet */
-    @Test
-    @AllowedDevices(DeviceProduct.CF_PHONE, DeviceProduct.CHEETAH)
-    fun isTabletShouldBeFalse() {
-        assertFalse(mLauncher.isTablet)
-    }
-}
diff --git a/tests/src/com/android/launcher3/ui/widget/TaplAddWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/TaplAddWidgetTest.java
index 460ffc4..3e3e643 100644
--- a/tests/src/com/android/launcher3/ui/widget/TaplAddWidgetTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/TaplAddWidgetTest.java
@@ -51,7 +51,6 @@
     @Test
     @PortraitLandscape
     public void testDragIcon() throws Throwable {
-        mLauncher.enableDebugTracing(); // b/289161193
         commitTransactionAndLoadHome(new FavoriteItemsTransaction(mTargetContext));
 
         waitForLauncherCondition("Workspace didn't finish loading", l -> !l.isWorkspaceLoading());
@@ -72,7 +71,6 @@
                 TestUtil.DEFAULT_UI_TIMEOUT);
         assertNotNull("Widget not found on the workspace", widget);
         widget.launch(getAppPackageName());
-        mLauncher.disableDebugTracing(); // b/289161193
     }
 
     /**