Merge "Render user's actual workspace in ThemePicker preview (Part 3)" into ub-launcher3-master
diff --git a/robolectric_tests/src/com/android/launcher3/model/GridSizeMigrationTaskTest.java b/robolectric_tests/src/com/android/launcher3/model/GridSizeMigrationTaskTest.java
index 1ed4bca..c426dc5 100644
--- a/robolectric_tests/src/com/android/launcher3/model/GridSizeMigrationTaskTest.java
+++ b/robolectric_tests/src/com/android/launcher3/model/GridSizeMigrationTaskTest.java
@@ -65,7 +65,7 @@
         };
 
         mIdp.numHotseatIcons = 3;
-        new GridSizeMigrationTask(mContext, mDb, mValidPackages, 5, 3)
+        new GridSizeMigrationTask(mContext, mDb, mValidPackages, false, 5, 3)
                 .migrateHotseat();
         // First item is dropped as it has the least weight.
         verifyHotseat(hotseatItems[1], hotseatItems[3], hotseatItems[4]);
@@ -82,7 +82,7 @@
         };
 
         mIdp.numHotseatIcons = 3;
-        new GridSizeMigrationTask(mContext, mDb, mValidPackages, 5, 3)
+        new GridSizeMigrationTask(mContext, mDb, mValidPackages, false, 5, 3)
                 .migrateHotseat();
         // First item is dropped as it has the least weight.
         verifyHotseat(hotseatItems[1], hotseatItems[3], hotseatItems[4]);
@@ -127,7 +127,7 @@
                 {  5,  2, -1,  6},
         }});
 
-        new GridSizeMigrationTask(mContext, mDb, mValidPackages,
+        new GridSizeMigrationTask(mContext, mDb, mValidPackages, false,
                 new Point(4, 4), new Point(3, 3)).migrateWorkspace();
 
         // Column 2 and row 2 got removed.
@@ -147,7 +147,7 @@
                 {  5,  2, -1,  6},
         }});
 
-        new GridSizeMigrationTask(mContext, mDb, mValidPackages,
+        new GridSizeMigrationTask(mContext, mDb, mValidPackages, false,
                 new Point(4, 4), new Point(3, 3)).migrateWorkspace();
 
         // Items in the second column get moved to new screen
@@ -172,7 +172,7 @@
                 {  3,  1, -1,  4},
         }});
 
-        new GridSizeMigrationTask(mContext, mDb, mValidPackages,
+        new GridSizeMigrationTask(mContext, mDb, mValidPackages, false,
                 new Point(4, 4), new Point(3, 3)).migrateWorkspace();
 
         // Items in the second column of the first screen should get placed on the 3rd
@@ -204,7 +204,7 @@
                 {  5,  2, -1,  6},
         }});
 
-        new GridSizeMigrationTask(mContext, mDb, mValidPackages,
+        new GridSizeMigrationTask(mContext, mDb, mValidPackages, false,
                 new Point(4, 4), new Point(3, 3)).migrateWorkspace();
 
         // Items in the second column of the first screen should get placed on a new screen.
@@ -235,7 +235,7 @@
                 {  5,  2,  7, -1},
         }}, 0);
 
-        new GridSizeMigrationTask(mContext, mDb, mValidPackages,
+        new GridSizeMigrationTask(mContext, mDb, mValidPackages, false,
                 new Point(4, 4), new Point(3, 4)).migrateWorkspace();
 
         // Items in the second column of the first screen should get placed on a new screen.
@@ -262,7 +262,7 @@
                 {  5,  6,  7, -1},
         }}, 0);
 
-        new GridSizeMigrationTask(mContext, mDb, mValidPackages,
+        new GridSizeMigrationTask(mContext, mDb, mValidPackages, false,
                 new Point(4, 4), new Point(3, 3)).migrateWorkspace();
 
         // Items in the second column of the first screen should get placed on a new screen.
@@ -282,7 +282,7 @@
      *            represent the workspace grid.
      */
     private void verifyWorkspace(int[][][] ids) {
-        IntArray allScreens = getWorkspaceScreenIds(mDb);
+        IntArray allScreens = getWorkspaceScreenIds(mDb, LauncherSettings.Favorites.TABLE_NAME);
         assertEquals(ids.length, allScreens.size());
         int total = 0;
 
@@ -351,7 +351,7 @@
         private final LinkedList<Point> mPoints;
 
         public MultiStepMigrationTaskVerifier(int... points) {
-            super(null, null, null);
+            super(null, null, null, false);
 
             mPoints = new LinkedList<>();
             for (int i = 0; i < points.length; i += 2) {
diff --git a/robolectric_tests/src/com/android/launcher3/model/LoaderCursorTest.java b/robolectric_tests/src/com/android/launcher3/model/LoaderCursorTest.java
index 7fa3ee9..6e41a4f 100644
--- a/robolectric_tests/src/com/android/launcher3/model/LoaderCursorTest.java
+++ b/robolectric_tests/src/com/android/launcher3/model/LoaderCursorTest.java
@@ -52,6 +52,7 @@
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.ItemInfo;
 import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.WorkspaceItemInfo;
 import com.android.launcher3.util.Executors;
 import com.android.launcher3.util.LauncherRoboTestRunner;
@@ -91,7 +92,7 @@
                 SCREEN, CELLX, CELLY, RESTORED, INTENT
         });
 
-        mLoaderCursor = new LoaderCursor(mCursor, mApp);
+        mLoaderCursor = new LoaderCursor(mCursor, LauncherSettings.Favorites.CONTENT_URI, mApp);
         mLoaderCursor.allUsers.put(0, Process.myUserHandle());
     }
 
diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java
index 857db8e..2ad84b9 100644
--- a/src/com/android/launcher3/InvariantDeviceProfile.java
+++ b/src/com/android/launcher3/InvariantDeviceProfile.java
@@ -47,6 +47,7 @@
 import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.graphics.IconShape;
+import com.android.launcher3.graphics.LauncherPreviewRenderer;
 import com.android.launcher3.util.ConfigMonitor;
 import com.android.launcher3.util.DefaultDisplay;
 import com.android.launcher3.util.DefaultDisplay.Info;
@@ -156,6 +157,11 @@
 
     @TargetApi(23)
     private InvariantDeviceProfile(Context context) {
+        if (context instanceof LauncherPreviewRenderer.PreviewContext) {
+            throw new IllegalArgumentException(
+                    "PreviewContext is passed into this IDP constructor");
+        }
+
         String gridName = Utilities.getPrefs(context).getBoolean(GRID_OPTIONS_PREFERENCE_KEY, false)
                 ? Utilities.getPrefs(context).getString(KEY_IDP_GRID_NAME, null)
                 : null;
diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java
index d77285d..2f38037 100644
--- a/src/com/android/launcher3/LauncherAppState.java
+++ b/src/com/android/launcher3/LauncherAppState.java
@@ -27,6 +27,8 @@
 import android.os.Handler;
 import android.util.Log;
 
+import androidx.annotation.Nullable;
+
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.icons.IconProvider;
@@ -55,12 +57,12 @@
     private final IconCache mIconCache;
     private final WidgetPreviewLoader mWidgetCache;
     private final InvariantDeviceProfile mInvariantDeviceProfile;
-    private final SecureSettingsObserver mNotificationDotsObserver;
 
-    private final InstallSessionTracker mInstallSessionTracker;
-    private final SimpleBroadcastReceiver mModelChangeReceiver;
-    private final SafeCloseable mCalendarChangeTracker;
-    private final SafeCloseable mUserChangeListener;
+    private SecureSettingsObserver mNotificationDotsObserver;
+    private InstallSessionTracker mInstallSessionTracker;
+    private SimpleBroadcastReceiver mModelChangeReceiver;
+    private SafeCloseable mCalendarChangeTracker;
+    private SafeCloseable mUserChangeListener;
 
     public static LauncherAppState getInstance(final Context context) {
         return INSTANCE.get(context);
@@ -74,15 +76,8 @@
         return mContext;
     }
 
-    private LauncherAppState(Context context) {
-        Log.v(Launcher.TAG, "LauncherAppState initiated");
-        Preconditions.assertUIThread();
-        mContext = context;
-
-        mInvariantDeviceProfile = InvariantDeviceProfile.INSTANCE.get(mContext);
-        mIconCache = new IconCache(mContext, mInvariantDeviceProfile);
-        mWidgetCache = new WidgetPreviewLoader(mContext, mIconCache);
-        mModel = new LauncherModel(this, mIconCache, AppFilter.newInstance(mContext));
+    public LauncherAppState(Context context) {
+        this(context, LauncherFiles.APP_ICONS_DB);
 
         mModelChangeReceiver = new SimpleBroadcastReceiver(mModel::onBroadcastIntent);
 
@@ -123,6 +118,17 @@
         }
     }
 
+    public LauncherAppState(Context context, @Nullable String iconCacheFileName) {
+        Log.v(Launcher.TAG, "LauncherAppState initiated");
+        Preconditions.assertUIThread();
+        mContext = context;
+
+        mInvariantDeviceProfile = InvariantDeviceProfile.INSTANCE.get(context);
+        mIconCache = new IconCache(mContext, mInvariantDeviceProfile, iconCacheFileName);
+        mWidgetCache = new WidgetPreviewLoader(mContext, mIconCache);
+        mModel = new LauncherModel(this, mIconCache, AppFilter.newInstance(mContext));
+    }
+
     protected void onNotificationSettingsChanged(boolean areNotificationDotsEnabled) {
         if (areNotificationDotsEnabled) {
             NotificationListener.requestRebind(new ComponentName(
@@ -148,11 +154,19 @@
      * Call from Application.onTerminate(), which is not guaranteed to ever be called.
      */
     public void onTerminate() {
-        mContext.unregisterReceiver(mModelChangeReceiver);
+        if (mModelChangeReceiver != null) {
+            mContext.unregisterReceiver(mModelChangeReceiver);
+        }
         mContext.getSystemService(LauncherApps.class).unregisterCallback(mModel);
-        mInstallSessionTracker.unregister();
-        mCalendarChangeTracker.close();
-        mUserChangeListener.close();
+        if (mInstallSessionTracker != null) {
+            mInstallSessionTracker.unregister();
+        }
+        if (mCalendarChangeTracker != null) {
+            mCalendarChangeTracker.close();
+        }
+        if (mUserChangeListener != null) {
+            mUserChangeListener.close();
+        }
         CustomWidgetManager.INSTANCE.get(mContext).setWidgetRefreshCallback(null);
 
         if (mNotificationDotsObserver != null) {
diff --git a/src/com/android/launcher3/LauncherSettings.java b/src/com/android/launcher3/LauncherSettings.java
index 49831f6..216c221 100644
--- a/src/com/android/launcher3/LauncherSettings.java
+++ b/src/com/android/launcher3/LauncherSettings.java
@@ -93,15 +93,26 @@
         public static final String TABLE_NAME = "favorites";
 
         /**
-         * Backup table created when when the favorites table is modified during grid migration
+         * Backup table created when the favorites table is modified during grid migration
          */
         public static final String BACKUP_TABLE_NAME = "favorites_bakup";
 
         /**
-         * The content:// style URL for this table
+         * Temporary table used specifically for grid migrations during wallpaper preview
          */
-        public static final Uri CONTENT_URI = Uri.parse("content://" +
-                LauncherProvider.AUTHORITY + "/" + TABLE_NAME);
+        public static final String PREVIEW_TABLE_NAME = "favorites_preview";
+
+        /**
+         * The content:// style URL for "favorites" table
+         */
+        public static final Uri CONTENT_URI = Uri.parse("content://"
+                + LauncherProvider.AUTHORITY + "/" + TABLE_NAME);
+
+        /**
+         * The content:// style URL for "favorites_preview" table
+         */
+        public static final Uri PREVIEW_CONTENT_URI = Uri.parse("content://"
+                + LauncherProvider.AUTHORITY + "/" + PREVIEW_TABLE_NAME);
 
         /**
          * The content:// style URL for a given row, identified by its id.
@@ -111,8 +122,8 @@
          * @return The unique content URL for the specified row.
          */
         public static Uri getContentUri(int id) {
-            return Uri.parse("content://" + LauncherProvider.AUTHORITY +
-                    "/" + TABLE_NAME + "/" + id);
+            return Uri.parse("content://" + LauncherProvider.AUTHORITY
+                    + "/" + TABLE_NAME + "/" + id);
         }
 
         /**
diff --git a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
index a429af2..0927b26 100644
--- a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
+++ b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
@@ -20,14 +20,19 @@
 import static android.view.View.VISIBLE;
 
 import static com.android.launcher3.config.FeatureFlags.ENABLE_LAUNCHER_PREVIEW_IN_GRID_PICKER;
+import static com.android.launcher3.config.FeatureFlags.MULTI_DB_GRID_MIRATION_ALGO;
+import static com.android.launcher3.model.GridSizeMigrationTask.needsToMigrate;
 import static com.android.launcher3.model.ModelUtils.filterCurrentWorkspaceItems;
 import static com.android.launcher3.model.ModelUtils.sortWorkspaceItemsSpatially;
+import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
 
 import android.annotation.TargetApi;
 import android.app.Fragment;
 import android.appwidget.AppWidgetHostView;
 import android.content.Context;
+import android.content.ContextWrapper;
 import android.content.Intent;
+import android.content.pm.ShortcutInfo;
 import android.content.res.TypedArray;
 import android.graphics.Bitmap;
 import android.graphics.Canvas;
@@ -70,17 +75,31 @@
 import com.android.launcher3.icons.BaseIconFactory;
 import com.android.launcher3.icons.BitmapInfo;
 import com.android.launcher3.icons.BitmapRenderer;
+import com.android.launcher3.icons.LauncherIcons;
 import com.android.launcher3.model.AllAppsList;
 import com.android.launcher3.model.BgDataModel;
 import com.android.launcher3.model.BgDataModel.Callbacks;
+import com.android.launcher3.model.GridSizeMigrationTask;
+import com.android.launcher3.model.GridSizeMigrationTaskV2;
 import com.android.launcher3.model.LoaderResults;
+import com.android.launcher3.model.LoaderTask;
 import com.android.launcher3.model.WidgetItem;
 import com.android.launcher3.model.WidgetsModel;
+import com.android.launcher3.pm.InstallSessionHelper;
+import com.android.launcher3.pm.UserCache;
+import com.android.launcher3.util.MainThreadInitializedObject;
 import com.android.launcher3.views.ActivityContext;
 import com.android.launcher3.views.BaseDragLayer;
 
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
 import java.util.concurrent.Callable;
+import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Executor;
@@ -101,6 +120,81 @@
 
     private static final String TAG = "LauncherPreviewRenderer";
 
+    /**
+     * Context used just for preview. It also provides a few objects (e.g. UserCache) just for
+     * preview purposes.
+     */
+    public static class PreviewContext extends ContextWrapper {
+
+        private static final Set<MainThreadInitializedObject> WHITELIST = new HashSet<>(
+                Arrays.asList(UserCache.INSTANCE, InstallSessionHelper.INSTANCE,
+                        LauncherAppState.INSTANCE, InvariantDeviceProfile.INSTANCE));
+
+        private final InvariantDeviceProfile mIdp;
+        private final Map<MainThreadInitializedObject, Object> mObjectMap = new HashMap<>();
+        private final ConcurrentLinkedQueue<LauncherIconsForPreview> mIconPool =
+                new ConcurrentLinkedQueue<>();
+
+        public PreviewContext(Context base, InvariantDeviceProfile idp) {
+            super(base);
+            mIdp = idp;
+        }
+
+        @Override
+        public Context getApplicationContext() {
+            return this;
+        }
+
+        /**
+         * Find a cached object from mObjectMap if we have already created one. If not, generate
+         * an object using the provider.
+         */
+        public <T> T getObject(MainThreadInitializedObject<T> mainThreadInitializedObject,
+                MainThreadInitializedObject.ObjectProvider<T> provider) {
+            if (!WHITELIST.contains(mainThreadInitializedObject)) {
+                throw new IllegalStateException("Leaking unknown objects");
+            }
+            if (mainThreadInitializedObject == LauncherAppState.INSTANCE) {
+                throw new IllegalStateException(
+                        "Should not use MainThreadInitializedObject to initialize this with "
+                                + "PreviewContext");
+            }
+            if (mainThreadInitializedObject == InvariantDeviceProfile.INSTANCE) {
+                return (T) mIdp;
+            }
+            if (mObjectMap.containsKey(mainThreadInitializedObject)) {
+                return (T) mObjectMap.get(mainThreadInitializedObject);
+            }
+            T t = provider.get(this);
+            mObjectMap.put(mainThreadInitializedObject, t);
+            return t;
+        }
+
+        public LauncherIcons newLauncherIcons(Context context, boolean shapeDetection) {
+            LauncherIconsForPreview launcherIconsForPreview = mIconPool.poll();
+            if (launcherIconsForPreview != null) {
+                return launcherIconsForPreview;
+            }
+            return new LauncherIconsForPreview(context, mIdp.fillResIconDpi, mIdp.iconBitmapSize,
+                    -1 /* poolId */, shapeDetection);
+        }
+
+        private final class LauncherIconsForPreview extends LauncherIcons {
+
+            private LauncherIconsForPreview(Context context, int fillResIconDpi, int iconBitmapSize,
+                    int poolId, boolean shapeDetection) {
+                super(context, fillResIconDpi, iconBitmapSize, poolId, shapeDetection);
+            }
+
+            @Override
+            public void recycle() {
+                // Clear any temporary state variables
+                clear();
+                mIconPool.offer(this);
+            }
+        }
+    }
+
     private final Handler mUiHandler;
     private final Context mContext;
     private final InvariantDeviceProfile mIdp;
@@ -282,15 +376,28 @@
 
         private void renderScreenShot(Canvas canvas) {
             if (ENABLE_LAUNCHER_PREVIEW_IN_GRID_PICKER.get()) {
-                final LauncherModel launcherModel = LauncherAppState.getInstance(
-                        mContext).getModel();
-                final WorkspaceItemsInfoFetcher fetcher = new WorkspaceItemsInfoFetcher();
-                launcherModel.enqueueModelUpdateTask(fetcher);
-                WorkspaceResult workspaceResult;
-                try {
-                    workspaceResult = fetcher.mTask.get(5, TimeUnit.SECONDS);
-                } catch (InterruptedException | ExecutionException | TimeoutException e) {
-                    Log.d(TAG, "Error fetching workspace items info", e);
+                boolean needsToMigrate = needsToMigrate(mContext, mIdp);
+                boolean success = false;
+                if (needsToMigrate) {
+                    success = MULTI_DB_GRID_MIRATION_ALGO.get()
+                            ? GridSizeMigrationTaskV2.migrateGridIfNeeded(mContext, mIdp)
+                            : GridSizeMigrationTask.migrateGridIfNeeded(mContext, mIdp);
+                }
+
+                WorkspaceFetcher fetcher;
+                if (needsToMigrate && success) {
+                    LauncherAppState appForPreview = new LauncherAppState(
+                            new PreviewContext(mContext, mIdp), null /* iconCacheFileName */);
+                    fetcher = new WorkspaceItemsInfoFromPreviewFetcher(appForPreview);
+                    MODEL_EXECUTOR.execute(fetcher);
+                } else {
+                    fetcher = new WorkspaceItemsInfoFetcher();
+                    LauncherAppState.getInstance(mContext).getModel().enqueueModelUpdateTask(
+                            (LauncherModel.ModelUpdateTask) fetcher);
+                }
+                WorkspaceResult workspaceResult = fetcher.get();
+
+                if (workspaceResult == null) {
                     return;
                 }
 
@@ -379,8 +486,13 @@
         }
     }
 
-    private static class WorkspaceItemsInfoFetcher implements Callable<WorkspaceResult>,
-            LauncherModel.ModelUpdateTask {
+    private static void measureView(View view, int width, int height) {
+        view.measure(makeMeasureSpec(width, EXACTLY), makeMeasureSpec(height, EXACTLY));
+        view.layout(0, 0, width, height);
+    }
+
+    private static class WorkspaceItemsInfoFetcher implements LauncherModel.ModelUpdateTask,
+            WorkspaceFetcher {
 
         private final FutureTask<WorkspaceResult> mTask = new FutureTask<>(this);
 
@@ -399,6 +511,11 @@
         }
 
         @Override
+        public FutureTask<WorkspaceResult> getTask() {
+            return mTask;
+        }
+
+        @Override
         public void run() {
             mTask.run();
         }
@@ -417,9 +534,45 @@
         }
     }
 
-    private static void measureView(View view, int width, int height) {
-        view.measure(makeMeasureSpec(width, EXACTLY), makeMeasureSpec(height, EXACTLY));
-        view.layout(0, 0, width, height);
+    private static class WorkspaceItemsInfoFromPreviewFetcher extends LoaderTask implements
+            WorkspaceFetcher {
+
+        private final FutureTask<WorkspaceResult> mTask = new FutureTask<>(this);
+
+        WorkspaceItemsInfoFromPreviewFetcher(LauncherAppState app) {
+            super(app, null, new BgDataModel(), null);
+        }
+
+        @Override
+        public FutureTask<WorkspaceResult> getTask() {
+            return mTask;
+        }
+
+        @Override
+        public void run() {
+            mTask.run();
+        }
+
+        @Override
+        public WorkspaceResult call() throws Exception {
+            List<ShortcutInfo> allShortcuts = new ArrayList<>();
+            loadWorkspace(allShortcuts, LauncherSettings.Favorites.PREVIEW_CONTENT_URI);
+            return new WorkspaceResult(mBgDataModel.workspaceItems, mBgDataModel.appWidgets,
+                    mBgDataModel.widgetsModel);
+        }
+    }
+
+    private interface WorkspaceFetcher extends Runnable, Callable<WorkspaceResult> {
+        FutureTask<WorkspaceResult> getTask();
+
+        default WorkspaceResult get() {
+            try {
+                return getTask().get(5, TimeUnit.SECONDS);
+            } catch (InterruptedException | ExecutionException | TimeoutException e) {
+                Log.d(TAG, "Error fetching workspace items info", e);
+                return null;
+            }
+        }
     }
 
     private static class WorkspaceResult {
diff --git a/src/com/android/launcher3/icons/IconCache.java b/src/com/android/launcher3/icons/IconCache.java
index 804acc3..f27c9da 100644
--- a/src/com/android/launcher3/icons/IconCache.java
+++ b/src/com/android/launcher3/icons/IconCache.java
@@ -81,9 +81,13 @@
 
     private int mPendingIconRequestCount = 0;
 
-    public IconCache(Context context, InvariantDeviceProfile inv) {
-        super(context, LauncherFiles.APP_ICONS_DB, MODEL_EXECUTOR.getLooper(),
-                inv.fillResIconDpi, inv.iconBitmapSize, true /* inMemoryCache */);
+    public IconCache(Context context, InvariantDeviceProfile idp) {
+        this(context, idp, LauncherFiles.APP_ICONS_DB);
+    }
+
+    public IconCache(Context context, InvariantDeviceProfile idp, String dbFileName) {
+        super(context, dbFileName, MODEL_EXECUTOR.getLooper(),
+                idp.fillResIconDpi, idp.iconBitmapSize, true /* inMemoryCache */);
         mComponentWithLabelCachingLogic = new ComponentCachingLogic(context, false);
         mLauncherActivityInfoCachingLogic = LauncherActivityCachingLogic.newInstance(context);
         mShortcutCachingLogic = new ShortcutCachingLogic();
diff --git a/src/com/android/launcher3/icons/LauncherIcons.java b/src/com/android/launcher3/icons/LauncherIcons.java
index cbd7c53..bf7897e 100644
--- a/src/com/android/launcher3/icons/LauncherIcons.java
+++ b/src/com/android/launcher3/icons/LauncherIcons.java
@@ -19,8 +19,8 @@
 import android.content.Context;
 
 import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.graphics.IconShape;
+import com.android.launcher3.graphics.LauncherPreviewRenderer;
 
 /**
  * Wrapper class to provide access to {@link BaseIconFactory} and also to provide pool of this class
@@ -41,6 +41,11 @@
      * avoid allocating new objects in many cases.
      */
     public static LauncherIcons obtain(Context context, boolean shapeDetection) {
+        if (context instanceof LauncherPreviewRenderer.PreviewContext) {
+            return ((LauncherPreviewRenderer.PreviewContext) context).newLauncherIcons(context,
+                    shapeDetection);
+        }
+
         int poolId;
         synchronized (sPoolSync) {
             if (sPool != null) {
@@ -52,7 +57,7 @@
             poolId = sPoolId;
         }
 
-        InvariantDeviceProfile idp = LauncherAppState.getIDP(context);
+        InvariantDeviceProfile idp = InvariantDeviceProfile.INSTANCE.get(context);
         return new LauncherIcons(context, idp.fillResIconDpi, idp.iconBitmapSize, poolId,
                 shapeDetection);
     }
@@ -68,7 +73,7 @@
 
     private LauncherIcons next;
 
-    private LauncherIcons(Context context, int fillResIconDpi, int iconBitmapSize, int poolId,
+    protected LauncherIcons(Context context, int fillResIconDpi, int iconBitmapSize, int poolId,
             boolean shapeDetection) {
         super(context, fillResIconDpi, iconBitmapSize, shapeDetection);
         mPoolId = poolId;
diff --git a/src/com/android/launcher3/model/GridBackupTable.java b/src/com/android/launcher3/model/GridBackupTable.java
index 07a7551..4a1bc4d 100644
--- a/src/com/android/launcher3/model/GridBackupTable.java
+++ b/src/com/android/launcher3/model/GridBackupTable.java
@@ -100,12 +100,24 @@
                     Process.myUserHandle()), 0);
             return false;
         }
+        return restoreIfBackupExists(Favorites.TABLE_NAME);
+    }
+
+    public boolean restoreToPreviewIfBackupExists() {
+        if (!tableExists(mDb, BACKUP_TABLE_NAME)) {
+            return false;
+        }
+
+        return restoreIfBackupExists(Favorites.PREVIEW_TABLE_NAME);
+    }
+
+    private boolean restoreIfBackupExists(String toTableName) {
         if (loadDBProperties() != STATE_SANITIZED) {
             return false;
         }
         long userSerial = UserCache.INSTANCE.get(mContext).getSerialNumberForUser(
                 Process.myUserHandle());
-        copyTable(mDb, BACKUP_TABLE_NAME, Favorites.TABLE_NAME, userSerial);
+        copyTable(mDb, BACKUP_TABLE_NAME, toTableName, userSerial);
         Log.d(TAG, "Backup table found");
         return true;
     }
diff --git a/src/com/android/launcher3/model/GridSizeMigrationTask.java b/src/com/android/launcher3/model/GridSizeMigrationTask.java
index c35c4b9..3ba740d 100644
--- a/src/com/android/launcher3/model/GridSizeMigrationTask.java
+++ b/src/com/android/launcher3/model/GridSizeMigrationTask.java
@@ -3,6 +3,7 @@
 import static com.android.launcher3.LauncherSettings.Settings.EXTRA_VALUE;
 import static com.android.launcher3.Utilities.getPointString;
 import static com.android.launcher3.Utilities.parsePoint;
+import static com.android.launcher3.provider.LauncherDbUtils.copyTable;
 
 import android.content.ComponentName;
 import android.content.ContentValues;
@@ -14,6 +15,7 @@
 import android.database.Cursor;
 import android.database.sqlite.SQLiteDatabase;
 import android.graphics.Point;
+import android.os.SystemClock;
 import android.util.Log;
 import android.util.SparseArray;
 
@@ -29,6 +31,7 @@
 import com.android.launcher3.Utilities;
 import com.android.launcher3.Workspace;
 import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.graphics.LauncherPreviewRenderer;
 import com.android.launcher3.pm.InstallSessionHelper;
 import com.android.launcher3.provider.LauncherDbUtils;
 import com.android.launcher3.provider.LauncherDbUtils.SQLiteTransaction;
@@ -69,6 +72,7 @@
 
     private final SparseArray<ContentValues> mUpdateOperations = new SparseArray<>();
     private final HashSet<String> mValidPackages;
+    private final String mTableName;
 
     private final int mSrcX, mSrcY;
     private final int mTrgX, mTrgY;
@@ -78,10 +82,12 @@
     private final int mDestHotseatSize;
 
     protected GridSizeMigrationTask(Context context, SQLiteDatabase db,
-            HashSet<String> validPackages, Point sourceSize, Point targetSize) {
+            HashSet<String> validPackages, boolean usePreviewTable, Point sourceSize,
+            Point targetSize) {
         mContext = context;
         mDb = db;
         mValidPackages = validPackages;
+        mTableName = usePreviewTable ? Favorites.PREVIEW_TABLE_NAME : Favorites.TABLE_NAME;
 
         mSrcX = sourceSize.x;
         mSrcY = sourceSize.y;
@@ -97,10 +103,12 @@
     }
 
     protected GridSizeMigrationTask(Context context, SQLiteDatabase db,
-            HashSet<String> validPackages, int srcHotseatSize, int destHotseatSize) {
+            HashSet<String> validPackages, boolean usePreviewTable, int srcHotseatSize,
+            int destHotseatSize) {
         mContext = context;
         mDb = db;
         mValidPackages = validPackages;
+        mTableName = usePreviewTable ? Favorites.PREVIEW_TABLE_NAME : Favorites.TABLE_NAME;
 
         mSrcHotseatSize = srcHotseatSize;
 
@@ -120,7 +128,7 @@
         // Update items
         int updateCount = mUpdateOperations.size();
         for (int i = 0; i < updateCount; i++) {
-            mDb.update(Favorites.TABLE_NAME, mUpdateOperations.valueAt(i),
+            mDb.update(mTableName, mUpdateOperations.valueAt(i),
                     "_id=" + mUpdateOperations.keyAt(i), null);
         }
 
@@ -128,8 +136,8 @@
             if (DEBUG) {
                 Log.d(TAG, "Removing items: " + mEntryToRemove.toConcatString());
             }
-            mDb.delete(Favorites.TABLE_NAME, Utilities.createDbSelectionQuery(
-                    Favorites._ID, mEntryToRemove), null);
+            mDb.delete(mTableName, Utilities.createDbSelectionQuery(Favorites._ID, mEntryToRemove),
+                    null);
         }
 
         return updateCount > 0 || !mEntryToRemove.isEmpty();
@@ -182,8 +190,8 @@
     }
 
     @VisibleForTesting
-    static IntArray getWorkspaceScreenIds(SQLiteDatabase db) {
-        return LauncherDbUtils.queryIntArray(db, Favorites.TABLE_NAME, Favorites.SCREEN,
+    static IntArray getWorkspaceScreenIds(SQLiteDatabase db, String tableName) {
+        return LauncherDbUtils.queryIntArray(db, tableName, Favorites.SCREEN,
                 Favorites.CONTAINER + " = " + Favorites.CONTAINER_DESKTOP,
                 Favorites.SCREEN, Favorites.SCREEN);
     }
@@ -192,7 +200,7 @@
      * @return true if any DB change was made
      */
     protected boolean migrateWorkspace() throws Exception {
-        IntArray allScreens = getWorkspaceScreenIds(mDb);
+        IntArray allScreens = getWorkspaceScreenIds(mDb, mTableName);
         if (allScreens.isEmpty()) {
             throw new Exception("Unable to get workspace screens");
         }
@@ -244,12 +252,12 @@
     /**
      * Migrate a particular screen id.
      * Strategy:
-     *   1) For all possible combinations of row and column, pick the one which causes the least
-     *      data loss: {@link #tryRemove(int, int, int, ArrayList, float[])}
-     *   2) Maintain a list of all lost items before this screen, and add any new item lost from
-     *      this screen to that list as well.
-     *   3) If all those items from the above list can be placed on this screen, place them
-     *      (otherwise they are placed on a new screen).
+     *  1) For all possible combinations of row and column, pick the one which causes the least
+     *    data loss: {@link #tryRemove(int, int, int, ArrayList, float[])}
+     *  2) Maintain a list of all lost items before this screen, and add any new item lost from
+     *    this screen to that list as well.
+     *  3) If all those items from the above list can be placed on this screen, place them
+     *    (otherwise they are placed on a new screen).
      */
     protected void migrateScreen(int screenId) {
         // If we are migrating the first screen, do not touch the first row.
@@ -362,9 +370,9 @@
     /**
      * Tries the remove the provided row and column.
      *
-     * @param items all the items on the screen under operation
+     * @param items   all the items on the screen under operation
      * @param outLoss array of size 2. The first entry is filled with weight loss, and the second
-     * with the overall item movement.
+     *                with the overall item movement.
      */
     private ArrayList<DbEntry> tryRemove(int col, int row, int startY,
             ArrayList<DbEntry> items, float[] outLoss) {
@@ -379,13 +387,13 @@
 
         for (DbEntry item : items) {
             if ((item.cellX <= col && (item.spanX + item.cellX) > col)
-                || (item.cellY <= row && (item.spanY + item.cellY) > row)) {
+                    || (item.cellY <= row && (item.spanY + item.cellY) > row)) {
                 removedItems.add(item);
-                if (item.cellX >= col) item.cellX --;
-                if (item.cellY >= row) item.cellY --;
+                if (item.cellX >= col) item.cellX--;
+                if (item.cellY >= row) item.cellY--;
             } else {
-                if (item.cellX > col) item.cellX --;
-                if (item.cellY > row) item.cellY --;
+                if (item.cellX > col) item.cellX--;
+                if (item.cellY > row) item.cellY--;
                 finalItems.add(item);
                 occupied.markCells(item, true);
             }
@@ -438,9 +446,9 @@
         /**
          * Recursively finds a placement for the provided items.
          *
-         * @param index the position in {@link #itemsToPlace} to start looking at.
-         * @param weightLoss total weight loss upto this point
-         * @param moveCost total move cost upto this point
+         * @param index       the position in {@link #itemsToPlace} to start looking at.
+         * @param weightLoss  total weight loss upto this point
+         * @param moveCost    total move cost upto this point
          * @param itemsPlaced all the items already placed upto this point
          */
         public void find(int index, float weightLoss, float moveCost,
@@ -481,11 +489,11 @@
                         float newMoveCost = moveCost;
                         if (x != myX) {
                             me.cellX = x;
-                            newMoveCost ++;
+                            newMoveCost++;
                         }
                         if (y != myY) {
                             me.cellY = y;
-                            newMoveCost ++;
+                            newMoveCost++;
                         }
                         if (ignoreMove) {
                             newMoveCost = moveCost;
@@ -500,35 +508,35 @@
 
                         // Try resizing horizontally
                         if (myW > me.minSpanX && occupied.isRegionVacant(x, y, myW - 1, myH)) {
-                            me.spanX --;
+                            me.spanX--;
                             occupied.markCells(me, true);
                             // 1 extra move cost
                             find(index + 1, weightLoss, newMoveCost + 1, itemsIncludingMe);
                             occupied.markCells(me, false);
-                            me.spanX ++;
+                            me.spanX++;
                         }
 
                         // Try resizing vertically
                         if (myH > me.minSpanY && occupied.isRegionVacant(x, y, myW, myH - 1)) {
-                            me.spanY --;
+                            me.spanY--;
                             occupied.markCells(me, true);
                             // 1 extra move cost
                             find(index + 1, weightLoss, newMoveCost + 1, itemsIncludingMe);
                             occupied.markCells(me, false);
-                            me.spanY ++;
+                            me.spanY++;
                         }
 
                         // Try resizing horizontally & vertically
                         if (myH > me.minSpanY && myW > me.minSpanX &&
                                 occupied.isRegionVacant(x, y, myW - 1, myH - 1)) {
-                            me.spanX --;
-                            me.spanY --;
+                            me.spanX--;
+                            me.spanY--;
                             occupied.markCells(me, true);
                             // 2 extra move cost
                             find(index + 1, weightLoss, newMoveCost + 2, itemsIncludingMe);
                             occupied.markCells(me, false);
-                            me.spanX ++;
-                            me.spanY ++;
+                            me.spanX++;
+                            me.spanY++;
                         }
                         me.cellX = myX;
                         me.cellY = myY;
@@ -565,11 +573,11 @@
                     float newMoveCost = moveCost;
                     if (newX != myX) {
                         me.cellX = newX;
-                        newMoveCost ++;
+                        newMoveCost++;
                     }
                     if (newY != myY) {
                         me.cellY = newY;
-                        newMoveCost ++;
+                        newMoveCost++;
                     }
                     if (ignoreMove) {
                         newMoveCost = moveCost;
@@ -602,7 +610,7 @@
     }
 
     private ArrayList<DbEntry> loadHotseatEntries() {
-        Cursor c =  queryWorkspace(
+        Cursor c = queryWorkspace(
                 new String[]{
                         Favorites._ID,                  // 0
                         Favorites.ITEM_TYPE,            // 1
@@ -787,7 +795,7 @@
     }
 
     protected Cursor queryWorkspace(String[] columns, String where) {
-        return mDb.query(Favorites.TABLE_NAME, columns, where, null, null, null, null);
+        return mDb.query(mTableName, columns, where, null, null, null, null);
     }
 
     /**
@@ -879,24 +887,44 @@
     }
 
     /**
-     * Migrates the workspace and hotseat in case their sizes changed.
+     * Check given a new IDP, if migration is necessary.
+     */
+    public static boolean needsToMigrate(Context context, InvariantDeviceProfile idp) {
+        SharedPreferences prefs = Utilities.getPrefs(context);
+        String gridSizeString = getPointString(idp.numColumns, idp.numRows);
+
+        return !gridSizeString.equals(prefs.getString(KEY_MIGRATION_SRC_WORKSPACE_SIZE, ""))
+                || idp.numHotseatIcons != prefs.getInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT,
+                idp.numHotseatIcons);
+    }
+
+    /** See {@link #migrateGridIfNeeded(Context, InvariantDeviceProfile)} */
+    public static boolean migrateGridIfNeeded(Context context) {
+        if (context instanceof LauncherPreviewRenderer.PreviewContext) {
+            return true;
+        }
+        return migrateGridIfNeeded(context, null);
+    }
+
+    /**
+     * Run the migration algorithm if needed. For preview, we provide the intended idp because it
+     * has not been changed. If idp is null, we read it from the context, for actual grid migration.
      *
      * @return false if the migration failed.
      */
-    public static boolean migrateGridIfNeeded(Context context) {
-        SharedPreferences prefs = Utilities.getPrefs(context);
-        InvariantDeviceProfile idp = LauncherAppState.getIDP(context);
+    public static boolean migrateGridIfNeeded(Context context, InvariantDeviceProfile idp) {
+        boolean migrateForPreview = idp != null;
+        if (!migrateForPreview) {
+            idp = LauncherAppState.getIDP(context);
+        }
 
-        String gridSizeString = getPointString(idp.numColumns, idp.numRows);
-
-        if (gridSizeString.equals(prefs.getString(KEY_MIGRATION_SRC_WORKSPACE_SIZE, "")) &&
-                idp.numHotseatIcons == prefs.getInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT,
-                        idp.numHotseatIcons)) {
-            // Skip if workspace and hotseat sizes have not changed.
+        if (!needsToMigrate(context, idp)) {
             return true;
         }
 
-        long migrationStartTime = System.currentTimeMillis();
+        SharedPreferences prefs = Utilities.getPrefs(context);
+        String gridSizeString = getPointString(idp.numColumns, idp.numRows);
+        long migrationStartTime = SystemClock.elapsedRealtime();
         try (SQLiteTransaction transaction = (SQLiteTransaction) Settings.call(
                 context.getContentResolver(), Settings.METHOD_NEW_TRANSACTION)
                 .getBinder(Settings.EXTRA_VALUE)) {
@@ -907,33 +935,39 @@
                     KEY_MIGRATION_SRC_WORKSPACE_SIZE, gridSizeString));
 
             boolean dbChanged = false;
+            if (migrateForPreview) {
+                copyTable(transaction.getDb(), Favorites.TABLE_NAME, Favorites.PREVIEW_TABLE_NAME,
+                        context);
+            }
 
             GridBackupTable backupTable = new GridBackupTable(context, transaction.getDb(),
                     srcHotseatCount, sourceSize.x, sourceSize.y);
-            if (backupTable.backupOrRestoreAsNeeded()) {
+            if (migrateForPreview ? backupTable.restoreToPreviewIfBackupExists()
+                    : backupTable.backupOrRestoreAsNeeded()) {
                 dbChanged = true;
                 srcHotseatCount = backupTable.getRestoreHotseatAndGridSize(sourceSize);
             }
 
             HashSet<String> validPackages = getValidPackages(context);
-            // Hotseat
+            // Hotseat.
             if (srcHotseatCount != idp.numHotseatIcons) {
                 // Migrate hotseat.
-                dbChanged = new GridSizeMigrationTask(context, transaction.getDb(),
-                        validPackages, srcHotseatCount, idp.numHotseatIcons).migrateHotseat();
+                dbChanged = new GridSizeMigrationTask(context, transaction.getDb(), validPackages,
+                        migrateForPreview, srcHotseatCount, idp.numHotseatIcons).migrateHotseat();
             }
 
             // Grid size
             Point targetSize = new Point(idp.numColumns, idp.numRows);
-            if (new MultiStepMigrationTask(validPackages, context, transaction.getDb())
-                    .migrate(sourceSize, targetSize)) {
+            if (new MultiStepMigrationTask(validPackages, context, transaction.getDb(),
+                    migrateForPreview).migrate(sourceSize, targetSize)) {
                 dbChanged = true;
             }
 
             if (dbChanged) {
                 // Make sure we haven't removed everything.
                 final Cursor c = context.getContentResolver().query(
-                        Favorites.CONTENT_URI, null, null, null, null);
+                        migrateForPreview ? Favorites.PREVIEW_CONTENT_URI : Favorites.CONTENT_URI,
+                        null, null, null, null);
                 boolean hasData = c.moveToNext();
                 c.close();
                 if (!hasData) {
@@ -942,21 +976,25 @@
             }
 
             transaction.commit();
-            Settings.call(context.getContentResolver(), Settings.METHOD_REFRESH_BACKUP_TABLE);
+            if (!migrateForPreview) {
+                Settings.call(context.getContentResolver(), Settings.METHOD_REFRESH_BACKUP_TABLE);
+            }
             return true;
         } catch (Exception e) {
-            Log.e(TAG, "Error during grid migration", e);
+            Log.e(TAG, "Error during preview grid migration", e);
 
             return false;
         } finally {
-            Log.v(TAG, "Workspace migration completed in "
-                    + (System.currentTimeMillis() - migrationStartTime));
+            Log.v(TAG, "Preview workspace migration completed in "
+                    + (SystemClock.elapsedRealtime() - migrationStartTime));
 
-            // Save current configuration, so that the migration does not run again.
-            prefs.edit()
-                    .putString(KEY_MIGRATION_SRC_WORKSPACE_SIZE, gridSizeString)
-                    .putInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT, idp.numHotseatIcons)
-                    .apply();
+            if (!migrateForPreview) {
+                // Save current configuration, so that the migration does not run again.
+                prefs.edit()
+                        .putString(KEY_MIGRATION_SRC_WORKSPACE_SIZE, gridSizeString)
+                        .putInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT, idp.numHotseatIcons)
+                        .apply();
+            }
         }
     }
 
@@ -989,7 +1027,7 @@
                 .getBinder(Settings.EXTRA_VALUE)) {
             GridSizeMigrationTask task = new GridSizeMigrationTask(
                     context, transaction.getDb(), getValidPackages(context),
-                    Integer.MAX_VALUE, Integer.MAX_VALUE);
+                    false /* usePreviewTable */, Integer.MAX_VALUE, Integer.MAX_VALUE);
 
             // Load all the valid entries
             ArrayList<DbEntry> items = task.loadHotseatEntries();
@@ -1011,12 +1049,14 @@
         private final HashSet<String> mValidPackages;
         private final Context mContext;
         private final SQLiteDatabase mDb;
+        private final boolean mUsePreviewTable;
 
         public MultiStepMigrationTask(HashSet<String> validPackages, Context context,
-                SQLiteDatabase db) {
+                SQLiteDatabase db, boolean usePreviewTable) {
             mValidPackages = validPackages;
             mContext = context;
             mDb = db;
+            mUsePreviewTable = usePreviewTable;
         }
 
         public boolean migrate(Point sourceSize, Point targetSize) throws Exception {
@@ -1052,8 +1092,8 @@
         }
 
         protected boolean runStepTask(Point sourceSize, Point nextSize) throws Exception {
-            return new GridSizeMigrationTask(mContext, mDb,
-                    mValidPackages, sourceSize, nextSize).migrateWorkspace();
+            return new GridSizeMigrationTask(mContext, mDb, mValidPackages, mUsePreviewTable,
+                    sourceSize, nextSize).migrateWorkspace();
         }
     }
 }
diff --git a/src/com/android/launcher3/model/GridSizeMigrationTaskV2.java b/src/com/android/launcher3/model/GridSizeMigrationTaskV2.java
index 63b7191..197b29c 100644
--- a/src/com/android/launcher3/model/GridSizeMigrationTaskV2.java
+++ b/src/com/android/launcher3/model/GridSizeMigrationTaskV2.java
@@ -18,6 +18,8 @@
 
 import android.content.Context;
 
+import com.android.launcher3.InvariantDeviceProfile;
+
 /**
  * This class takes care of shrinking the workspace (by maximum of one row and one column), as a
  * result of restoring from a larger device or device density change.
@@ -28,12 +30,19 @@
 
     }
 
+    /** See {@link #migrateGridIfNeeded(Context, InvariantDeviceProfile)} */
+    public static boolean migrateGridIfNeeded(Context context) {
+        // To be implemented.
+        return true;
+    }
+
     /**
-     * Migrates the workspace and hotseat in case their sizes changed.
+     * Run the migration algorithm if needed. For preview, we provide the intended idp because it
+     * has not been changed. If idp is null, we read it from the context, for actual grid migration.
      *
      * @return false if the migration failed.
      */
-    public static boolean migrateGridIfNeeded(Context context) {
+    public static boolean migrateGridIfNeeded(Context context, InvariantDeviceProfile idp) {
         // To be implemented.
         return true;
     }
diff --git a/src/com/android/launcher3/model/LoaderCursor.java b/src/com/android/launcher3/model/LoaderCursor.java
index 4961432..2311dcc 100644
--- a/src/com/android/launcher3/model/LoaderCursor.java
+++ b/src/com/android/launcher3/model/LoaderCursor.java
@@ -28,6 +28,7 @@
 import android.content.pm.PackageManager;
 import android.database.Cursor;
 import android.database.CursorWrapper;
+import android.net.Uri;
 import android.os.UserHandle;
 import android.provider.BaseColumns;
 import android.text.TextUtils;
@@ -64,6 +65,7 @@
 
     public final LongSparseArray<UserHandle> allUsers = new LongSparseArray<>();
 
+    private final Uri mContentUri;
     private final Context mContext;
     private final PackageManager mPM;
     private final IconCache mIconCache;
@@ -96,8 +98,10 @@
     public int itemType;
     public int restoreFlag;
 
-    public LoaderCursor(Cursor c, LauncherAppState app) {
-        super(c);
+    public LoaderCursor(Cursor cursor, Uri contentUri, LauncherAppState app) {
+        super(cursor);
+
+        mContentUri = contentUri;
         mContext = app.getContext();
         mIconCache = app.getIconCache();
         mIDP = app.getInvariantDeviceProfile();
@@ -312,9 +316,8 @@
     public boolean commitDeleted() {
         if (itemsToRemove.size() > 0) {
             // Remove dead items
-            mContext.getContentResolver().delete(LauncherSettings.Favorites.CONTENT_URI,
-                    Utilities.createDbSelectionQuery(
-                            LauncherSettings.Favorites._ID, itemsToRemove), null);
+            mContext.getContentResolver().delete(mContentUri, Utilities.createDbSelectionQuery(
+                    LauncherSettings.Favorites._ID, itemsToRemove), null);
             return true;
         }
         return false;
@@ -339,7 +342,7 @@
             // Update restored items that no longer require special handling
             ContentValues values = new ContentValues();
             values.put(LauncherSettings.Favorites.RESTORED, 0);
-            mContext.getContentResolver().update(LauncherSettings.Favorites.CONTENT_URI, values,
+            mContext.getContentResolver().update(mContentUri, values,
                     Utilities.createDbSelectionQuery(
                             LauncherSettings.Favorites._ID, restoredRows), null);
         }
diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java
index 6223a23..fbf01fc 100644
--- a/src/com/android/launcher3/model/LoaderTask.java
+++ b/src/com/android/launcher3/model/LoaderTask.java
@@ -36,6 +36,7 @@
 import android.content.pm.PackageInstaller;
 import android.content.pm.PackageInstaller.SessionInfo;
 import android.content.pm.ShortcutInfo;
+import android.net.Uri;
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.text.TextUtils;
@@ -76,7 +77,6 @@
 import com.android.launcher3.shortcuts.ShortcutKey;
 import com.android.launcher3.shortcuts.ShortcutRequest;
 import com.android.launcher3.shortcuts.ShortcutRequest.QueryResult;
-import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.IOUtils;
 import com.android.launcher3.util.LooperIdleLock;
@@ -106,7 +106,7 @@
 
     private final LauncherAppState mApp;
     private final AllAppsList mBgAllAppsList;
-    private final BgDataModel mBgDataModel;
+    protected final BgDataModel mBgDataModel;
 
     private FirstScreenBroadcast mFirstScreenBroadcast;
 
@@ -284,6 +284,10 @@
 
     @VisibleForTesting
     void loadWorkspace(List<ShortcutInfo> allDeepShortcuts) {
+        loadWorkspace(allDeepShortcuts, LauncherSettings.Favorites.CONTENT_URI);
+    }
+
+    protected void loadWorkspace(List<ShortcutInfo> allDeepShortcuts, Uri contentUri) {
         final Context context = mApp.getContext();
         final ContentResolver contentResolver = context.getContentResolver();
         final PackageManagerHelper pmHelper = new PackageManagerHelper(context);
@@ -327,8 +331,8 @@
             mFirstScreenBroadcast = new FirstScreenBroadcast(installingPkgs);
 
             Map<ShortcutKey, ShortcutInfo> shortcutKeyToPinnedShortcuts = new HashMap<>();
-            final LoaderCursor c = new LoaderCursor(contentResolver.query(
-                    LauncherSettings.Favorites.CONTENT_URI, null, null, null, null), mApp);
+            final LoaderCursor c = new LoaderCursor(
+                    contentResolver.query(contentUri, null, null, null, null), contentUri, mApp);
 
             Map<ComponentKey, AppWidgetProviderInfo> widgetProvidersMap = null;
 
diff --git a/src/com/android/launcher3/provider/LauncherDbUtils.java b/src/com/android/launcher3/provider/LauncherDbUtils.java
index 2c843f9..f7ecc3f 100644
--- a/src/com/android/launcher3/provider/LauncherDbUtils.java
+++ b/src/com/android/launcher3/provider/LauncherDbUtils.java
@@ -22,10 +22,12 @@
 import android.database.DatabaseUtils;
 import android.database.sqlite.SQLiteDatabase;
 import android.os.Binder;
+import android.os.Process;
 import android.util.Log;
 
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherSettings.Favorites;
+import com.android.launcher3.pm.UserCache;
 import com.android.launcher3.util.IntArray;
 
 import java.util.Locale;
@@ -116,6 +118,15 @@
         db.execSQL("DROP TABLE IF EXISTS " + tableName);
     }
 
+    /** Copy from table to the to table. */
+    public static void copyTable(SQLiteDatabase db, String from, String to, Context context) {
+        long userSerial = UserCache.INSTANCE.get(context).getSerialNumberForUser(
+                Process.myUserHandle());
+        dropTable(db, to);
+        Favorites.addTableToDb(db, userSerial, false, to);
+        db.execSQL("INSERT INTO " + to + " SELECT * FROM " + from);
+    }
+
     /**
      * Utility class to simplify managing sqlite transactions
      */
diff --git a/src/com/android/launcher3/provider/LossyScreenMigrationTask.java b/src/com/android/launcher3/provider/LossyScreenMigrationTask.java
index 6d839f3..c0111b9 100644
--- a/src/com/android/launcher3/provider/LossyScreenMigrationTask.java
+++ b/src/com/android/launcher3/provider/LossyScreenMigrationTask.java
@@ -18,7 +18,6 @@
 
 import android.content.ContentValues;
 import android.content.Context;
-import android.database.Cursor;
 import android.database.sqlite.SQLiteDatabase;
 import android.graphics.Point;
 
@@ -43,7 +42,7 @@
     protected LossyScreenMigrationTask(
             Context context, InvariantDeviceProfile idp, SQLiteDatabase db) {
         // Decrease the rows count by 1
-        super(context, db, getValidPackages(context),
+        super(context, db, getValidPackages(context), false /* usePreviewTable */,
                 new Point(idp.numColumns, idp.numRows + 1),
                 new Point(idp.numColumns, idp.numRows));
 
diff --git a/src/com/android/launcher3/util/MainThreadInitializedObject.java b/src/com/android/launcher3/util/MainThreadInitializedObject.java
index 520a9ed..fc9f8f7 100644
--- a/src/com/android/launcher3/util/MainThreadInitializedObject.java
+++ b/src/com/android/launcher3/util/MainThreadInitializedObject.java
@@ -22,6 +22,7 @@
 
 import androidx.annotation.VisibleForTesting;
 
+import com.android.launcher3.graphics.LauncherPreviewRenderer.PreviewContext;
 import com.android.launcher3.util.ResourceBasedOverride.Overrides;
 
 import java.util.concurrent.ExecutionException;
@@ -39,6 +40,10 @@
     }
 
     public T get(Context context) {
+        if (context instanceof PreviewContext) {
+            return ((PreviewContext) context).getObject(this, mProvider);
+        }
+
         if (mValue == null) {
             if (Looper.myLooper() == Looper.getMainLooper()) {
                 mValue = TraceHelper.whitelistIpcs("main.thread.object",