Merge "[Launcher Jank] Move binder call of RotationChangeProvider to BG thread" into main
diff --git a/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java
index 98de415..62cc0bb 100644
--- a/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java
+++ b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java
@@ -93,6 +93,7 @@
      */
     public void unregisterSystemUiListener() {
         SystemUiProxy.INSTANCE.get(mLauncher).setDesktopTaskListener(null);
+        mDesktopTaskListener = null;
     }
 
     /**
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java
index 48fc7d1..39b4f77 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java
@@ -186,7 +186,7 @@
             @NonNull ImageView thumbnailView,
             @ColorInt int backgroundColor,
             @Nullable ThumbnailData thumbnailData) {
-        Bitmap bm = thumbnailData == null ? null : thumbnailData.thumbnail;
+        Bitmap bm = thumbnailData == null ? null : thumbnailData.getThumbnail();
 
         if (thumbnailView.getVisibility() != VISIBLE) {
             thumbnailView.setVisibility(VISIBLE);
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java
index 7f201b4..84874a9 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java
@@ -37,7 +37,7 @@
 import androidx.core.graphics.Insets;
 import androidx.core.view.WindowInsetsCompat;
 
-import com.android.app.viewcapture.SettingsAwareViewCapture;
+import com.android.app.viewcapture.ViewCaptureFactory;
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.shared.TestProtocol;
@@ -150,7 +150,7 @@
     protected void onAttachedToWindow() {
         super.onAttachedToWindow();
         getViewTreeObserver().addOnComputeInternalInsetsListener(mTaskbarInsetsComputer);
-        mViewCaptureCloseable = SettingsAwareViewCapture.getInstance(getContext())
+        mViewCaptureCloseable = ViewCaptureFactory.getInstance(getContext())
                 .startCapture(getRootView(), ".Taskbar");
     }
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
index f3ac1e4..43fe251 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
@@ -394,21 +394,24 @@
      * from the internal location that was used during bubble bar layout, translation values are
      * calculated to position the bar at the desired location.
      *
-     * @param initialTranslation initial bubble bar translation at the start of drag
+     * @param initialTranslation initial bubble translation inside the bar at the start of drag
      * @param location           desired location of the bubble bar when drag is released
      * @return point with x and y values representing translation on x and y-axis
      */
     public PointF getDraggedBubbleReleaseTranslation(PointF initialTranslation,
             BubbleBarLocation location) {
-        // Start with bubble bar translation
-        final PointF dragEndTranslation = new PointF(
-                getBubbleBarDragReleaseTranslation(initialTranslation, location));
-        // Apply individual bubble translation, as the order may have changed
-        int viewIndex = indexOfChild(mDraggedBubbleView);
-        dragEndTranslation.x += getExpandedBubbleTranslationX(viewIndex,
-                getChildCount(),
-                location.isOnLeft(isLayoutRtl()));
-        return dragEndTranslation;
+        float dragEndTranslationX = initialTranslation.x;
+        boolean newLocationOnLeft = location.isOnLeft(isLayoutRtl());
+        if (getBubbleBarLocation().isOnLeft(isLayoutRtl()) != newLocationOnLeft) {
+            // Calculate translationX based on bar and bubble translations
+            float bubbleBarTx = getBubbleBarDragReleaseTranslation(initialTranslation, location).x;
+            float bubbleTx =
+                    getExpandedBubbleTranslationX(
+                            indexOfChild(mDraggedBubbleView), getChildCount(), newLocationOnLeft);
+            dragEndTranslationX = bubbleBarTx + bubbleTx;
+        }
+        // translationY does not change during drag and can be reused
+        return new PointF(dragEndTranslationX, initialTranslation.y);
     }
 
     private float getDistanceFromOtherSide() {
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
index 0722e46..5f75b3b 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -506,12 +506,10 @@
 
     /**
      * Notifies SystemUI to expand the selected bubble when the bubble is released.
-     * @param bubbleView dragged bubble view
      */
-    public void onDragRelease(@NonNull BubbleView bubbleView, BubbleBarLocation location) {
-        if (bubbleView.getBubble() == null) return;
+    public void onDragRelease(BubbleBarLocation location) {
         // TODO(b/330585402): send new bubble bar bounds to shell for the animation
-        mSystemUiProxy.stopBubbleDrag(bubbleView.getBubble().getKey(), location);
+        mSystemUiProxy.stopBubbleDrag(location);
     }
 
     /**
@@ -556,7 +554,7 @@
      * @param bubble dismissed bubble item
      */
     public void onDismissBubbleWhileDragging(@NonNull BubbleBarItem bubble) {
-        mSystemUiProxy.removeBubble(bubble.getKey());
+        mSystemUiProxy.dragBubbleToDismiss(bubble.getKey());
     }
 
     /**
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java
index f4b393a..604ae89 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java
@@ -113,7 +113,7 @@
             @Override
             protected void onDragRelease() {
                 mBubblePinController.onDragEnd();
-                mBubbleBarViewController.onDragRelease(bubbleView, mReleasedLocation);
+                mBubbleBarViewController.onDragRelease(mReleasedLocation);
             }
 
             @Override
diff --git a/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayDragLayer.java b/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayDragLayer.java
index 9c3e8af..773b0b9 100644
--- a/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayDragLayer.java
+++ b/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayDragLayer.java
@@ -30,7 +30,7 @@
 
 import androidx.annotation.NonNull;
 
-import com.android.app.viewcapture.SettingsAwareViewCapture;
+import com.android.app.viewcapture.ViewCaptureFactory;
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.shared.TestProtocol;
@@ -59,7 +59,7 @@
     protected void onAttachedToWindow() {
         super.onAttachedToWindow();
         getViewTreeObserver().addOnComputeInternalInsetsListener(this);
-        mViewCaptureCloseable = SettingsAwareViewCapture.getInstance(getContext())
+        mViewCaptureCloseable = ViewCaptureFactory.getInstance(getContext())
                 .startCapture(getRootView(), ".TaskbarOverlay");
     }
 
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index bffee03..e379b2a 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -103,7 +103,7 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 
-import com.android.app.viewcapture.SettingsAwareViewCapture;
+import com.android.app.viewcapture.ViewCaptureFactory;
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Flags;
@@ -672,7 +672,7 @@
         addMultiWindowModeChangedListener(mDepthController);
         initUnfoldTransitionProgressProvider();
         if (FeatureFlags.CONTINUOUS_VIEW_TREE_CAPTURE.get()) {
-            mViewCapture = SettingsAwareViewCapture.getInstance(this).startCapture(getWindow());
+            mViewCapture = ViewCaptureFactory.getInstance(this).startCapture(getWindow());
         }
         getWindow().addPrivateFlags(PRIVATE_FLAG_OPTIMIZE_MEASURE);
         QuickstepOnboardingPrefs.setup(this);
diff --git a/quickstep/src/com/android/launcher3/uioverrides/SystemApiWrapper.kt b/quickstep/src/com/android/launcher3/uioverrides/SystemApiWrapper.kt
index 535b4c2..146ff3d 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/SystemApiWrapper.kt
+++ b/quickstep/src/com/android/launcher3/uioverrides/SystemApiWrapper.kt
@@ -40,7 +40,7 @@
 import com.android.quickstep.util.FadeOutRemoteTransition
 
 /** A wrapper for the hidden API calls */
-class SystemApiWrapper(context: Context?) : ApiWrapper(context) {
+open class SystemApiWrapper(context: Context?) : ApiWrapper(context) {
 
     override fun getPersons(si: ShortcutInfo) = si.persons ?: Utilities.EMPTY_PERSON_ARRAY
 
diff --git a/quickstep/src/com/android/launcher3/uioverrides/flags/DevOptionsUiHelper.kt b/quickstep/src/com/android/launcher3/uioverrides/flags/DevOptionsUiHelper.kt
index 3881e9a..dc6365b 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/flags/DevOptionsUiHelper.kt
+++ b/quickstep/src/com/android/launcher3/uioverrides/flags/DevOptionsUiHelper.kt
@@ -16,14 +16,28 @@
 
 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
 import android.content.Intent
+import android.content.Intent.ACTION_CREATE_DOCUMENT
+import android.content.Intent.ACTION_OPEN_DOCUMENT
 import android.content.pm.PackageManager
 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.util.Base64
+import android.util.Base64.NO_PADDING
+import android.util.Base64.NO_WRAP
 import android.view.inputmethod.EditorInfo
 import android.widget.TextView
 import android.widget.Toast
@@ -33,11 +47,32 @@
 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_KEY
+import com.android.launcher3.LauncherSettings.Settings.LAYOUT_DIGEST_LABEL
+import com.android.launcher3.LauncherSettings.Settings.LAYOUT_DIGEST_TAG
 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.OnboardingPrefs.ALL_APPS_VISITED_COUNT
 import com.android.launcher3.util.OnboardingPrefs.HOME_BOUNCE_COUNT
 import com.android.launcher3.util.OnboardingPrefs.HOME_BOUNCE_SEEN
@@ -45,12 +80,17 @@
 import com.android.launcher3.util.OnboardingPrefs.HOTSEAT_LONGPRESS_TIP_SEEN
 import com.android.launcher3.util.OnboardingPrefs.TASKBAR_EDU_TOOLTIP_STEP
 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.util.Locale
+import java.util.concurrent.Executor
 
 /** Helper class to generate UI for Device Config */
 class DevOptionsUiHelper(c: Context, attr: AttributeSet?) : PreferenceGroup(c, attr) {
@@ -67,6 +107,9 @@
         (holder.findViewById(R.id.filter_box) as TextView?)?.doAfterTextChanged {
             val query: String = it.toString().lowercase(Locale.getDefault()).replace("_", " ")
             filterPreferences(query, this)
+
+            // Always keep myself visible
+            this@DevOptionsUiHelper.isVisible = true
         }
     }
 
@@ -97,6 +140,7 @@
         }
         addIntentTargets()
         addOnboardingPrefsCategory()
+        addLayoutSharePref()
     }
 
     private fun newCategory(titleText: String, subTitleText: String? = null) =
@@ -359,7 +403,7 @@
             Preference(context).also {
                 it.title = title
                 it.summary = "Tap to reset"
-                setOnPreferenceClickListener { _ ->
+                it.setOnPreferenceClickListener { _ ->
                     LauncherPrefs.getPrefs(context)
                         .edit()
                         .apply { keys.forEach { key -> remove(key) } }
@@ -370,6 +414,137 @@
             }
         )
 
+    private fun addLayoutSharePref() {
+        val model = LauncherAppState.getInstance(context).model
+        val category = newCategory("Workspace grid layout")
+        Preference(context).apply {
+            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)
+                        }
+
+                        context.contentResolver.openOutputStream(uri).use { os ->
+                            builder.build(OutputStreamWriter(os))
+                        }
+
+                        MAIN_EXECUTOR.execute {
+                            Toast.makeText(context, "File saved", Toast.LENGTH_LONG).show()
+                        }
+                    }
+                }
+            category.addPreference(this)
+        }
+
+        Preference(context).apply {
+            title = "Import"
+            intent =
+                createUriPickerIntent(ACTION_OPEN_DOCUMENT, ORDERED_BG_EXECUTOR) { uri ->
+                    val resolver = context.contentResolver
+                    val data =
+                        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) {
+                            val key = Base64.encodeToString(digest, NO_WRAP or NO_PADDING)
+                            Secure.putString(resolver, LAYOUT_DIGEST_KEY, key)
+
+                            MODEL_EXECUTOR.submit { model.modelDbController.createEmptyDB() }.get()
+                            MAIN_EXECUTOR.submit { model.forceReload() }.get()
+                            MODEL_EXECUTOR.submit {}.get()
+                            Secure.putString(resolver, LAYOUT_DIGEST_KEY, null)
+                        }
+                    }
+                }
+            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,
+        callback: (uri: Uri) -> Unit
+    ): Intent {
+        val pendingIntent =
+            PendingIntent(
+                object : Stub() {
+                    override fun send(
+                        code: Int,
+                        intent: Intent,
+                        resolvedType: String?,
+                        allowlistToken: IBinder?,
+                        finishedReceiver: IIntentReceiver?,
+                        requiredPermission: String?,
+                        options: Bundle?
+                    ) {
+                        intent.data?.let { uri -> executor.execute { callback(uri) } }
+                    }
+                }
+            )
+        val params = StartActivityParams(pendingIntent, 0)
+        params.intent =
+            Intent(action)
+                .addCategory(Intent.CATEGORY_OPENABLE)
+                .setType("text/xml")
+                .putExtra(Intent.EXTRA_TITLE, "launcher_grid.xml")
+        return ProxyActivityStarter.getLaunchIntent(context, params)
+    }
+
     private inner class CustomSwitchPref(
         private val bindCallback: (holder: PreferenceViewHolder, pref: SwitchPreference) -> Unit
     ) : SwitchPreference(context) {
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java
index e039611..da1d322 100644
--- a/quickstep/src/com/android/quickstep/SystemUiProxy.java
+++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java
@@ -774,19 +774,6 @@
     }
 
     /**
-     * Tells SysUI to remove the bubble with the provided key.
-     * @param key the key of the bubble to show.
-     */
-    public void removeBubble(String key) {
-        if (mBubbles == null) return;
-        try {
-            mBubbles.removeBubble(key);
-        } catch (RemoteException e) {
-            Log.w(TAG, "Failed call removeBubble");
-        }
-    }
-
-    /**
      * Tells SysUI to remove all bubbles.
      */
     public void removeAllBubbles() {
@@ -828,19 +815,31 @@
     /**
      * Tells SysUI when the bubble stops being dragged.
      * Should be called only when the bubble bar is expanded.
-     * @param bubbleKey key of the bubble being dragged
      * @param location location of the bubble bar
      */
-    public void stopBubbleDrag(@Nullable String bubbleKey, BubbleBarLocation location) {
+    public void stopBubbleDrag(BubbleBarLocation location) {
         if (mBubbles == null) return;
         try {
-            mBubbles.stopBubbleDrag(bubbleKey, location);
+            mBubbles.stopBubbleDrag(location);
         } catch (RemoteException e) {
             Log.w(TAG, "Failed call stopBubbleDrag");
         }
     }
 
     /**
+     * Tells SysUI to dismiss the bubble with the provided key.
+     * @param key the key of the bubble to dismiss.
+     */
+    public void dragBubbleToDismiss(String key) {
+        if (mBubbles == null) return;
+        try {
+            mBubbles.dragBubbleToDismiss(key);
+        } catch (RemoteException e) {
+            Log.w(TAG, "Failed call dragBubbleToDismiss");
+        }
+    }
+
+    /**
      * Tells SysUI to show user education relative to the reference point provided.
      * @param position the bubble bar top center position in Screen coordinates.
      */
diff --git a/quickstep/src/com/android/quickstep/TaskThumbnailCache.java b/quickstep/src/com/android/quickstep/TaskThumbnailCache.java
index f6eef62..7ebb767 100644
--- a/quickstep/src/com/android/quickstep/TaskThumbnailCache.java
+++ b/quickstep/src/com/android/quickstep/TaskThumbnailCache.java
@@ -153,7 +153,7 @@
         Preconditions.assertUIThread();
 
         boolean lowResolution = !mHighResLoadingState.isEnabled();
-        if (task.thumbnail != null && task.thumbnail.thumbnail != null
+        if (task.thumbnail != null && task.thumbnail.getThumbnail() != null
                 && (!task.thumbnail.reducedResolution || lowResolution)) {
             // Nothing to load, the thumbnail is already high-resolution or matches what the
             // request, so just callback
@@ -189,7 +189,7 @@
         Preconditions.assertUIThread();
 
         ThumbnailData cachedThumbnail = mCache.getAndInvalidateIfModified(key);
-        if (cachedThumbnail != null &&  cachedThumbnail.thumbnail != null
+        if (cachedThumbnail != null &&  cachedThumbnail.getThumbnail() != null
                 && (!cachedThumbnail.reducedResolution || lowResolution)) {
             // Already cached, lets use that thumbnail
             callback.accept(cachedThumbnail);
@@ -200,7 +200,7 @@
                 () -> {
                     ThumbnailData thumbnailData = ActivityManagerWrapper.getInstance()
                             .getTaskThumbnail(key.id, lowResolution);
-                    return thumbnailData.thumbnail != null ? thumbnailData
+                    return thumbnailData.getThumbnail() != null ? thumbnailData
                             : ActivityManagerWrapper.getInstance().takeTaskThumbnail(key.id);
                 },
                 MAIN_EXECUTOR,
@@ -210,7 +210,7 @@
                     if (enableGridOnlyOverview() && result.reducedResolution
                             && getHighResLoadingState().isEnabled()) {
                         ThumbnailData newCachedThumbnail = mCache.getAndInvalidateIfModified(key);
-                        if (newCachedThumbnail != null && newCachedThumbnail.thumbnail != null
+                        if (newCachedThumbnail != null && newCachedThumbnail.getThumbnail() != null
                                 && !newCachedThumbnail.reducedResolution) {
                             return;
                         }
diff --git a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
index df1879e..7e7c794 100644
--- a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
+++ b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
@@ -983,6 +983,7 @@
         void onDestroy() {
             SystemUiProxy.INSTANCE.get(mLauncher).unregisterSplitSelectListener(
                     mSplitSelectListener);
+            mSplitSelectListener = null;
         }
 
         /**
diff --git a/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java b/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java
index 85d4f4b..5e42b90 100644
--- a/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java
+++ b/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java
@@ -136,7 +136,7 @@
             RectF startingTaskRect = new RectF();
             final FloatingTaskView floatingTaskView = FloatingTaskView.getFloatingTaskView(
                     mLauncher, mLauncher.getDragLayer(),
-                    controller.screenshotTask(runningTaskInfo.taskId).thumbnail,
+                    controller.screenshotTask(runningTaskInfo.taskId).getThumbnail(),
                     null /* icon */, startingTaskRect);
             RecentsModel.INSTANCE.get(mLauncher.getApplicationContext())
                     .getIconCache()
diff --git a/quickstep/src/com/android/quickstep/views/TaskThumbnailViewDeprecated.java b/quickstep/src/com/android/quickstep/views/TaskThumbnailViewDeprecated.java
index f7b6496..fed6af5 100644
--- a/quickstep/src/com/android/quickstep/views/TaskThumbnailViewDeprecated.java
+++ b/quickstep/src/com/android/quickstep/views/TaskThumbnailViewDeprecated.java
@@ -202,6 +202,17 @@
     }
 
     /**
+     * Sets TaskOverlayFactory without binding a task.
+     *
+     * @deprecated Should only be used when using new
+     * {@link com.android.quickstep.task.thumbnail.TaskThumbnailView}.
+     */
+    @Deprecated
+    public void setTaskOverlayFactory(TaskOverlayFactory taskOverlayFactory) {
+        mTaskOverlayFactory = taskOverlayFactory;
+    }
+
+    /**
      * Updates the thumbnail.
      *
      * @param refreshNow whether the {@code thumbnailData} will be used to redraw immediately.
@@ -215,8 +226,8 @@
             boolean refreshNow) {
         mTask = task;
         boolean thumbnailWasNull = mThumbnailData == null;
-        mThumbnailData =
-                (thumbnailData != null && thumbnailData.thumbnail != null) ? thumbnailData : null;
+        mThumbnailData = (thumbnailData != null && thumbnailData.getThumbnail() != null)
+                ? thumbnailData : null;
         if (mTask != null) {
             updateSplashView(mTask.icon);
         }
@@ -241,8 +252,8 @@
      * @param shouldRefreshOverlay whether to re-initialize overlay
      */
     private void refresh(boolean shouldRefreshOverlay) {
-        if (mThumbnailData != null && mThumbnailData.thumbnail != null) {
-            Bitmap bm = mThumbnailData.thumbnail;
+        if (mThumbnailData != null && mThumbnailData.getThumbnail() != null) {
+            Bitmap bm = mThumbnailData.getThumbnail();
             bm.prepareToDraw();
             mBitmapShader = new BitmapShader(bm, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
             mPaint.setShader(mBitmapShader);
@@ -306,8 +317,10 @@
         }
 
         RectF bitmapRect = new RectF(
-                0, 0,
-                mThumbnailData.thumbnail.getWidth(), mThumbnailData.thumbnail.getHeight());
+                0,
+                0,
+                mThumbnailData.getThumbnail().getWidth(),
+                mThumbnailData.getThumbnail().getHeight());
         RectF viewRect = new RectF(0, 0, getMeasuredWidth(), getMeasuredHeight());
 
         // The position helper matrix tells us how to transform the bitmap to fit the view, the
@@ -506,13 +519,13 @@
     }
 
     private boolean isThumbnailAspectRatioDifferentFromThumbnailData() {
-        if (mThumbnailData == null || mThumbnailData.thumbnail == null) {
+        if (mThumbnailData == null || mThumbnailData.getThumbnail() == null) {
             return false;
         }
 
         float thumbnailViewAspect = getWidth() / (float) getHeight();
-        float thumbnailDataAspect =
-                mThumbnailData.thumbnail.getWidth() / (float) mThumbnailData.thumbnail.getHeight();
+        float thumbnailDataAspect = mThumbnailData.getThumbnail().getWidth()
+                / (float) mThumbnailData.getThumbnail().getHeight();
 
         return isRelativePercentDifferenceGreaterThan(thumbnailViewAspect,
                 thumbnailDataAspect, MAX_PCT_BEFORE_ASPECT_RATIOS_CONSIDERED_DIFFERENT);
@@ -563,8 +576,8 @@
         DeviceProfile dp = mContainer.getDeviceProfile();
         mPreviewPositionHelper.setOrientationChanged(false);
         if (mBitmapShader != null && mThumbnailData != null) {
-            mPreviewRect.set(0, 0, mThumbnailData.thumbnail.getWidth(),
-                    mThumbnailData.thumbnail.getHeight());
+            mPreviewRect.set(0, 0, mThumbnailData.getThumbnail().getWidth(),
+                    mThumbnailData.getThumbnail().getHeight());
             int currentRotation = getTaskView().getOrientedState().getRecentsActivityRotation();
             boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
             mPreviewPositionHelper.updateThumbnailMatrix(mPreviewRect, mThumbnailData,
@@ -597,7 +610,7 @@
         if (mThumbnailData == null) {
             return null;
         }
-        return mThumbnailData.thumbnail;
+        return mThumbnailData.getThumbnail();
     }
 
     /**
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.kt b/quickstep/src/com/android/quickstep/views/TaskView.kt
index d19f09c..baeaa87 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskView.kt
@@ -630,6 +630,7 @@
                 )
             )
         if (enableRefactorTaskThumbnail()) {
+            taskThumbnailViewDeprecated.setTaskOverlayFactory(taskOverlayFactory)
             bindTaskThumbnailView()
         } else {
             taskThumbnailViewDeprecated.bind(task, taskOverlayFactory)
diff --git a/src/com/android/launcher3/AutoInstallsLayout.java b/src/com/android/launcher3/AutoInstallsLayout.java
index cf86528..175d6ec 100644
--- a/src/com/android/launcher3/AutoInstallsLayout.java
+++ b/src/com/android/launcher3/AutoInstallsLayout.java
@@ -19,6 +19,8 @@
 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
 import static com.android.launcher3.LauncherSettings.Favorites.TABLE_NAME;
 import static com.android.launcher3.provider.LauncherDbUtils.itemIdMatch;
+import static com.android.launcher3.util.UserIconInfo.TYPE_CLONED;
+import static com.android.launcher3.util.UserIconInfo.TYPE_WORK;
 
 import android.content.ComponentName;
 import android.content.ContentValues;
@@ -34,6 +36,7 @@
 import android.database.sqlite.SQLiteDatabase;
 import android.os.Bundle;
 import android.os.Process;
+import android.os.UserHandle;
 import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.AttributeSet;
@@ -56,6 +59,7 @@
 import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.Partner;
 import com.android.launcher3.util.Thunk;
+import com.android.launcher3.util.UserIconInfo;
 import com.android.launcher3.widget.LauncherWidgetHolder;
 
 import org.xmlpull.v1.XmlPullParser;
@@ -63,6 +67,7 @@
 
 import java.io.IOException;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.Locale;
 import java.util.Map;
 import java.util.function.Supplier;
@@ -125,34 +130,38 @@
     private static final String TAG_INCLUDE = "include";
     public static final String TAG_WORKSPACE = "workspace";
     private static final String TAG_APP_ICON = "appicon";
-    private static final String TAG_AUTO_INSTALL = "autoinstall";
-    private static final String TAG_FOLDER = "folder";
-    private static final String TAG_APPWIDGET = "appwidget";
+    public static final String TAG_AUTO_INSTALL = "autoinstall";
+    public static final String TAG_FOLDER = "folder";
+    public static final String TAG_APPWIDGET = "appwidget";
     protected static final String TAG_SEARCH_WIDGET = "searchwidget";
-    private static final String TAG_SHORTCUT = "shortcut";
+    public static final String TAG_SHORTCUT = "shortcut";
     private static final String TAG_EXTRA = "extra";
 
-    private static final String ATTR_CONTAINER = "container";
-    private static final String ATTR_RANK = "rank";
+    public static final String ATTR_CONTAINER = "container";
+    public static final String ATTR_RANK = "rank";
 
-    private static final String ATTR_PACKAGE_NAME = "packageName";
-    private static final String ATTR_CLASS_NAME = "className";
-    private static final String ATTR_TITLE = "title";
-    private static final String ATTR_TITLE_TEXT = "titleText";
-    private static final String ATTR_SCREEN = "screen";
-    private static final String ATTR_SHORTCUT_ID = "shortcutId";
+    public static final String ATTR_PACKAGE_NAME = "packageName";
+    public static final String ATTR_CLASS_NAME = "className";
+    public static final String ATTR_TITLE = "title";
+    public static final String ATTR_TITLE_TEXT = "titleText";
+    public static final String ATTR_SCREEN = "screen";
+    public static final String ATTR_SHORTCUT_ID = "shortcutId";
 
     // x and y can be specified as negative integers, in which case -1 represents the
     // last row / column, -2 represents the second last, and so on.
-    private static final String ATTR_X = "x";
-    private static final String ATTR_Y = "y";
+    public static final String ATTR_X = "x";
+    public static final String ATTR_Y = "y";
 
-    private static final String ATTR_SPAN_X = "spanX";
-    private static final String ATTR_SPAN_Y = "spanY";
+    public static final String ATTR_SPAN_X = "spanX";
+    public static final String ATTR_SPAN_Y = "spanY";
 
     // Attrs for "Include"
     private static final String ATTR_WORKSPACE = "workspace";
 
+    public static final String ATTR_USER_TYPE = "userType";
+    public static final String USER_TYPE_WORK = "work";
+    public static final String USER_TYPE_CLONED = "cloned";
+
     // Style attrs -- "Extra"
     private static final String ATTR_KEY = "key";
     private static final String ATTR_VALUE = "value";
@@ -168,6 +177,8 @@
     protected final SourceResources mSourceRes;
     protected final Supplier<XmlPullParser> mInitialLayoutSupplier;
 
+    private final Map<String, Long> mUserTypeToSerial;
+
     private final InvariantDeviceProfile mIdp;
     private final int mRowCount;
     private final int mColumnCount;
@@ -204,15 +215,25 @@
         mRowCount = mIdp.numRows;
         mColumnCount = mIdp.numColumns;
         mActivityOverride = ApiWrapper.INSTANCE.get(context).getActivityOverrides();
+
+        mUserTypeToSerial = new HashMap<>();
+        UserCache cache = UserCache.getInstance(context);
+        for (UserHandle user : cache.getUserProfiles()) {
+            UserIconInfo uii = cache.getUserInfo(user);
+            switch (uii.type) {
+                case TYPE_WORK -> mUserTypeToSerial.put(USER_TYPE_WORK, uii.userSerial);
+                case TYPE_CLONED -> mUserTypeToSerial.put(USER_TYPE_CLONED, uii.userSerial);
+            }
+        }
     }
 
     /**
      * Loads the layout in the db and returns the number of entries added on the desktop.
      */
-    public int loadLayout(SQLiteDatabase db, IntArray screenIds) {
+    public int loadLayout(SQLiteDatabase db) {
         mDb = db;
         try {
-            return parseLayout(mInitialLayoutSupplier.get(), screenIds);
+            return parseLayout(mInitialLayoutSupplier.get());
         } catch (Exception e) {
             Log.e(TAG, "Error parsing layout: ", e);
             return -1;
@@ -222,7 +243,7 @@
     /**
      * Parses the layout and returns the number of elements added on the homescreen.
      */
-    protected int parseLayout(XmlPullParser parser, IntArray screenIds)
+    protected int parseLayout(XmlPullParser parser)
             throws XmlPullParserException, IOException {
         beginDocument(parser, mRootTag);
         final int depth = parser.getDepth();
@@ -235,7 +256,7 @@
             if (type != XmlPullParser.START_TAG) {
                 continue;
             }
-            count += parseAndAddNode(parser, tagParserMap, screenIds);
+            count += parseAndAddNode(parser, tagParserMap);
         }
         return count;
     }
@@ -259,14 +280,14 @@
      * Parses the current node and returns the number of elements added.
      */
     protected int parseAndAddNode(
-            XmlPullParser parser, ArrayMap<String, TagParser> tagParserMap, IntArray screenIds)
+            XmlPullParser parser, ArrayMap<String, TagParser> tagParserMap)
             throws XmlPullParserException, IOException {
 
         if (TAG_INCLUDE.equals(parser.getName())) {
             final int resId = getAttributeResourceValue(parser, ATTR_WORKSPACE, 0);
             if (resId != 0) {
                 // recursively load some more favorites, why not?
-                return parseLayout(mSourceRes.getXml(resId), screenIds);
+                return parseLayout(mSourceRes.getXml(resId));
             } else {
                 return 0;
             }
@@ -284,22 +305,17 @@
                 convertToDistanceFromEnd(getAttributeValue(parser, ATTR_X), mColumnCount));
         mValues.put(Favorites.CELLY,
                 convertToDistanceFromEnd(getAttributeValue(parser, ATTR_Y), mRowCount));
+        Long profileId = mUserTypeToSerial.get(getAttributeValue(parser, ATTR_USER_TYPE));
+        if (profileId != null) {
+            mValues.put(Favorites.PROFILE_ID, profileId);
+        }
 
         TagParser tagParser = tagParserMap.get(parser.getName());
         if (tagParser == null) {
             if (LOGD) Log.d(TAG, "Ignoring unknown element tag: " + parser.getName());
             return 0;
         }
-        int newElementId = tagParser.parseAndAdd(parser);
-        if (newElementId >= 0) {
-            // Keep track of the set of screens which need to be added to the db.
-            if (!screenIds.contains(screenId) &&
-                    container == Favorites.CONTAINER_DESKTOP) {
-                screenIds.add(screenId);
-            }
-            return 1;
-        }
-        return 0;
+        return tagParser.parseAndAdd(parser) >= 0 ? 1 : 0;
     }
 
     protected int addShortcut(String title, Intent intent, int type) {
@@ -311,10 +327,11 @@
         mValues.put(Favorites.SPANY, 1);
         mValues.put(Favorites._ID, id);
 
-        if (type == ITEM_TYPE_APPLICATION) {
-            ComponentName cn = intent.getComponent();
-            if (cn != null && mActivityOverride.containsKey(cn.getPackageName())) {
-                LauncherActivityInfo replacementInfo = mActivityOverride.get(cn.getPackageName());
+        ComponentName cn = intent.getComponent();
+        if (cn != null && type == ITEM_TYPE_APPLICATION
+                && !mValues.containsKey(Favorites.PROFILE_ID)) {
+            LauncherActivityInfo replacementInfo = mActivityOverride.get(cn.getPackageName());
+            if (replacementInfo != null) {
                 mValues.put(Favorites.PROFILE_ID, UserCache.INSTANCE.get(mContext)
                         .getSerialNumberForUser(replacementInfo.getUser()));
                 mValues.put(Favorites.INTENT, AppInfo.makeLaunchIntent(replacementInfo).toUri(0));
@@ -420,11 +437,7 @@
             }
 
             mValues.put(Favorites.RESTORED, WorkspaceItemInfo.FLAG_AUTOINSTALL_ICON);
-            final Intent intent = new Intent(Intent.ACTION_MAIN, null)
-                    .addCategory(Intent.CATEGORY_LAUNCHER)
-                    .setComponent(new ComponentName(packageName, className))
-                    .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
-                            | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
+            Intent intent = AppInfo.makeLaunchIntent(new ComponentName(packageName, className));
             return addShortcut(mContext.getString(R.string.package_state_unknown), intent,
                     ITEM_TYPE_APPLICATION);
         }
diff --git a/src/com/android/launcher3/model/DatabaseHelper.java b/src/com/android/launcher3/model/DatabaseHelper.java
index 132b606..8368256 100644
--- a/src/com/android/launcher3/model/DatabaseHelper.java
+++ b/src/com/android/launcher3/model/DatabaseHelper.java
@@ -505,7 +505,7 @@
 
     public int loadFavorites(SQLiteDatabase db, AutoInstallsLayout loader) {
         // TODO: Use multiple loaders with fall-back and transaction.
-        int count = loader.loadLayout(db, new IntArray());
+        int count = loader.loadLayout(db);
 
         // Ensure that the max ids are initialized
         mMaxItemId = initializeMaxItemId(db);
diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java
index 102d28a..0d40a24 100644
--- a/src/com/android/launcher3/model/LoaderTask.java
+++ b/src/com/android/launcher3/model/LoaderTask.java
@@ -171,7 +171,7 @@
         mLauncherApps = mApp.getContext().getSystemService(LauncherApps.class);
         mUserManager = mApp.getContext().getSystemService(UserManager.class);
         mUserCache = UserCache.INSTANCE.get(mApp.getContext());
-        mPmHelper = new PackageManagerHelper(mApp.getContext());
+        mPmHelper = PackageManagerHelper.INSTANCE.get(mApp.getContext());
         mSessionHelper = InstallSessionHelper.INSTANCE.get(mApp.getContext());
         mIconCache = mApp.getIconCache();
         mUserManagerState = userManagerState;
diff --git a/src/com/android/launcher3/model/UserManagerState.java b/src/com/android/launcher3/model/UserManagerState.java
index 720f08e..ed32430 100644
--- a/src/com/android/launcher3/model/UserManagerState.java
+++ b/src/com/android/launcher3/model/UserManagerState.java
@@ -17,6 +17,7 @@
 
 import android.os.UserHandle;
 import android.os.UserManager;
+import android.util.Log;
 import android.util.LongSparseArray;
 import android.util.SparseBooleanArray;
 
@@ -27,6 +28,8 @@
  */
 public class UserManagerState {
 
+    private static final String TAG = "UserManagerState";
+
     public final LongSparseArray<UserHandle> allUsers = new LongSparseArray<>();
 
     private final LongSparseArray<Boolean> mQuietUsersSerialNoMap = new LongSparseArray<>();
@@ -39,6 +42,13 @@
         for (UserHandle user : userManager.getUserProfiles()) {
             long serialNo = userCache.getSerialNumberForUser(user);
             boolean isUserQuiet = userManager.isQuietModeEnabled(user);
+            // Mapping different UserHandles to the same serialNo in allUsers could lead to losing
+            // UserHandle and cause a series of problems, such as incorrectly marking app as
+            // disabled and deleting app icons from workspace.
+            if (allUsers.get(serialNo) != null) {
+                Log.w(TAG, String.format("Override allUsers[%d]=%s with %s",
+                        serialNo, allUsers.get(serialNo), user));
+            }
             allUsers.put(serialNo, user);
             mQuietUsersHashCodeMap.put(user.hashCode(), isUserQuiet);
             mQuietUsersSerialNoMap.put(serialNo, isUserQuiet);
diff --git a/src/com/android/launcher3/util/LauncherLayoutBuilder.kt b/src/com/android/launcher3/util/LauncherLayoutBuilder.kt
new file mode 100644
index 0000000..ecc9953
--- /dev/null
+++ b/src/com/android/launcher3/util/LauncherLayoutBuilder.kt
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2019 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.util.Xml
+import com.android.launcher3.AutoInstallsLayout.ATTR_CLASS_NAME
+import com.android.launcher3.AutoInstallsLayout.ATTR_CONTAINER
+import com.android.launcher3.AutoInstallsLayout.ATTR_PACKAGE_NAME
+import com.android.launcher3.AutoInstallsLayout.ATTR_RANK
+import com.android.launcher3.AutoInstallsLayout.ATTR_SCREEN
+import com.android.launcher3.AutoInstallsLayout.ATTR_SHORTCUT_ID
+import com.android.launcher3.AutoInstallsLayout.ATTR_SPAN_X
+import com.android.launcher3.AutoInstallsLayout.ATTR_SPAN_Y
+import com.android.launcher3.AutoInstallsLayout.ATTR_TITLE
+import com.android.launcher3.AutoInstallsLayout.ATTR_TITLE_TEXT
+import com.android.launcher3.AutoInstallsLayout.ATTR_USER_TYPE
+import com.android.launcher3.AutoInstallsLayout.ATTR_X
+import com.android.launcher3.AutoInstallsLayout.ATTR_Y
+import com.android.launcher3.AutoInstallsLayout.TAG_APPWIDGET
+import com.android.launcher3.AutoInstallsLayout.TAG_AUTO_INSTALL
+import com.android.launcher3.AutoInstallsLayout.TAG_FOLDER
+import com.android.launcher3.AutoInstallsLayout.TAG_SHORTCUT
+import com.android.launcher3.AutoInstallsLayout.TAG_WORKSPACE
+import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP
+import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT
+import com.android.launcher3.LauncherSettings.Favorites.containerToString
+import java.io.IOException
+import java.io.StringWriter
+import java.io.Writer
+import org.xmlpull.v1.XmlSerializer
+
+/** Helper class to build xml for Launcher Layout */
+class LauncherLayoutBuilder {
+    private val nodes = ArrayList<Node>()
+
+    fun atHotseat(rank: Int) =
+        ItemTarget(
+            mapOf(
+                ATTR_CONTAINER to containerToString(CONTAINER_HOTSEAT),
+                ATTR_RANK to rank.toString()
+            )
+        )
+
+    fun atWorkspace(x: Int, y: Int, screen: Int) =
+        ItemTarget(
+            mapOf(
+                ATTR_CONTAINER to containerToString(CONTAINER_DESKTOP),
+                ATTR_X to x.toString(),
+                ATTR_Y to y.toString(),
+                ATTR_SCREEN to screen.toString()
+            )
+        )
+
+    @Throws(IOException::class) fun build() = StringWriter().apply { build(this) }.toString()
+
+    @Throws(IOException::class)
+    fun build(writer: Writer) {
+        Xml.newSerializer().apply {
+            setOutput(writer)
+            setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true)
+            startDocument("UTF-8", true)
+            startTag(null, TAG_WORKSPACE)
+            writeNodes(nodes)
+            endTag(null, TAG_WORKSPACE)
+            endDocument()
+            flush()
+        }
+    }
+
+    open inner class ItemTarget(private val baseValues: Map<String, String>) {
+        @JvmOverloads
+        fun putApp(packageName: String, className: String?, userType: String? = null) =
+            addItem(
+                TAG_AUTO_INSTALL,
+                userType,
+                mapOf(
+                    ATTR_PACKAGE_NAME to packageName,
+                    ATTR_CLASS_NAME to (className ?: packageName)
+                )
+            )
+
+        @JvmOverloads
+        fun putShortcut(packageName: String, shortcutId: String, userType: String? = null) =
+            addItem(
+                TAG_SHORTCUT,
+                userType,
+                mapOf(ATTR_PACKAGE_NAME to packageName, ATTR_SHORTCUT_ID to shortcutId)
+            )
+
+        @JvmOverloads
+        fun putWidget(
+            packageName: String,
+            className: String,
+            spanX: Int,
+            spanY: Int,
+            userType: String? = null
+        ) =
+            addItem(
+                TAG_APPWIDGET,
+                userType,
+                mapOf(
+                    ATTR_PACKAGE_NAME to packageName,
+                    ATTR_CLASS_NAME to className,
+                    ATTR_SPAN_X to spanX.toString(),
+                    ATTR_SPAN_Y to spanY.toString()
+                )
+            )
+
+        fun putFolder(titleResId: Int) = putFolder(ATTR_TITLE, titleResId.toString())
+
+        fun putFolder(title: String?) = putFolder(ATTR_TITLE_TEXT, title)
+
+        protected open fun addItem(
+            tag: String,
+            userType: String?,
+            props: Map<String, String>,
+            children: List<Node>? = null
+        ): LauncherLayoutBuilder {
+            nodes.add(
+                Node(
+                    tag,
+                    HashMap(baseValues).apply {
+                        putAll(props)
+                        userType?.let { put(ATTR_USER_TYPE, it) }
+                    },
+                    children
+                )
+            )
+            return this@LauncherLayoutBuilder
+        }
+
+        protected open fun putFolder(titleKey: String, titleValue: String?): FolderBuilder {
+            val folderBuilder = FolderBuilder()
+            addItem(TAG_FOLDER, null, mapOf(titleKey to (titleValue ?: "")), folderBuilder.children)
+            return folderBuilder
+        }
+    }
+
+    inner class FolderBuilder : ItemTarget(mapOf()) {
+
+        val children = ArrayList<Node>()
+
+        fun addApp(packageName: String, className: String?): FolderBuilder {
+            putApp(packageName, className)
+            return this
+        }
+
+        fun addShortcut(packageName: String, shortcutId: String): FolderBuilder {
+            putShortcut(packageName, shortcutId)
+            return this
+        }
+
+        override fun addItem(
+            tag: String,
+            userType: String?,
+            props: Map<String, String>,
+            childrenIgnored: List<Node>?
+        ): LauncherLayoutBuilder {
+            children.add(
+                Node(tag, HashMap(props).apply { userType?.let { put(ATTR_USER_TYPE, it) } })
+            )
+            return this@LauncherLayoutBuilder
+        }
+
+        override fun putFolder(titleKey: String, titleValue: String?): FolderBuilder {
+            throw IllegalArgumentException("Can't have folder inside a folder")
+        }
+
+        fun build() = this@LauncherLayoutBuilder
+    }
+
+    @Throws(IOException::class)
+    private fun XmlSerializer.writeNodes(nodes: List<Node>) {
+        nodes.forEach { node ->
+            startTag(null, node.name)
+            node.attrs.forEach { (key, value) -> attribute(null, key, value) }
+            node.children?.let { writeNodes(it) }
+            endTag(null, node.name)
+        }
+    }
+
+    data class Node(
+        val name: String,
+        val attrs: Map<String, String>,
+        val children: List<Node>? = null
+    )
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/AutoInstallsLayoutTest.kt b/tests/multivalentTests/src/com/android/launcher3/AutoInstallsLayoutTest.kt
new file mode 100644
index 0000000..b04bcca
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/AutoInstallsLayoutTest.kt
@@ -0,0 +1,216 @@
+/*
+ * 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
+
+import android.content.ComponentName
+import android.content.ContentValues
+import android.database.sqlite.SQLiteDatabase
+import android.os.Process.myUserHandle
+import android.os.UserHandle
+import android.util.Xml
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.AutoInstallsLayout.LayoutParserCallback
+import com.android.launcher3.AutoInstallsLayout.SourceResources
+import com.android.launcher3.AutoInstallsLayout.TAG_WORKSPACE
+import com.android.launcher3.AutoInstallsLayout.USER_TYPE_WORK
+import com.android.launcher3.LauncherSettings.Favorites.APPWIDGET_PROVIDER
+import com.android.launcher3.LauncherSettings.Favorites.CONTAINER
+import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP
+import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT
+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.ITEM_TYPE_APPWIDGET
+import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_FOLDER
+import com.android.launcher3.LauncherSettings.Favorites.PROFILE_ID
+import com.android.launcher3.LauncherSettings.Favorites.SPANX
+import com.android.launcher3.LauncherSettings.Favorites.SPANY
+import com.android.launcher3.LauncherSettings.Favorites._ID
+import com.android.launcher3.model.data.AppInfo
+import com.android.launcher3.pm.UserCache
+import com.android.launcher3.util.ApiWrapper
+import com.android.launcher3.util.Executors
+import com.android.launcher3.util.LauncherLayoutBuilder
+import com.android.launcher3.util.LauncherModelHelper
+import com.android.launcher3.util.LauncherModelHelper.SandboxModelContext
+import com.android.launcher3.util.TestUtil
+import com.android.launcher3.util.UserIconInfo
+import com.android.launcher3.util.UserIconInfo.TYPE_MAIN
+import com.android.launcher3.util.UserIconInfo.TYPE_WORK
+import com.android.launcher3.widget.LauncherWidgetHolder
+import com.google.common.truth.Truth.assertThat
+import java.io.StringReader
+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 org.mockito.kotlin.doReturn
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.whenever
+
+/** Tests for [AutoInstallsLayout] */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class AutoInstallsLayoutTest {
+
+    lateinit var modelHelper: LauncherModelHelper
+    lateinit var targetContext: SandboxModelContext
+
+    lateinit var callback: MyCallback
+
+    @Mock lateinit var widgetHolder: LauncherWidgetHolder
+    @Mock lateinit var db: SQLiteDatabase
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        modelHelper = LauncherModelHelper()
+        targetContext = modelHelper.sandboxContext
+        callback = MyCallback()
+    }
+
+    @After
+    fun tearDown() {
+        modelHelper.destroy()
+    }
+
+    @Test
+    fun pending_icon_added_on_home() {
+        LauncherLayoutBuilder()
+            .atWorkspace(1, 1, 0)
+            .putApp("p1", "c1")
+            .toAutoInstallsLayout()
+            .loadLayout(db)
+
+        assertThat(callback.items.size).isEqualTo(1)
+        assertThat(callback.items[0][ITEM_TYPE]).isEqualTo(ITEM_TYPE_APPLICATION)
+        assertThat(callback.items[0][INTENT])
+            .isEqualTo(AppInfo.makeLaunchIntent(ComponentName("p1", "c1")).toUri(0))
+        assertThat(callback.items[0][CONTAINER]).isEqualTo(CONTAINER_DESKTOP)
+        assertThat(callback.items[0].containsKey(PROFILE_ID)).isFalse()
+    }
+
+    @Test
+    fun pending_icon_added_on_hotseat() {
+        LauncherLayoutBuilder()
+            .atHotseat(1)
+            .putApp("p1", "c1")
+            .toAutoInstallsLayout()
+            .loadLayout(db)
+
+        assertThat(callback.items.size).isEqualTo(1)
+        assertThat(callback.items[0][ITEM_TYPE]).isEqualTo(ITEM_TYPE_APPLICATION)
+        assertThat(callback.items[0][CONTAINER]).isEqualTo(CONTAINER_HOTSEAT)
+    }
+
+    @Test
+    fun widget_added_to_home() {
+        LauncherLayoutBuilder()
+            .atWorkspace(1, 1, 0)
+            .putWidget("p1", "c1", 2, 3)
+            .toAutoInstallsLayout()
+            .loadLayout(db)
+
+        assertThat(callback.items.size).isEqualTo(1)
+        assertThat(callback.items[0][ITEM_TYPE]).isEqualTo(ITEM_TYPE_APPWIDGET)
+        assertThat(callback.items[0][CONTAINER]).isEqualTo(CONTAINER_DESKTOP)
+        assertThat(callback.items[0][APPWIDGET_PROVIDER])
+            .isEqualTo(ComponentName("p1", "c1").flattenToString())
+        assertThat(callback.items[0][SPANX]).isEqualTo(2.toString())
+        assertThat(callback.items[0][SPANY]).isEqualTo(3.toString())
+    }
+
+    @Test
+    fun items_added_to_folder() {
+        LauncherLayoutBuilder()
+            .atHotseat(1)
+            .putFolder("Test")
+            .addApp("p1", "c")
+            .addApp("p2", "c")
+            .addApp("p3", "c")
+            .build()
+            .toAutoInstallsLayout()
+            .loadLayout(db)
+
+        assertThat(callback.items.size).isEqualTo(4)
+        assertThat(callback.items[0][ITEM_TYPE]).isEqualTo(ITEM_TYPE_FOLDER)
+        assertThat(callback.items[0][CONTAINER]).isEqualTo(CONTAINER_HOTSEAT)
+
+        val folderId = callback.items[0][_ID]
+        assertThat(callback.items[1][CONTAINER]).isEqualTo(folderId)
+        assertThat(callback.items[2][CONTAINER]).isEqualTo(folderId)
+        assertThat(callback.items[3][CONTAINER]).isEqualTo(folderId)
+    }
+
+    @Test
+    fun work_item_added_to_home() {
+        val apiWrapperMock = spy(ApiWrapper.INSTANCE[targetContext])
+        targetContext.putObject(ApiWrapper.INSTANCE, apiWrapperMock)
+        doReturn(
+                mapOf(
+                    myUserHandle() to UserIconInfo(myUserHandle(), TYPE_MAIN, 0),
+                    UserHandle.of(20) to UserIconInfo(UserHandle.of(20), TYPE_WORK, 20),
+                )
+            )
+            .whenever(apiWrapperMock)
+            .queryAllUsers()
+
+        val cache = UserCache.getInstance(targetContext)
+        TestUtil.runOnExecutorSync(Executors.MODEL_EXECUTOR) {
+            assertThat(cache.userProfiles.size).isEqualTo(2)
+        }
+
+        LauncherLayoutBuilder()
+            .atWorkspace(1, 1, 0)
+            .putApp("p1", "c1", USER_TYPE_WORK)
+            .toAutoInstallsLayout()
+            .loadLayout(db)
+
+        assertThat(callback.items.size).isEqualTo(1)
+        assertThat(callback.items[0][ITEM_TYPE]).isEqualTo(ITEM_TYPE_APPLICATION)
+        assertThat(callback.items[0][INTENT])
+            .isEqualTo(AppInfo.makeLaunchIntent(ComponentName("p1", "c1")).toUri(0))
+        assertThat(callback.items[0][CONTAINER]).isEqualTo(CONTAINER_DESKTOP)
+        assertThat(callback.items[0][PROFILE_ID]).isEqualTo(20)
+    }
+
+    private fun LauncherLayoutBuilder.toAutoInstallsLayout() =
+        AutoInstallsLayout(
+            targetContext,
+            widgetHolder,
+            callback,
+            SourceResources.wrap(targetContext.resources),
+            { Xml.newPullParser().also { it.setInput(StringReader(build())) } },
+            TAG_WORKSPACE
+        )
+
+    class MyCallback : LayoutParserCallback {
+
+        val items = ArrayList<ContentValues>()
+
+        override fun generateNewItemId() = items.size
+
+        override fun insertAndCheck(db: SQLiteDatabase?, values: ContentValues): Int {
+            val id = values[_ID]
+            items.add(ContentValues(values))
+            return if (id is Int) id else 0
+        }
+    }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/LauncherLayoutBuilder.java b/tests/multivalentTests/src/com/android/launcher3/util/LauncherLayoutBuilder.java
deleted file mode 100644
index ba01b04..0000000
--- a/tests/multivalentTests/src/com/android/launcher3/util/LauncherLayoutBuilder.java
+++ /dev/null
@@ -1,199 +0,0 @@
-/**
- * Copyright (C) 2019 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.text.TextUtils;
-import android.util.Pair;
-import android.util.Xml;
-
-import org.xmlpull.v1.XmlSerializer;
-
-import java.io.IOException;
-import java.io.StringWriter;
-import java.io.Writer;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * Helper class to build xml for Launcher Layout
- */
-public class LauncherLayoutBuilder {
-
-    // Object Tags
-    private static final String TAG_WORKSPACE = "workspace";
-    private static final String TAG_AUTO_INSTALL = "autoinstall";
-    private static final String TAG_FOLDER = "folder";
-    private static final String TAG_APPWIDGET = "appwidget";
-    private static final String TAG_SHORTCUT = "shortcut";
-    private static final String TAG_EXTRA = "extra";
-
-    private static final String ATTR_CONTAINER = "container";
-    private static final String ATTR_RANK = "rank";
-
-    private static final String ATTR_PACKAGE_NAME = "packageName";
-    private static final String ATTR_CLASS_NAME = "className";
-    private static final String ATTR_TITLE = "title";
-    private static final String ATTR_TITLE_TEXT = "titleText";
-    private static final String ATTR_SCREEN = "screen";
-    private static final String ATTR_SHORTCUT_ID = "shortcutId";
-
-    // x and y can be specified as negative integers, in which case -1 represents the
-    // last row / column, -2 represents the second last, and so on.
-    private static final String ATTR_X = "x";
-    private static final String ATTR_Y = "y";
-    private static final String ATTR_SPAN_X = "spanX";
-    private static final String ATTR_SPAN_Y = "spanY";
-
-    private static final String ATTR_CHILDREN = "children";
-
-
-    // Style attrs -- "Extra"
-    private static final String ATTR_KEY = "key";
-    private static final String ATTR_VALUE = "value";
-
-    private static final String CONTAINER_DESKTOP = "desktop";
-    private static final String CONTAINER_HOTSEAT = "hotseat";
-
-    private final ArrayList<Pair<String, HashMap<String, Object>>> mNodes = new ArrayList<>();
-
-    public Location atHotseat(int rank) {
-        Location l = new Location();
-        l.items.put(ATTR_CONTAINER, CONTAINER_HOTSEAT);
-        l.items.put(ATTR_RANK, Integer.toString(rank));
-        return l;
-    }
-
-    public Location atWorkspace(int x, int y, int screen) {
-        Location l = new Location();
-        l.items.put(ATTR_CONTAINER, CONTAINER_DESKTOP);
-        l.items.put(ATTR_X, Integer.toString(x));
-        l.items.put(ATTR_Y, Integer.toString(y));
-        l.items.put(ATTR_SCREEN, Integer.toString(screen));
-        return l;
-    }
-
-    public String build() throws IOException {
-        StringWriter writer = new StringWriter();
-        build(writer);
-        return writer.toString();
-    }
-
-    public void build(Writer writer) throws IOException {
-        XmlSerializer serializer = Xml.newSerializer();
-        serializer.setOutput(writer);
-
-        serializer.startDocument("UTF-8", true);
-        serializer.startTag(null, TAG_WORKSPACE);
-        writeNodes(serializer, mNodes);
-        serializer.endTag(null, TAG_WORKSPACE);
-        serializer.endDocument();
-        serializer.flush();
-    }
-
-    private static void writeNodes(XmlSerializer serializer,
-            ArrayList<Pair<String, HashMap<String, Object>>> nodes) throws IOException {
-        for (Pair<String, HashMap<String, Object>> node : nodes) {
-            ArrayList<Pair<String, HashMap<String, Object>>> children = null;
-
-            serializer.startTag(null, node.first);
-            for (Map.Entry<String, Object> attr : node.second.entrySet()) {
-                if (ATTR_CHILDREN.equals(attr.getKey())) {
-                    children = (ArrayList<Pair<String, HashMap<String, Object>>>) attr.getValue();
-                } else {
-                    serializer.attribute(null, attr.getKey(), (String) attr.getValue());
-                }
-            }
-
-            if (children != null) {
-                writeNodes(serializer, children);
-            }
-            serializer.endTag(null, node.first);
-        }
-    }
-
-    public class Location {
-
-        final HashMap<String, Object> items = new HashMap<>();
-
-        public LauncherLayoutBuilder putApp(String packageName, String className) {
-            items.put(ATTR_PACKAGE_NAME, packageName);
-            items.put(ATTR_CLASS_NAME, TextUtils.isEmpty(className) ? packageName : className);
-            mNodes.add(Pair.create(TAG_AUTO_INSTALL, items));
-            return LauncherLayoutBuilder.this;
-        }
-
-        public LauncherLayoutBuilder putShortcut(String packageName, String shortcutId) {
-            items.put(ATTR_PACKAGE_NAME, packageName);
-            items.put(ATTR_SHORTCUT_ID, shortcutId);
-            mNodes.add(Pair.create(TAG_SHORTCUT, items));
-            return LauncherLayoutBuilder.this;
-        }
-
-        public LauncherLayoutBuilder putWidget(String packageName, String className,
-                int spanX, int spanY) {
-            items.put(ATTR_PACKAGE_NAME, packageName);
-            items.put(ATTR_CLASS_NAME, className);
-            items.put(ATTR_SPAN_X, Integer.toString(spanX));
-            items.put(ATTR_SPAN_Y, Integer.toString(spanY));
-            mNodes.add(Pair.create(TAG_APPWIDGET, items));
-            return LauncherLayoutBuilder.this;
-        }
-
-        public FolderBuilder putFolder(int titleResId) {
-            items.put(ATTR_TITLE, Integer.toString(titleResId));
-            return putFolder();
-        }
-
-        public FolderBuilder putFolder(String title) {
-            items.put(ATTR_TITLE_TEXT, title);
-            return putFolder();
-        }
-
-        private FolderBuilder putFolder() {
-            FolderBuilder folderBuilder = new FolderBuilder();
-            items.put(ATTR_CHILDREN, folderBuilder.mChildren);
-            mNodes.add(Pair.create(TAG_FOLDER, items));
-            return folderBuilder;
-        }
-    }
-
-    public class FolderBuilder {
-
-        final ArrayList<Pair<String, HashMap<String, Object>>> mChildren = new ArrayList<>();
-
-        public FolderBuilder addApp(String packageName, String className) {
-            HashMap<String, Object> items = new HashMap<>();
-            items.put(ATTR_PACKAGE_NAME, packageName);
-            items.put(ATTR_CLASS_NAME, TextUtils.isEmpty(className) ? packageName : className);
-            mChildren.add(Pair.create(TAG_AUTO_INSTALL, items));
-            return this;
-        }
-
-        public FolderBuilder addShortcut(String packageName, String shortcutId) {
-            HashMap<String, Object> items = new HashMap<>();
-            items.put(ATTR_PACKAGE_NAME, packageName);
-            items.put(ATTR_SHORTCUT_ID, shortcutId);
-            mChildren.add(Pair.create(TAG_SHORTCUT, items));
-            return this;
-        }
-
-        public LauncherLayoutBuilder build() {
-            return LauncherLayoutBuilder.this;
-        }
-    }
-}
diff --git a/tests/src/com/android/launcher3/model/WorkspaceItemProcessorTest.kt b/tests/src/com/android/launcher3/model/WorkspaceItemProcessorTest.kt
index 6bbcf85..6cf3b19 100644
--- a/tests/src/com/android/launcher3/model/WorkspaceItemProcessorTest.kt
+++ b/tests/src/com/android/launcher3/model/WorkspaceItemProcessorTest.kt
@@ -23,6 +23,7 @@
 import android.content.pm.LauncherApps
 import android.content.pm.PackageInstaller
 import android.content.pm.ShortcutInfo
+import android.os.Process
 import android.os.UserHandle
 import android.platform.test.annotations.EnableFlags
 import android.util.LongSparseArray
@@ -429,6 +430,7 @@
                 whenever(disabledMessage).thenReturn("")
                 whenever(disabledReason).thenReturn(0)
                 whenever(persons).thenReturn(EMPTY_PERSON_ARRAY)
+                whenever(userHandle).thenReturn(Process.myUserHandle())
             }
         mIconRequestInfos = mutableListOf()
         // Make sure shortcuts map has expected key from expected package