Adding logic to pull in workspace data from another Launcher3 based
provider. This allows OEMs to keep the user's homescreen intact while
changing the default home app package.

Bug: 28536314
Change-Id: Ibebfd7dd33aa2cbd9ca28d2d611dd0a4a5971444
diff --git a/src/com/android/launcher3/DefaultLayoutParser.java b/src/com/android/launcher3/DefaultLayoutParser.java
index 2bba380..e6f34d9 100644
--- a/src/com/android/launcher3/DefaultLayoutParser.java
+++ b/src/com/android/launcher3/DefaultLayoutParser.java
@@ -33,7 +33,7 @@
     private static final String TAG_FAVORITES = "favorites";
     protected static final String TAG_FAVORITE = "favorite";
     private static final String TAG_APPWIDGET = "appwidget";
-    private static final String TAG_SHORTCUT = "shortcut";
+    protected static final String TAG_SHORTCUT = "shortcut";
     private static final String TAG_FOLDER = "folder";
     private static final String TAG_PARTNER_FOLDER = "partner-folder";
 
@@ -89,7 +89,7 @@
     /**
      * AppShortcutParser which also supports adding URI based intents
      */
-    @Thunk class AppShortcutWithUriParser extends AppShortcutParser {
+    public class AppShortcutWithUriParser extends AppShortcutParser {
 
         @Override
         protected long invalidPackageOrClass(XmlResourceParser parser) {
@@ -179,7 +179,7 @@
     /**
      * Shortcut parser which allows any uri and not just web urls.
      */
-    private class UriShortcutParser extends ShortcutParser {
+    public class UriShortcutParser extends ShortcutParser {
 
         public UriShortcutParser(Resources iconRes) {
             super(iconRes);
@@ -201,7 +201,7 @@
     /**
      * Contains a list of <favorite> nodes, and accepts the first successfully parsed node.
      */
-    protected class ResolveParser implements TagParser {
+    public class ResolveParser implements TagParser {
 
         private final AppShortcutWithUriParser mChildParser = new AppShortcutWithUriParser();
 
diff --git a/src/com/android/launcher3/LauncherAppWidgetHostView.java b/src/com/android/launcher3/LauncherAppWidgetHostView.java
index d371a86..7c8bfab 100644
--- a/src/com/android/launcher3/LauncherAppWidgetHostView.java
+++ b/src/com/android/launcher3/LauncherAppWidgetHostView.java
@@ -62,8 +62,8 @@
         mLongPressHelper = new CheckLongPressHelper(this);
         mStylusEventHelper = new StylusEventHelper(new SimpleOnStylusPressListener(this), this);
         mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
-        mDragLayer = ((Launcher) context).getDragLayer();
-        setAccessibilityDelegate(((Launcher) context).getAccessibilityDelegate());
+        mDragLayer = Launcher.getLauncher(context).getDragLayer();
+        setAccessibilityDelegate(Launcher.getLauncher(context).getAccessibilityDelegate());
 
         setBackgroundResource(R.drawable.widget_internal_focus_bg);
     }
diff --git a/src/com/android/launcher3/LauncherFiles.java b/src/com/android/launcher3/LauncherFiles.java
index adb5031..9c4646b 100644
--- a/src/com/android/launcher3/LauncherFiles.java
+++ b/src/com/android/launcher3/LauncherFiles.java
@@ -14,33 +14,20 @@
 
     private static final String XML = ".xml";
 
-    public static final String DEFAULT_WALLPAPER_THUMBNAIL = "default_thumb2.jpg";
-    public static final String DEFAULT_WALLPAPER_THUMBNAIL_OLD = "default_thumb.jpg";
     public static final String LAUNCHER_DB = "launcher.db";
     public static final String SHARED_PREFERENCES_KEY = "com.android.launcher3.prefs";
-    public static final String WALLPAPER_CROP_PREFERENCES_KEY =
-            "com.android.launcher3.WallpaperCropActivity";
     public static final String MANAGED_USER_PREFERENCES_KEY = "com.android.launcher3.managedusers.prefs";
+    // This preference file is not backed up to cloud.
+    public static final String DEVICE_PREFERENCES_KEY = "com.android.launcher3.device.prefs";
 
-    public static final String WALLPAPER_IMAGES_DB = "saved_wallpaper_images.db";
     public static final String WIDGET_PREVIEWS_DB = "widgetpreviews.db";
     public static final String APP_ICONS_DB = "app_icons.db";
 
     public static final List<String> ALL_FILES = Collections.unmodifiableList(Arrays.asList(
-            DEFAULT_WALLPAPER_THUMBNAIL,
-            DEFAULT_WALLPAPER_THUMBNAIL_OLD,
             LAUNCHER_DB,
             SHARED_PREFERENCES_KEY + XML,
-            WALLPAPER_CROP_PREFERENCES_KEY + XML,
-            WALLPAPER_IMAGES_DB,
             WIDGET_PREVIEWS_DB,
             MANAGED_USER_PREFERENCES_KEY + XML,
+            DEVICE_PREFERENCES_KEY + XML,
             APP_ICONS_DB));
-
-    // TODO: Delete these files on upgrade
-    public static final List<String> OBSOLETE_FILES = Collections.unmodifiableList(Arrays.asList(
-            "launches.logTap",
-            "stats.logTap",
-            "launcher.preferences",
-            "com.android.launcher3.compat.PackageInstallerCompatV16.queue"));
 }
diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java
index 3e98c13..39e28c0 100644
--- a/src/com/android/launcher3/LauncherModel.java
+++ b/src/com/android/launcher3/LauncherModel.java
@@ -60,6 +60,7 @@
 import com.android.launcher3.logging.FileLog;
 import com.android.launcher3.model.GridSizeMigrationTask;
 import com.android.launcher3.model.WidgetsModel;
+import com.android.launcher3.provider.ImportDataTask;
 import com.android.launcher3.provider.LauncherDbUtils;
 import com.android.launcher3.shortcuts.DeepShortcutManager;
 import com.android.launcher3.shortcuts.ShortcutInfoCompat;
@@ -1648,7 +1649,14 @@
             int countY = profile.numRows;
 
             boolean clearDb = false;
-            if (GridSizeMigrationTask.ENABLED &&
+            try {
+                ImportDataTask.performImportIfPossible(context);
+            } catch (Exception e) {
+                // Migration failed. Clear workspace.
+                clearDb = true;
+            }
+
+            if (!clearDb && GridSizeMigrationTask.ENABLED &&
                     !GridSizeMigrationTask.migrateGridIfNeeded(mContext)) {
                 // Migration failed. Clear workspace.
                 clearDb = true;
diff --git a/src/com/android/launcher3/LauncherProvider.java b/src/com/android/launcher3/LauncherProvider.java
index a79df0d..eee5627 100644
--- a/src/com/android/launcher3/LauncherProvider.java
+++ b/src/com/android/launcher3/LauncherProvider.java
@@ -359,6 +359,12 @@
                 clearFlagEmptyDbCreated();
                 return null;
             }
+            case LauncherSettings.Settings.METHOD_WAS_EMPTY_DB_CREATED : {
+                Bundle result = new Bundle();
+                result.putBoolean(LauncherSettings.Settings.EXTRA_VALUE,
+                        Utilities.getPrefs(getContext()).getBoolean(EMPTY_DATABASE_CREATED, false));
+                return result;
+            }
             case LauncherSettings.Settings.METHOD_DELETE_EMPTY_FOLDERS: {
                 Bundle result = new Bundle();
                 result.putSerializable(LauncherSettings.Settings.EXTRA_VALUE, deleteEmptyFolders());
diff --git a/src/com/android/launcher3/LauncherSettings.java b/src/com/android/launcher3/LauncherSettings.java
index 8157eb3..e884393 100644
--- a/src/com/android/launcher3/LauncherSettings.java
+++ b/src/com/android/launcher3/LauncherSettings.java
@@ -283,6 +283,7 @@
                 ProviderConfig.AUTHORITY + "/settings");
 
         public static final String METHOD_CLEAR_EMPTY_DB_FLAG = "clear_empty_db_flag";
+        public static final String METHOD_WAS_EMPTY_DB_CREATED = "get_empty_db_flag";
 
         public static final String METHOD_DELETE_EMPTY_FOLDERS = "delete_empty_folders";
 
diff --git a/src/com/android/launcher3/PendingAppWidgetHostView.java b/src/com/android/launcher3/PendingAppWidgetHostView.java
index a455026..f01c7f2 100644
--- a/src/com/android/launcher3/PendingAppWidgetHostView.java
+++ b/src/com/android/launcher3/PendingAppWidgetHostView.java
@@ -32,6 +32,7 @@
 import android.text.StaticLayout;
 import android.text.TextPaint;
 import android.util.TypedValue;
+import android.view.ContextThemeWrapper;
 import android.view.View;
 import android.view.View.OnClickListener;
 
@@ -63,9 +64,9 @@
     @TargetApi(Build.VERSION_CODES.LOLLIPOP)
     public PendingAppWidgetHostView(Context context, LauncherAppWidgetInfo info,
             boolean disabledForSafeMode) {
-        super(context);
+        super(new ContextThemeWrapper(context, R.style.WidgetContainerTheme));
 
-        mLauncher = (Launcher) context;
+        mLauncher = Launcher.getLauncher(context);
         mInfo = info;
         mStartState = info.restoreStatus;
         mIconLookupIntent = new Intent().setComponent(info.providerName);
diff --git a/src/com/android/launcher3/model/GridSizeMigrationTask.java b/src/com/android/launcher3/model/GridSizeMigrationTask.java
index 7287ced..600768e 100644
--- a/src/com/android/launcher3/model/GridSizeMigrationTask.java
+++ b/src/com/android/launcher3/model/GridSizeMigrationTask.java
@@ -281,7 +281,9 @@
 
         // Try removing all possible combinations
         for (int x = 0; x < mSrcX; x++) {
-            for (int y = startY; y < mSrcY; y++) {
+            // Try removing the rows first from bottom. This keeps the workspace
+            // nicely aligned with hotseat.
+            for (int y = mSrcY - 1; y >= startY; y--) {
                 // Use a deep copy when trying out a particular combination as it can change
                 // the underlying object.
                 ArrayList<DbEntry> itemsOnScreen = tryRemove(x, y, startY, deepCopy(items), outLoss);
@@ -879,6 +881,14 @@
         return String.format(Locale.ENGLISH, "%d,%d", x, y);
     }
 
+    public static void markForMigration(
+            Context context, int gridX, int gridY, int hotseatSize) {
+        Utilities.getPrefs(context).edit()
+                .putString(KEY_MIGRATION_SRC_WORKSPACE_SIZE, getPointString(gridX, gridY))
+                .putInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT, hotseatSize)
+                .apply();
+    }
+
     /**
      * Migrates the workspace and hotseat in case their sizes changed.
      * @return false if the migration failed.
@@ -915,45 +925,8 @@
             Point sourceSize = parsePoint(prefs.getString(
                     KEY_MIGRATION_SRC_WORKSPACE_SIZE, gridSizeString));
 
-            if (!targetSize.equals(sourceSize)) {
-
-                // The following list defines all possible grid sizes (and intermediate steps
-                // during migration). Note that at each step, dx <= 1 && dy <= 1. Any grid size
-                // which is not in this list is not migrated.
-                // Note that the InvariantDeviceProfile defines (rows, cols) but the Points
-                // specified here are defined as (cols, rows).
-                ArrayList<Point> gridSizeSteps = new ArrayList<>();
-                gridSizeSteps.add(new Point(3, 2));
-                gridSizeSteps.add(new Point(3, 3));
-                gridSizeSteps.add(new Point(4, 3));
-                gridSizeSteps.add(new Point(4, 4));
-                gridSizeSteps.add(new Point(5, 5));
-                gridSizeSteps.add(new Point(6, 5));
-                gridSizeSteps.add(new Point(6, 6));
-                gridSizeSteps.add(new Point(7, 7));
-
-                int sourceSizeIndex = gridSizeSteps.indexOf(sourceSize);
-                int targetSizeIndex = gridSizeSteps.indexOf(targetSize);
-
-                if (sourceSizeIndex <= -1 || targetSizeIndex <= -1) {
-                    throw new Exception("Unable to migrate grid size from " + sourceSize
-                            + " to " + targetSize);
-                }
-
-                // Migrate the workspace grid, step by step.
-                while (targetSizeIndex < sourceSizeIndex ) {
-                    // We only need to migrate the grid if source size is greater
-                    // than the target size.
-                    Point stepTargetSize = gridSizeSteps.get(sourceSizeIndex - 1);
-                    Point stepSourceSize = gridSizeSteps.get(sourceSizeIndex);
-
-                    if (new GridSizeMigrationTask(context,
-                            LauncherAppState.getInstance().getInvariantDeviceProfile(),
-                            validPackages, stepSourceSize, stepTargetSize).migrateWorkspace()) {
-                        dbChanged = true;
-                    }
-                    sourceSizeIndex--;
-                }
+            if (new MultiStepMigrationTask(validPackages, context).migrate(sourceSize, targetSize)) {
+                dbChanged = true;
             }
 
             if (dbChanged) {
@@ -999,4 +972,55 @@
                 .updateAndGetActiveSessionCache().keySet());
         return validPackages;
     }
+
+    /**
+     * Task to run grid migration in multiple steps when the size difference is more than 1.
+     */
+    protected static class MultiStepMigrationTask {
+        private final HashSet<String> mValidPackages;
+        private final Context mContext;
+
+        public MultiStepMigrationTask(HashSet<String> validPackages, Context context) {
+            mValidPackages = validPackages;
+            mContext = context;
+        }
+
+        public boolean migrate(Point sourceSize, Point targetSize) throws Exception {
+            boolean dbChanged = false;
+            if (!targetSize.equals(sourceSize)) {
+                if (sourceSize.x < targetSize.x) {
+                    // Source is smaller that target, just expand the grid without actual migration.
+                    sourceSize.x = targetSize.x;
+                }
+                if (sourceSize.y < targetSize.y) {
+                    // Source is smaller that target, just expand the grid without actual migration.
+                    sourceSize.y = targetSize.y;
+                }
+
+                // Migrate the workspace grid, such that the points differ by max 1 in x and y
+                // each on every step.
+                while (!targetSize.equals(sourceSize)) {
+                    // Get the next size, such that the points differ by max 1 in x and y each
+                    Point nextSize = new Point(sourceSize);
+                    if (targetSize.x < nextSize.x) {
+                        nextSize.x--;
+                    }
+                    if (targetSize.y < nextSize.y) {
+                        nextSize.y--;
+                    }
+                    if (runStepTask(sourceSize, nextSize)) {
+                        dbChanged = true;
+                    }
+                    sourceSize.set(nextSize.x, nextSize.y);
+                }
+            }
+            return dbChanged;
+        }
+
+        protected boolean runStepTask(Point sourceSize, Point nextSize) throws Exception {
+            return new GridSizeMigrationTask(mContext,
+                    LauncherAppState.getInstance().getInvariantDeviceProfile(),
+                    mValidPackages, sourceSize, nextSize).migrateWorkspace();
+        }
+    }
 }
diff --git a/src/com/android/launcher3/provider/ImportDataTask.java b/src/com/android/launcher3/provider/ImportDataTask.java
new file mode 100644
index 0000000..233c3ed
--- /dev/null
+++ b/src/com/android/launcher3/provider/ImportDataTask.java
@@ -0,0 +1,451 @@
+/*
+ * Copyright (C) 2016 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.provider;
+
+import android.content.ContentProviderOperation;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ProviderInfo;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.net.Uri;
+import android.os.Process;
+import android.text.TextUtils;
+import android.util.LongSparseArray;
+import android.util.SparseBooleanArray;
+
+import com.android.launcher3.AutoInstallsLayout.LayoutParserCallback;
+import com.android.launcher3.DefaultLayoutParser;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.LauncherAppWidgetInfo;
+import com.android.launcher3.LauncherFiles;
+import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.LauncherSettings.Favorites;
+import com.android.launcher3.LauncherSettings.Settings;
+import com.android.launcher3.LauncherSettings.WorkspaceScreens;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.Workspace;
+import com.android.launcher3.compat.UserHandleCompat;
+import com.android.launcher3.compat.UserManagerCompat;
+import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.config.ProviderConfig;
+import com.android.launcher3.logging.FileLog;
+import com.android.launcher3.model.GridSizeMigrationTask;
+import com.android.launcher3.util.LongArrayMap;
+
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+
+/**
+ * Utility class to import data from another Launcher which is based on Launcher3 schema.
+ */
+public class ImportDataTask {
+
+    public static final String KEY_DATA_IMPORT_SRC_PKG = "data_import_src_pkg";
+    public static final String KEY_DATA_IMPORT_SRC_AUTHORITY = "data_import_src_authority";
+
+    private static final String TAG = "ImportDataTask";
+    private static final int MIN_ITEM_COUNT_FOR_SUCCESSFUL_MIGRATION = 6;
+    // Insert items progressively to avoid OOM exception when loading icons.
+    private static final int BATCH_INSERT_SIZE = 15;
+
+    private final Context mContext;
+
+    private final Uri mOtherScreensUri;
+    private final Uri mOtherFavoritesUri;
+
+    private int mHotseatSize;
+    private int mMaxGridSizeX;
+    private int mMaxGridSizeY;
+
+    private ImportDataTask(Context context, String sourceAuthority) {
+        mContext = context;
+        mOtherScreensUri = Uri.parse("content://" +
+                sourceAuthority + "/" + WorkspaceScreens.TABLE_NAME);
+        mOtherFavoritesUri = Uri.parse("content://" + sourceAuthority + "/" + Favorites.TABLE_NAME);
+    }
+
+    public boolean importWorkspace() throws Exception {
+        ArrayList<Long> allScreens = LauncherDbUtils.getScreenIdsFromCursor(
+                mContext.getContentResolver().query(mOtherScreensUri, null, null, null,
+                        LauncherSettings.WorkspaceScreens.SCREEN_RANK));
+
+        // During import we reset the screen IDs to 0-indexed values.
+        if (allScreens.isEmpty()) {
+            // No thing to migrate
+            return false;
+        }
+
+        mHotseatSize = mMaxGridSizeX = mMaxGridSizeY = 0;
+
+        // Build screen update
+        ArrayList<ContentProviderOperation> screenOps = new ArrayList<>();
+        int count = allScreens.size();
+        LongSparseArray<Long> screenIdMap = new LongSparseArray<>(count);
+        for (int i = 0; i < count; i++) {
+            ContentValues v = new ContentValues();
+            v.put(LauncherSettings.WorkspaceScreens._ID, i);
+            v.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, i);
+            screenIdMap.put(allScreens.get(i), (long) i);
+            screenOps.add(ContentProviderOperation.newInsert(
+                    LauncherSettings.WorkspaceScreens.CONTENT_URI).withValues(v).build());
+        }
+        mContext.getContentResolver().applyBatch(ProviderConfig.AUTHORITY, screenOps);
+        importWorkspaceItems(allScreens.get(0), screenIdMap);
+
+        GridSizeMigrationTask.markForMigration(mContext, mMaxGridSizeX, mMaxGridSizeY, mHotseatSize);
+
+        // Create empty DB flag.
+        LauncherSettings.Settings.call(mContext.getContentResolver(),
+                LauncherSettings.Settings.METHOD_CLEAR_EMPTY_DB_FLAG);
+        return true;
+    }
+
+    /**
+     * 1) Imports all the workspace entries from the source provider.
+     * 2) For home screen entries, maps the screen id based on {@param screenIdMap}
+     * 3) In the end fills any holes in hotseat with items from default hotseat layout.
+     */
+    private void importWorkspaceItems(
+            long firsetScreenId, LongSparseArray<Long> screenIdMap) throws Exception {
+        String profileId = Long.toString(UserManagerCompat.getInstance(mContext)
+                .getSerialNumberForUser(UserHandleCompat.myUserHandle()));
+
+        boolean createEmptyRowOnFirstScreen = false;
+        if (FeatureFlags.QSB_ON_FIRST_SCREEN) {
+            try (Cursor c = mContext.getContentResolver().query(mOtherFavoritesUri, null,
+                    // get items on the first row of the first screen
+                    "profileId = ? AND container = -100 AND screen = ? AND cellY = 0",
+                    new String[]{profileId, Long.toString(firsetScreenId)},
+                    null)) {
+                // First row of first screen is not empty
+                createEmptyRowOnFirstScreen = c.moveToNext();
+            }
+        }
+
+        ArrayList<ContentProviderOperation> insertOperations = new ArrayList<>(BATCH_INSERT_SIZE);
+
+        // Set of package names present in hotseat
+        final HashSet<String> hotseatTargetApps = new HashSet<>();
+        final LongArrayMap<Intent> hotseatItems = new LongArrayMap<>();
+        int maxId = 0;
+
+        // Number of imported items on workspace and hotseat
+        int totalItemsOnWorkspace = 0;
+
+        try (Cursor c = mContext.getContentResolver()
+                .query(mOtherFavoritesUri, null,
+                        // Only migrate the primary user
+                        Favorites.PROFILE_ID + " = ?", new String[]{profileId},
+                        // Get the items sorted by container, so that the folders are loaded
+                        // before the corresponding items.
+                        Favorites.CONTAINER)) {
+
+            // various columns we expect to exist.
+            final int idIndex = c.getColumnIndexOrThrow(Favorites._ID);
+            final int intentIndex = c.getColumnIndexOrThrow(Favorites.INTENT);
+            final int titleIndex = c.getColumnIndexOrThrow(Favorites.TITLE);
+            final int containerIndex = c.getColumnIndexOrThrow(Favorites.CONTAINER);
+            final int itemTypeIndex = c.getColumnIndexOrThrow(Favorites.ITEM_TYPE);
+            final int widgetProviderIndex = c.getColumnIndexOrThrow(Favorites.APPWIDGET_PROVIDER);
+            final int screenIndex = c.getColumnIndexOrThrow(Favorites.SCREEN);
+            final int cellXIndex = c.getColumnIndexOrThrow(Favorites.CELLX);
+            final int cellYIndex = c.getColumnIndexOrThrow(Favorites.CELLY);
+            final int spanXIndex = c.getColumnIndexOrThrow(Favorites.SPANX);
+            final int spanYIndex = c.getColumnIndexOrThrow(Favorites.SPANY);
+            final int rankIndex = c.getColumnIndexOrThrow(Favorites.RANK);
+            final int iconIndex = c.getColumnIndexOrThrow(Favorites.ICON);
+            final int iconPackageIndex = c.getColumnIndexOrThrow(Favorites.ICON_PACKAGE);
+            final int iconResourceIndex = c.getColumnIndexOrThrow(Favorites.ICON_RESOURCE);
+
+            SparseBooleanArray mValidFolders = new SparseBooleanArray();
+            ContentValues values = new ContentValues();
+
+            while (c.moveToNext()) {
+                values.clear();
+                int id = c.getInt(idIndex);
+                maxId = Math.max(maxId, id);
+                int type = c.getInt(itemTypeIndex);
+                int container = c.getInt(containerIndex);
+
+                long screen = c.getLong(screenIndex);
+
+                int cellX = c.getInt(cellXIndex);
+                int cellY = c.getInt(cellYIndex);
+                int spanX = c.getInt(spanXIndex);
+                int spanY = c.getInt(spanYIndex);
+
+                switch (container) {
+                    case Favorites.CONTAINER_DESKTOP: {
+                        Long newScreenId = screenIdMap.get(screen);
+                        if (newScreenId == null) {
+                            FileLog.d(TAG, String.format("Skipping item %d, type %d not on a valid screen %d", id, type, screen));
+                            continue;
+                        }
+                        // Reset the screen to 0-index value
+                        screen = newScreenId;
+                        if (createEmptyRowOnFirstScreen && screen == Workspace.FIRST_SCREEN_ID) {
+                            // Shift items by 1.
+                            cellY++;
+                        }
+
+                        mMaxGridSizeX = Math.max(mMaxGridSizeX, cellX + spanX);
+                        mMaxGridSizeY = Math.max(mMaxGridSizeY, cellY + spanY);
+                        break;
+                    }
+                    case Favorites.CONTAINER_HOTSEAT: {
+                        mHotseatSize = Math.max(mHotseatSize, (int) screen + 1);
+                        break;
+                    }
+                    default:
+                        if (!mValidFolders.get(container)) {
+                            FileLog.d(TAG, String.format("Skipping item %d, type %d not in a valid folder %d", id, type, container));
+                            continue;
+                        }
+                }
+
+                Intent intent = null;
+                switch (type) {
+                    case Favorites.ITEM_TYPE_FOLDER: {
+                        mValidFolders.put(id, true);
+                        // Use a empty intent to indicate a folder.
+                        intent = new Intent();
+                        break;
+                    }
+                    case Favorites.ITEM_TYPE_APPWIDGET: {
+                        values.put(Favorites.RESTORED,
+                                LauncherAppWidgetInfo.FLAG_ID_NOT_VALID |
+                                        LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY |
+                                        LauncherAppWidgetInfo.FLAG_UI_NOT_READY);
+                        values.put(Favorites.APPWIDGET_PROVIDER, c.getString(widgetProviderIndex));
+                        break;
+                    }
+                    case Favorites.ITEM_TYPE_SHORTCUT:
+                    case Favorites.ITEM_TYPE_APPLICATION: {
+                        intent = Intent.parseUri(c.getString(intentIndex), 0);
+                        if (Utilities.isLauncherAppTarget(intent)) {
+                            type = Favorites.ITEM_TYPE_APPLICATION;
+                        } else {
+                            values.put(Favorites.ICON_PACKAGE, c.getString(iconPackageIndex));
+                            values.put(Favorites.ICON_RESOURCE, c.getString(iconResourceIndex));
+                        }
+                        values.put(Favorites.ICON,  c.getBlob(iconIndex));
+                        values.put(Favorites.INTENT, intent.toUri(0));
+                        values.put(Favorites.RANK, c.getInt(rankIndex));
+
+                        values.put(Favorites.RESTORED, 1);
+                        break;
+                    }
+                    default:
+                        FileLog.d(TAG, String.format("Skipping item %d, not a valid type %d", id, type));
+                        continue;
+                }
+
+                if (container == Favorites.CONTAINER_HOTSEAT) {
+                    if (intent == null) {
+                        FileLog.d(TAG, String.format("Skipping item %d, null intent on hotseat", id));
+                        continue;
+                    }
+                    if (intent.getComponent() != null) {
+                        intent.setPackage(intent.getComponent().getPackageName());
+                    }
+                    hotseatItems.put(screen, intent);
+                    hotseatTargetApps.add(getPackage(intent));
+                }
+
+                values.put(Favorites._ID, id);
+                values.put(Favorites.ITEM_TYPE, type);
+                values.put(Favorites.CONTAINER, container);
+                values.put(Favorites.SCREEN, screen);
+                values.put(Favorites.CELLX, cellX);
+                values.put(Favorites.CELLY, cellY);
+                values.put(Favorites.SPANX, spanX);
+                values.put(Favorites.SPANY, spanY);
+                values.put(Favorites.TITLE, c.getString(titleIndex));
+                insertOperations.add(ContentProviderOperation
+                        .newInsert(Favorites.CONTENT_URI).withValues(values).build());
+                if (container < 0) {
+                    totalItemsOnWorkspace++;
+                }
+
+                if (insertOperations.size() >= BATCH_INSERT_SIZE) {
+                    mContext.getContentResolver().applyBatch(ProviderConfig.AUTHORITY,
+                            insertOperations);
+                    insertOperations.clear();
+                }
+            }
+        }
+        if (totalItemsOnWorkspace < MIN_ITEM_COUNT_FOR_SUCCESSFUL_MIGRATION) {
+            throw new Exception("Insufficient data");
+        }
+
+        int myHotseatCount = LauncherAppState.getInstance().getInvariantDeviceProfile().numHotseatIcons;
+        if (!FeatureFlags.NO_ALL_APPS_ICON) {
+            myHotseatCount--;
+        }
+        if (hotseatItems.size() < myHotseatCount) {
+            // Insufficient hotseat items. Add a few more.
+            HotseatParserCallback parserCallback = new HotseatParserCallback(
+                    hotseatTargetApps, hotseatItems, insertOperations, maxId + 1);
+            new HotseatLayoutParser(mContext,
+                    parserCallback).loadLayout(null, new ArrayList<Long>());
+            mHotseatSize = (int) hotseatItems.keyAt(hotseatItems.size() - 1) + 1;
+        }
+        if (!insertOperations.isEmpty()) {
+            mContext.getContentResolver().applyBatch(ProviderConfig.AUTHORITY,
+                    insertOperations);
+        }
+    }
+
+    private static final String getPackage(Intent intent) {
+        return intent.getComponent() != null ? intent.getComponent().getPackageName()
+                : intent.getPackage();
+    }
+
+    /**
+     * Performs data import if possible.
+     * @return true on successful data import, false if it was not available
+     * @throws Exception if the import failed
+     */
+    public static boolean performImportIfPossible(Context context) throws Exception {
+        SharedPreferences devicePrefs = getDevicePrefs(context);
+        String sourcePackage = devicePrefs.getString(KEY_DATA_IMPORT_SRC_PKG, "");
+        String sourceAuthority = devicePrefs.getString(KEY_DATA_IMPORT_SRC_AUTHORITY, "");
+
+        if (TextUtils.isEmpty(sourcePackage) || TextUtils.isEmpty(sourceAuthority)) {
+            return false;
+        }
+
+        // Synchronously clear the migration flags. This ensures that we do not try migration
+        // again and thus prevents potential crash loops due to migration failure.
+        devicePrefs.edit().remove(KEY_DATA_IMPORT_SRC_PKG).remove(KEY_DATA_IMPORT_SRC_AUTHORITY).commit();
+
+        if (!Settings.call(context.getContentResolver(), Settings.METHOD_WAS_EMPTY_DB_CREATED)
+                .getBoolean(Settings.EXTRA_VALUE, false)) {
+            // Only migration if a new DB was created.
+            return false;
+        }
+
+        for (ProviderInfo info : context.getPackageManager().queryContentProviders(
+                null, context.getApplicationInfo().uid, 0)) {
+
+            if (sourcePackage.equals(info.packageName)) {
+                if ((info.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0) {
+                    // Only migrate if the source launcher is also on system image.
+                    return false;
+                }
+
+                // Wait until we found a provider with matching authority.
+                if (sourceAuthority.equals(info.authority)) {
+                    if (TextUtils.isEmpty(info.readPermission) ||
+                            context.checkPermission(info.readPermission, Process.myPid(),
+                                    Process.myUid()) == PackageManager.PERMISSION_GRANTED) {
+                        // All checks passed, run the import task.
+                        return new ImportDataTask(context, sourceAuthority).importWorkspace();
+                    }
+                }
+            }
+        }
+        return false;
+    }
+
+    private static SharedPreferences getDevicePrefs(Context c) {
+        return c.getSharedPreferences(LauncherFiles.DEVICE_PREFERENCES_KEY, Context.MODE_PRIVATE);
+    }
+
+    private static final int getMyHotseatLayoutId() {
+        return LauncherAppState.getInstance().getInvariantDeviceProfile().numHotseatIcons <= 5
+                ? R.xml.dw_phone_hotseat
+                : R.xml.dw_tablet_hotseat;
+    }
+
+    /**
+     * Extension of {@link DefaultLayoutParser} which only allows icons and shortcuts.
+     */
+    private static class HotseatLayoutParser extends DefaultLayoutParser {
+        public HotseatLayoutParser(Context context, LayoutParserCallback callback) {
+            super(context, null, callback, context.getResources(), getMyHotseatLayoutId());
+        }
+
+        @Override
+        protected HashMap<String, TagParser> getLayoutElementsMap() {
+            // Only allow shortcut parsers
+            HashMap<String, TagParser> parsers = new HashMap<String, TagParser>();
+            parsers.put(TAG_FAVORITE, new AppShortcutWithUriParser());
+            parsers.put(TAG_SHORTCUT, new UriShortcutParser(mSourceRes));
+            parsers.put(TAG_RESOLVE, new ResolveParser());
+            return parsers;
+        }
+    }
+
+    /**
+     * {@link LayoutParserCallback} which adds items in empty hotseat spots.
+     */
+    private static class HotseatParserCallback implements LayoutParserCallback {
+        private final HashSet<String> mExisitingApps;
+        private final LongArrayMap<Intent> mExistingItems;
+        private final ArrayList<ContentProviderOperation> mOutOps;
+        private int mStartItemId;
+
+        HotseatParserCallback(
+                HashSet<String> existingApps, LongArrayMap<Intent> existingItems,
+                ArrayList<ContentProviderOperation> outOps, int startItemId) {
+            mExisitingApps = existingApps;
+            mExistingItems = existingItems;
+            mOutOps = outOps;
+            mStartItemId = startItemId;
+        }
+
+        @Override
+        public long generateNewItemId() {
+            return mStartItemId++;
+        }
+
+        @Override
+        public long insertAndCheck(SQLiteDatabase db, ContentValues values) {
+            Intent intent;
+            try {
+                intent = Intent.parseUri(values.getAsString(Favorites.INTENT), 0);
+            } catch (URISyntaxException e) {
+                return 0;
+            }
+            String pkg = getPackage(intent);
+            if (pkg == null || mExisitingApps.contains(pkg)) {
+                // The item does not target an app or is already in hotseat.
+                return 0;
+            }
+            mExisitingApps.add(pkg);
+
+            // find next vacant spot.
+            long screen = 0;
+            while (mExistingItems.get(screen) != null) {
+                screen++;
+            }
+            mExistingItems.put(screen, intent);
+            values.put(Favorites.SCREEN, screen);
+            mOutOps.add(ContentProviderOperation.newInsert(Favorites.CONTENT_URI).withValues(values).build());
+            return 0;
+        }
+    }
+}
diff --git a/tests/src/com/android/launcher3/model/GridSizeMigrationTaskTest.java b/tests/src/com/android/launcher3/model/GridSizeMigrationTaskTest.java
index 4dae42f..fc7fe48 100644
--- a/tests/src/com/android/launcher3/model/GridSizeMigrationTaskTest.java
+++ b/tests/src/com/android/launcher3/model/GridSizeMigrationTaskTest.java
@@ -1,6 +1,7 @@
 package com.android.launcher3.model;
 
 import android.content.ContentValues;
+import android.content.Context;
 import android.content.Intent;
 import android.database.Cursor;
 import android.graphics.Point;
@@ -12,10 +13,12 @@
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.config.ProviderConfig;
+import com.android.launcher3.model.GridSizeMigrationTask.MultiStepMigrationTask;
 import com.android.launcher3.util.TestLauncherProvider;
 
 import java.util.ArrayList;
 import java.util.HashSet;
+import java.util.LinkedList;
 
 /**
  * Unit tests for {@link GridSizeMigrationTask}
@@ -337,7 +340,7 @@
                     } else {
                         assertEquals(1, c.getCount());
                         c.moveToNext();
-                        assertEquals(String.format("Failed to verify item ad %d %d, %d", i, y, x),
+                        assertEquals(String.format("Failed to verify item at %d %d, %d", i, y, x),
                                 id, c.getLong(0));
                         total++;
                     }
@@ -388,4 +391,53 @@
         getMockContentResolver().insert(LauncherSettings.Favorites.CONTENT_URI, values);
         return id;
     }
+
+    public void testMultiStepMigration_small_to_large() throws Exception {
+        MultiStepMigrationTaskVerifier verifier = new MultiStepMigrationTaskVerifier();
+        verifier.migrate(new Point(3, 3), new Point(5, 5));
+        verifier.assertCompleted();
+    }
+
+    public void testMultiStepMigration_large_to_small() throws Exception {
+        MultiStepMigrationTaskVerifier verifier = new MultiStepMigrationTaskVerifier(
+                5, 5, 4, 4,
+                4, 4, 3, 4
+        );
+        verifier.migrate(new Point(5, 5), new Point(3, 4));
+        verifier.assertCompleted();
+    }
+
+    public void testMultiStepMigration_zig_zag() throws Exception {
+        MultiStepMigrationTaskVerifier verifier = new MultiStepMigrationTaskVerifier(
+                5, 7, 4, 7,
+                4, 7, 3, 7
+        );
+        verifier.migrate(new Point(5, 5), new Point(3, 7));
+        verifier.assertCompleted();
+    }
+
+    private static class MultiStepMigrationTaskVerifier extends MultiStepMigrationTask {
+
+        private final LinkedList<Point> mPoints;
+
+        public MultiStepMigrationTaskVerifier(int... points) {
+            super(null, null);
+
+            mPoints = new LinkedList<>();
+            for (int i = 0; i < points.length; i += 2) {
+                mPoints.add(new Point(points[i], points[i + 1]));
+            }
+        }
+
+        @Override
+        protected boolean runStepTask(Point sourceSize, Point nextSize) throws Exception {
+            assertEquals(sourceSize, mPoints.poll());
+            assertEquals(nextSize, mPoints.poll());
+            return false;
+        }
+
+        public void assertCompleted() {
+            assertTrue(mPoints.isEmpty());
+        }
+    }
 }