Refactoring some loadWorkspace logic in a separate class

Bug: 34112546
Change-Id: I8a43ed1646056aa1957ac3d6ea82018691df6386
diff --git a/src/com/android/launcher3/AppWidgetsRestoredReceiver.java b/src/com/android/launcher3/AppWidgetsRestoredReceiver.java
index c5b3104..c6f1c48 100644
--- a/src/com/android/launcher3/AppWidgetsRestoredReceiver.java
+++ b/src/com/android/launcher3/AppWidgetsRestoredReceiver.java
@@ -5,13 +5,13 @@
 import android.appwidget.AppWidgetProviderInfo;
 import android.content.BroadcastReceiver;
 import android.content.ContentResolver;
-import android.content.ContentValues;
 import android.content.Context;
 import android.content.Intent;
 import android.database.Cursor;
 import android.util.Log;
 
 import com.android.launcher3.LauncherSettings.Favorites;
+import com.android.launcher3.util.ContentWriter;
 
 public class AppWidgetsRestoredReceiver extends BroadcastReceiver {
 
@@ -50,14 +50,13 @@
                 state = LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY;
             }
 
-            ContentValues values = new ContentValues();
-            values.put(LauncherSettings.Favorites.APPWIDGET_ID, newWidgetIds[i]);
-            values.put(LauncherSettings.Favorites.RESTORED, state);
-
             String[] widgetIdParams = new String[] { Integer.toString(oldWidgetIds[i]) };
+            int result = new ContentWriter(context, new ContentWriter.CommitParams(
+                    "appWidgetId=? and (restored & 1) = 1", widgetIdParams))
+                    .put(LauncherSettings.Favorites.APPWIDGET_ID, newWidgetIds[i])
+                    .put(LauncherSettings.Favorites.RESTORED, state)
+                    .commit();
 
-            int result = cr.update(Favorites.CONTENT_URI, values,
-                    "appWidgetId=? and (restored & 1) = 1", widgetIdParams);
             if (result == 0) {
                 Cursor cursor = cr.query(Favorites.CONTENT_URI,
                         new String[] {Favorites.APPWIDGET_ID},
diff --git a/src/com/android/launcher3/InstallShortcutReceiver.java b/src/com/android/launcher3/InstallShortcutReceiver.java
index df2deb8..ecbfe38 100644
--- a/src/com/android/launcher3/InstallShortcutReceiver.java
+++ b/src/com/android/launcher3/InstallShortcutReceiver.java
@@ -26,6 +26,7 @@
 import android.content.pm.PackageManager;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
+import android.os.Parcelable;
 import android.os.Process;
 import android.os.UserHandle;
 import android.text.TextUtils;
@@ -35,6 +36,7 @@
 import com.android.launcher3.compat.LauncherActivityInfoCompat;
 import com.android.launcher3.compat.LauncherAppsCompat;
 import com.android.launcher3.compat.UserManagerCompat;
+import com.android.launcher3.graphics.LauncherIcons;
 import com.android.launcher3.shortcuts.DeepShortcutManager;
 import com.android.launcher3.shortcuts.ShortcutInfoCompat;
 import com.android.launcher3.shortcuts.ShortcutKey;
@@ -454,7 +456,7 @@
                 widgetInfo.spanY = Math.min(info.spanY, idp.numRows);
                 return widgetInfo;
             } else {
-                return LauncherAppState.getInstance().getModel().infoFromShortcutIntent(mContext, data);
+                return createShortcutInfo(data, LauncherAppState.getInstance());
             }
         }
 
@@ -598,4 +600,42 @@
             return installQueue;
         }
     }
+
+    private static ShortcutInfo createShortcutInfo(Intent data, LauncherAppState app) {
+        Intent intent = data.getParcelableExtra(Intent.EXTRA_SHORTCUT_INTENT);
+        String name = data.getStringExtra(Intent.EXTRA_SHORTCUT_NAME);
+        Parcelable bitmap = data.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON);
+
+        if (intent == null) {
+            // If the intent is null, we can't construct a valid ShortcutInfo, so we return null
+            Log.e(TAG, "Can't construct ShorcutInfo with null intent");
+            return null;
+        }
+
+        final ShortcutInfo info = new ShortcutInfo();
+
+        // Only support intents for current user for now. Intents sent from other
+        // users wouldn't get here without intent forwarding anyway.
+        info.user = Process.myUserHandle();
+
+        if (bitmap instanceof Bitmap) {
+            info.iconBitmap = LauncherIcons.createIconBitmap((Bitmap) bitmap, app.getContext());
+        } else {
+            Parcelable extra = data.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE);
+            if (extra instanceof Intent.ShortcutIconResource) {
+                info.iconResource = (Intent.ShortcutIconResource) extra;
+                info.iconBitmap = LauncherIcons.createIconBitmap(info.iconResource, app.getContext());
+            }
+        }
+        if (info.iconBitmap == null) {
+            info.iconBitmap = app.getIconCache().getDefaultIcon(info.user);
+        }
+
+        info.title = Utilities.trim(name);
+        info.contentDescription = UserManagerCompat.getInstance(app.getContext())
+                .getBadgedLabelForUser(info.title, info.user);
+        info.intent = intent;
+        return info;
+    }
+
 }
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 8e28912..d6c8cfb 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -2337,13 +2337,14 @@
             showBrokenAppInstallDialog(packageName,
                 new DialogInterface.OnClickListener() {
                     public void onClick(DialogInterface dialog, int id) {
-                        startActivitySafely(v, LauncherModel.getMarketIntent(packageName), info);
+                        startActivitySafely(
+                                v, PackageManagerHelper.getMarketIntent(packageName), info);
                     }
                 });
         } else {
             // Download has started.
             final String packageName = info.providerName.getPackageName();
-            startActivitySafely(v, LauncherModel.getMarketIntent(packageName), info);
+            startActivitySafely(v, PackageManagerHelper.getMarketIntent(packageName), info);
         }
     }
 
diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java
index daa910f..616b2c7 100644
--- a/src/com/android/launcher3/LauncherModel.java
+++ b/src/com/android/launcher3/LauncherModel.java
@@ -24,21 +24,16 @@
 import android.content.ContentValues;
 import android.content.Context;
 import android.content.Intent;
-import android.content.Intent.ShortcutIconResource;
 import android.content.IntentFilter;
 import android.content.pm.PackageManager;
-import android.database.Cursor;
-import android.graphics.Bitmap;
 import android.net.Uri;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.Looper;
-import android.os.Parcelable;
 import android.os.Process;
 import android.os.SystemClock;
 import android.os.Trace;
 import android.os.UserHandle;
-import android.provider.BaseColumns;
 import android.text.TextUtils;
 import android.util.Log;
 import android.util.LongSparseArray;
@@ -50,18 +45,17 @@
 import com.android.launcher3.compat.PackageInstallerCompat;
 import com.android.launcher3.compat.PackageInstallerCompat.PackageInstallInfo;
 import com.android.launcher3.compat.UserManagerCompat;
-import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.config.ProviderConfig;
 import com.android.launcher3.dynamicui.ExtractionUtils;
 import com.android.launcher3.folder.Folder;
 import com.android.launcher3.folder.FolderIcon;
-import com.android.launcher3.graphics.LauncherIcons;
 import com.android.launcher3.logging.FileLog;
 import com.android.launcher3.model.AddWorkspaceItemsTask;
 import com.android.launcher3.model.BgDataModel;
 import com.android.launcher3.model.CacheDataUpdatedTask;
 import com.android.launcher3.model.ExtendedModelTask;
 import com.android.launcher3.model.GridSizeMigrationTask;
+import com.android.launcher3.model.LoaderCursor;
 import com.android.launcher3.model.PackageInstallStateChangedTask;
 import com.android.launcher3.model.PackageItemInfo;
 import com.android.launcher3.model.PackageUpdatedTask;
@@ -77,10 +71,7 @@
 import com.android.launcher3.shortcuts.ShortcutKey;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.ContentWriter;
-import com.android.launcher3.util.CursorIconInfo;
-import com.android.launcher3.util.GridOccupancy;
 import com.android.launcher3.util.ItemInfoMatcher;
-import com.android.launcher3.util.LongArrayMap;
 import com.android.launcher3.util.ManagedProfileHeuristic;
 import com.android.launcher3.util.MultiHashMap;
 import com.android.launcher3.util.PackageManagerHelper;
@@ -91,7 +82,6 @@
 
 import java.lang.ref.WeakReference;
 import java.net.URISyntaxException;
-import java.security.InvalidParameterException;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
@@ -1096,96 +1086,6 @@
             }
         }
 
-        // check & update map of what's occupied; used to discard overlapping/invalid items
-        private boolean checkItemPlacement(LongArrayMap<GridOccupancy> occupied, ItemInfo item,
-                   ArrayList<Long> workspaceScreens) {
-            LauncherAppState app = LauncherAppState.getInstance();
-            InvariantDeviceProfile profile = app.getInvariantDeviceProfile();
-
-            long containerIndex = item.screenId;
-            if (item.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
-                // Return early if we detect that an item is under the hotseat button
-                if (!FeatureFlags.NO_ALL_APPS_ICON &&
-                        profile.isAllAppsButtonRank((int) item.screenId)) {
-                    Log.e(TAG, "Error loading shortcut into hotseat " + item
-                            + " into position (" + item.screenId + ":" + item.cellX + ","
-                            + item.cellY + ") occupied by all apps");
-                    return false;
-                }
-
-                final GridOccupancy hotseatOccupancy =
-                        occupied.get((long) LauncherSettings.Favorites.CONTAINER_HOTSEAT);
-
-                if (item.screenId >= profile.numHotseatIcons) {
-                    Log.e(TAG, "Error loading shortcut " + item
-                            + " into hotseat position " + item.screenId
-                            + ", position out of bounds: (0 to " + (profile.numHotseatIcons - 1)
-                            + ")");
-                    return false;
-                }
-
-                if (hotseatOccupancy != null) {
-                    if (hotseatOccupancy.cells[(int) item.screenId][0]) {
-                        Log.e(TAG, "Error loading shortcut into hotseat " + item
-                                + " into position (" + item.screenId + ":" + item.cellX + ","
-                                + item.cellY + ") already occupied");
-                            return false;
-                    } else {
-                        hotseatOccupancy.cells[(int) item.screenId][0] = true;
-                        return true;
-                    }
-                } else {
-                    final GridOccupancy occupancy = new GridOccupancy(profile.numHotseatIcons, 1);
-                    occupancy.cells[(int) item.screenId][0] = true;
-                    occupied.put((long) LauncherSettings.Favorites.CONTAINER_HOTSEAT, occupancy);
-                    return true;
-                }
-            } else if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) {
-                if (!workspaceScreens.contains((Long) item.screenId)) {
-                    // The item has an invalid screen id.
-                    return false;
-                }
-            } else {
-                // Skip further checking if it is not the hotseat or workspace container
-                return true;
-            }
-
-            final int countX = profile.numColumns;
-            final int countY = profile.numRows;
-            if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP &&
-                    item.cellX < 0 || item.cellY < 0 ||
-                    item.cellX + item.spanX > countX || item.cellY + item.spanY > countY) {
-                Log.e(TAG, "Error loading shortcut " + item
-                        + " into cell (" + containerIndex + "-" + item.screenId + ":"
-                        + item.cellX + "," + item.cellY
-                        + ") out of screen bounds ( " + countX + "x" + countY + ")");
-                return false;
-            }
-
-            if (!occupied.containsKey(item.screenId)) {
-                GridOccupancy screen = new GridOccupancy(countX + 1, countY + 1);
-                if (item.screenId == Workspace.FIRST_SCREEN_ID) {
-                    // Mark the first row as occupied (if the feature is enabled)
-                    // in order to account for the QSB.
-                    screen.markCells(0, 0, countX + 1, 1, FeatureFlags.QSB_ON_FIRST_SCREEN);
-                }
-                occupied.put(item.screenId, screen);
-            }
-            final GridOccupancy occupancy = occupied.get(item.screenId);
-
-            // Check if any workspace icons overlap with each other
-            if (occupancy.isRegionVacant(item.cellX, item.cellY, item.spanX, item.spanY)) {
-                occupancy.markCells(item, true);
-                return true;
-            } else {
-                Log.e(TAG, "Error loading shortcut " + item
-                        + " into cell (" + containerIndex + "-" + item.screenId + ":"
-                        + item.cellX + "," + item.cellX + "," + item.spanX + "," + item.spanY
-                        + ") already occupied");
-                return false;
-            }
-        }
-
         private void loadWorkspace() {
             if (LauncherAppState.PROFILE_STARTUP) {
                 Trace.beginSection("Loading Workspace");
@@ -1237,37 +1137,19 @@
                         .getInstance(mContext).updateAndGetActiveSessionCache();
                 sBgDataModel.workspaceScreens.addAll(loadWorkspaceScreensDb(mContext));
 
-                final ArrayList<Long> itemsToRemove = new ArrayList<>();
-                final ArrayList<Long> restoredRows = new ArrayList<>();
                 Map<ShortcutKey, ShortcutInfoCompat> shortcutKeyToPinnedShortcuts = new HashMap<>();
-                final Uri contentUri = LauncherSettings.Favorites.CONTENT_URI;
-                if (DEBUG_LOADERS) Log.d(TAG, "loading model from " + contentUri);
-                final Cursor c = contentResolver.query(contentUri, null, null, null, null);
+                final LoaderCursor c = new LoaderCursor(contentResolver.query(
+                        LauncherSettings.Favorites.CONTENT_URI, null, null, null, null), mApp);
 
-                // +1 for the hotseat (it can be larger than the workspace)
-                // Load workspace in reverse order to ensure that latest items are loaded first (and
-                // before any earlier duplicates)
-                final LongArrayMap<GridOccupancy> occupied = new LongArrayMap<>();
                 HashMap<ComponentKey, AppWidgetProviderInfo> widgetProvidersMap = null;
 
                 try {
-                    final int idIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites._ID);
                     final int intentIndex = c.getColumnIndexOrThrow
                             (LauncherSettings.Favorites.INTENT);
-                    final int containerIndex = c.getColumnIndexOrThrow(
-                            LauncherSettings.Favorites.CONTAINER);
-                    final int itemTypeIndex = c.getColumnIndexOrThrow(
-                            LauncherSettings.Favorites.ITEM_TYPE);
                     final int appWidgetIdIndex = c.getColumnIndexOrThrow(
                             LauncherSettings.Favorites.APPWIDGET_ID);
                     final int appWidgetProviderIndex = c.getColumnIndexOrThrow(
                             LauncherSettings.Favorites.APPWIDGET_PROVIDER);
-                    final int screenIndex = c.getColumnIndexOrThrow(
-                            LauncherSettings.Favorites.SCREEN);
-                    final int cellXIndex = c.getColumnIndexOrThrow
-                            (LauncherSettings.Favorites.CELLX);
-                    final int cellYIndex = c.getColumnIndexOrThrow
-                            (LauncherSettings.Favorites.CELLY);
                     final int spanXIndex = c.getColumnIndexOrThrow
                             (LauncherSettings.Favorites.SPANX);
                     final int spanYIndex = c.getColumnIndexOrThrow(
@@ -1276,13 +1158,10 @@
                             LauncherSettings.Favorites.RANK);
                     final int restoredIndex = c.getColumnIndexOrThrow(
                             LauncherSettings.Favorites.RESTORED);
-                    final int profileIdIndex = c.getColumnIndexOrThrow(
-                            LauncherSettings.Favorites.PROFILE_ID);
                     final int optionsIndex = c.getColumnIndexOrThrow(
                             LauncherSettings.Favorites.OPTIONS);
-                    final CursorIconInfo cursorIconInfo = new CursorIconInfo(mContext, c);
 
-                    final LongSparseArray<UserHandle> allUsers = new LongSparseArray<>();
+                    final LongSparseArray<UserHandle> allUsers = c.allUsers;
                     final LongSparseArray<Boolean> quietMode = new LongSparseArray<>();
                     final LongSparseArray<Boolean> unlockedUsers = new LongSparseArray<>();
                     for (UserHandle user : mUserManager.getUserProfiles()) {
@@ -1314,44 +1193,42 @@
                     ShortcutInfo info;
                     String intentDescription;
                     LauncherAppWidgetInfo appWidgetInfo;
-                    int container;
-                    long id;
-                    long serialNumber;
                     Intent intent;
-                    UserHandle user;
                     String targetPackage;
 
                     while (!mStopped && c.moveToNext()) {
                         try {
-                            int itemType = c.getInt(itemTypeIndex);
+                            if (c.user == null) {
+                                // User has been deleted, remove the item.
+                                c.markDeleted("User has been deleted");
+                                continue;
+                            }
+
                             boolean restored = 0 != c.getInt(restoredIndex);
                             boolean allowMissingTarget = false;
-                            container = c.getInt(containerIndex);
-
-                            switch (itemType) {
+                            switch (c.itemType) {
+                            case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT: {
+                                if (!Process.myUserHandle().equals(c.user)) {
+                                    c.markDeleted("Legacy shortcuts are only allowed for default user");
+                                    continue;
+                                }
+                                // Follow through.
+                            }
                             case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
-                            case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
                             case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT:
-                                id = c.getLong(idIndex);
                                 intentDescription = c.getString(intentIndex);
-                                serialNumber = c.getInt(profileIdIndex);
-                                user = allUsers.get(serialNumber);
                                 int promiseType = c.getInt(restoredIndex);
                                 int disabledState = 0;
                                 targetPackage = null;
-                                if (user == null) {
-                                    // User has been deleted remove the item.
-                                    itemsToRemove.add(id);
-                                    continue;
-                                }
+
                                 try {
                                     intent = Intent.parseUri(intentDescription, 0);
                                     ComponentName cn = intent.getComponent();
                                     if (cn != null && cn.getPackageName() != null) {
                                         boolean validPkg = launcherApps.isPackageEnabledForProfile(
-                                                cn.getPackageName(), user);
+                                                cn.getPackageName(), c.user);
                                         boolean validComponent = validPkg &&
-                                                launcherApps.isActivityEnabledForProfile(cn, user);
+                                                launcherApps.isActivityEnabledForProfile(cn, c.user);
                                         if (validPkg) {
                                             targetPackage = cn.getPackageName();
                                         }
@@ -1359,10 +1236,10 @@
                                         if (validComponent) {
                                             if (restored) {
                                                 // no special handling necessary for this item
-                                                restoredRows.add(id);
+                                                c.markRestored();
                                                 restored = false;
                                             }
-                                            if (quietMode.get(serialNumber)) {
+                                            if (quietMode.get(c.serialNumber)) {
                                                 disabledState = ShortcutInfo.FLAG_DISABLED_QUIET_USER;
                                             }
                                         } else if (validPkg) {
@@ -1373,22 +1250,20 @@
                                                 intent = manager.getLaunchIntentForPackage(
                                                         cn.getPackageName());
                                                 if (intent != null) {
-                                                    ContentValues values = new ContentValues();
-                                                    values.put(LauncherSettings.Favorites.INTENT,
-                                                            intent.toUri(0));
-                                                    updateItem(id, values);
+                                                    c.updater().put(
+                                                            LauncherSettings.Favorites.INTENT,
+                                                            intent.toUri(0)).commit();
                                                 }
                                             }
 
                                             if (intent == null) {
                                                 // The app is installed but the component is no
                                                 // longer available.
-                                                FileLog.d(TAG, "Invalid component removed: " + cn);
-                                                itemsToRemove.add(id);
+                                                c.markDeleted("Invalid component removed: " + cn);
                                                 continue;
                                             } else {
                                                 // no special handling necessary for this item
-                                                restoredRows.add(id);
+                                                c.markRestored();
                                                 restored = false;
                                             }
                                         } else if (restored) {
@@ -1401,13 +1276,11 @@
                                             } else if (installingPkgs.containsKey(cn.getPackageName())) {
                                                 // App restore has started. Update the flag
                                                 promiseType |= ShortcutInfo.FLAG_RESTORE_STARTED;
-                                                ContentValues values = new ContentValues();
-                                                values.put(LauncherSettings.Favorites.RESTORED,
-                                                        promiseType);
-                                                updateItem(id, values);
+                                                c.updater().put(
+                                                        LauncherSettings.Favorites.RESTORED,
+                                                        promiseType).commit();
                                             } else {
-                                                FileLog.d(TAG, "Unrestored package removed: " + cn);
-                                                itemsToRemove.add(id);
+                                                c.markDeleted("Unrestored package removed: " + cn);
                                                 continue;
                                             }
                                         } else if (PackageManagerHelper.isAppOnSdcard(
@@ -1419,70 +1292,64 @@
                                             // SdCard is not ready yet. Package might get available,
                                             // once it is ready.
                                             Log.d(TAG, "Invalid package: " + cn + " (check again later)");
-                                            pendingPackages.addToList(user, cn.getPackageName());
+                                            pendingPackages.addToList(c.user, cn.getPackageName());
                                             allowMissingTarget = true;
                                             // Add the icon on the workspace anyway.
 
                                         } else {
                                             // Do not wait for external media load anymore.
                                             // Log the invalid package, and remove it
-                                            FileLog.d(TAG, "Invalid package removed: " + cn);
-                                            itemsToRemove.add(id);
+                                            c.markDeleted("Invalid package removed: " + cn);
                                             continue;
                                         }
                                     } else if (cn == null) {
                                         // For shortcuts with no component, keep them as they are
-                                        restoredRows.add(id);
+                                        c.markRestored();
                                         restored = false;
                                     }
                                 } catch (URISyntaxException e) {
-                                    FileLog.d(TAG, "Invalid uri: " + intentDescription);
-                                    itemsToRemove.add(id);
+                                    c.markDeleted("Invalid uri: " + intentDescription);
                                     continue;
                                 }
 
-                                boolean useLowResIcon = container >= 0 &&
+                                boolean useLowResIcon = !c.isOnWorkspaceOrHotseat() &&
                                         c.getInt(rankIndex) >= FolderIcon.NUM_ITEMS_IN_PREVIEW;
 
                                 if (restored) {
-                                    if (user.equals(Process.myUserHandle())) {
-                                        info = getRestoredItemInfo(c, intent,
-                                                promiseType, itemType, cursorIconInfo);
-                                        intent = getRestoredItemIntent(c, context, intent);
+                                    if (c.user.equals(Process.myUserHandle())) {
+                                        info = c.getRestoredItemInfo(intent, promiseType);
+                                        intent = PackageManagerHelper.getMarketIntent(
+                                                intent.getComponent().getPackageName());
                                     } else {
                                         // Don't restore items for other profiles.
-                                        itemsToRemove.add(id);
+                                        c.markDeleted("Restore from managed profile not supported");
                                         continue;
                                     }
-                                } else if (itemType ==
+                                } else if (c.itemType ==
                                         LauncherSettings.Favorites.ITEM_TYPE_APPLICATION) {
-                                    info = getAppShortcutInfo(intent, user, c,
-                                            cursorIconInfo, allowMissingTarget, useLowResIcon);
-                                } else if (itemType ==
+                                    info = c.getAppShortcutInfo(
+                                            intent, allowMissingTarget, useLowResIcon);
+                                } else if (c.itemType ==
                                         LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
 
-                                    ShortcutKey key = ShortcutKey.fromIntent(intent, user);
-                                    if (unlockedUsers.get(serialNumber)) {
+                                    ShortcutKey key = ShortcutKey.fromIntent(intent, c.user);
+                                    if (unlockedUsers.get(c.serialNumber)) {
                                         ShortcutInfoCompat pinnedShortcut =
                                                 shortcutKeyToPinnedShortcuts.get(key);
                                         if (pinnedShortcut == null) {
                                             // The shortcut is no longer valid.
-                                            itemsToRemove.add(id);
+                                            c.markDeleted("Pinned shortcut not found");
                                             continue;
                                         }
                                         info = new ShortcutInfo(pinnedShortcut, context);
                                         intent = info.intent;
                                     } else {
                                         // Create a shortcut info in disabled mode for now.
-                                        info = new ShortcutInfo();
-                                        info.user = user;
-                                        info.itemType = itemType;
-                                        loadInfoFromCursor(info, c, cursorIconInfo);
-
+                                        info = c.loadSimpleShortcut();
                                         info.isDisabled |= ShortcutInfo.FLAG_DISABLED_LOCKED_USER;
                                     }
                                 } else { // item type == ITEM_TYPE_SHORTCUT
-                                    info = getShortcutInfo(c, cursorIconInfo);
+                                    info = c.loadSimpleShortcut();
 
                                     // Shortcuts are only available on the primary profile
                                     if (PackageManagerHelper.isAppSuspended(manager, targetPackage)) {
@@ -1503,30 +1370,23 @@
                                 }
 
                                 if (info != null) {
-                                    info.id = id;
+                                    c.applyCommonProperties(info);
+
                                     info.intent = intent;
-                                    info.container = container;
-                                    info.screenId = c.getInt(screenIndex);
-                                    info.cellX = c.getInt(cellXIndex);
-                                    info.cellY = c.getInt(cellYIndex);
                                     info.rank = c.getInt(rankIndex);
                                     info.spanX = 1;
                                     info.spanY = 1;
-                                    info.intent.putExtra(ItemInfo.EXTRA_PROFILE, serialNumber);
+                                    // TODO: Remove this extra. Instead we should be using
+                                    // itemInfo#user.
+                                    info.intent.putExtra(ItemInfo.EXTRA_PROFILE, c.serialNumber);
                                     if (info.promisedIntent != null) {
-                                        info.promisedIntent.putExtra(ItemInfo.EXTRA_PROFILE, serialNumber);
+                                        info.promisedIntent.putExtra(ItemInfo.EXTRA_PROFILE, c.serialNumber);
                                     }
                                     info.isDisabled |= disabledState;
                                     if (isSafeMode && !Utilities.isSystemApp(context, intent)) {
                                         info.isDisabled |= ShortcutInfo.FLAG_DISABLED_SAFEMODE;
                                     }
 
-                                    // check & update map of what's occupied
-                                    if (!checkItemPlacement(occupied, info, sBgDataModel.workspaceScreens)) {
-                                        itemsToRemove.add(id);
-                                        break;
-                                    }
-
                                     if (restored) {
                                         ComponentName cn = info.getTargetComponent();
                                         if (cn != null) {
@@ -1539,55 +1399,38 @@
                                         }
                                     }
 
-                                    sBgDataModel.addItem(info, false);
+                                    c.checkAndAddItem(info, sBgDataModel);
                                 } else {
                                     throw new RuntimeException("Unexpected null ShortcutInfo");
                                 }
                                 break;
 
                             case LauncherSettings.Favorites.ITEM_TYPE_FOLDER:
-                                id = c.getLong(idIndex);
-                                FolderInfo folderInfo = sBgDataModel.findOrMakeFolder(id);
+                                FolderInfo folderInfo = sBgDataModel.findOrMakeFolder(c.id);
+                                c.applyCommonProperties(folderInfo);
 
                                 // Do not trim the folder label, as is was set by the user.
-                                folderInfo.title = c.getString(cursorIconInfo.titleIndex);
-                                folderInfo.id = id;
-                                folderInfo.container = container;
-                                folderInfo.screenId = c.getInt(screenIndex);
-                                folderInfo.cellX = c.getInt(cellXIndex);
-                                folderInfo.cellY = c.getInt(cellYIndex);
+                                folderInfo.title = c.getString(c.titleIndex);
                                 folderInfo.spanX = 1;
                                 folderInfo.spanY = 1;
                                 folderInfo.options = c.getInt(optionsIndex);
 
-                                // check & update map of what's occupied
-                                if (!checkItemPlacement(occupied, folderInfo, sBgDataModel.workspaceScreens)) {
-                                    itemsToRemove.add(id);
-                                    break;
-                                }
                                 if (restored) {
                                     // no special handling required for restored folders
-                                    restoredRows.add(id);
+                                    c.markRestored();
                                 }
 
-                                sBgDataModel.addItem(folderInfo, false);
+                                c.checkAndAddItem(folderInfo, sBgDataModel);
                                 break;
 
                             case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET:
                             case LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET:
                                 // Read all Launcher-specific widget details
-                                boolean customWidget = itemType ==
+                                boolean customWidget = c.itemType ==
                                     LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET;
 
                                 int appWidgetId = c.getInt(appWidgetIdIndex);
-                                serialNumber = c.getLong(profileIdIndex);
                                 String savedProvider = c.getString(appWidgetProviderIndex);
-                                id = c.getLong(idIndex);
-                                user = allUsers.get(serialNumber);
-                                if (user == null) {
-                                    itemsToRemove.add(id);
-                                    continue;
-                                }
 
                                 final ComponentName component =
                                         ComponentName.unflattenFromString(savedProvider);
@@ -1605,14 +1448,14 @@
                                 final AppWidgetProviderInfo provider = widgetProvidersMap.get(
                                         new ComponentKey(
                                                 ComponentName.unflattenFromString(savedProvider),
-                                                user));
+                                                c.user));
 
                                 final boolean isProviderReady = isValidProvider(provider);
                                 if (!isSafeMode && !customWidget &&
                                         wasProviderReady && !isProviderReady) {
-                                    FileLog.d(TAG, "Deleting widget that isn't installed anymore: "
+                                    c.markDeleted(
+                                            "Deleting widget that isn't installed anymore: "
                                             + provider);
-                                    itemsToRemove.add(id);
                                 } else {
                                     if (isProviderReady) {
                                         appWidgetInfo = new LauncherAppWidgetInfo(appWidgetId,
@@ -1637,7 +1480,7 @@
                                         }
                                         appWidgetInfo.restoreStatus = status;
                                     } else {
-                                        Log.v(TAG, "Widget restore pending id=" + id
+                                        Log.v(TAG, "Widget restore pending id=" + c.id
                                                 + " appWidgetId=" + appWidgetId
                                                 + " status =" + restoreStatus);
                                         appWidgetInfo = new LauncherAppWidgetInfo(appWidgetId,
@@ -1652,8 +1495,7 @@
                                             appWidgetInfo.restoreStatus |=
                                                     LauncherAppWidgetInfo.FLAG_RESTORE_STARTED;
                                         } else if (!isSafeMode) {
-                                            FileLog.d(TAG, "Unrestored widget removed: " + component);
-                                            itemsToRemove.add(id);
+                                            c.markDeleted("Unrestored widget removed: " + component);
                                             continue;
                                         }
 
@@ -1669,44 +1511,31 @@
                                         }
                                     }
 
-                                    appWidgetInfo.id = id;
-                                    appWidgetInfo.screenId = c.getInt(screenIndex);
-                                    appWidgetInfo.cellX = c.getInt(cellXIndex);
-                                    appWidgetInfo.cellY = c.getInt(cellYIndex);
+                                    c.applyCommonProperties(appWidgetInfo);
                                     appWidgetInfo.spanX = c.getInt(spanXIndex);
                                     appWidgetInfo.spanY = c.getInt(spanYIndex);
-                                    appWidgetInfo.user = user;
+                                    appWidgetInfo.user = c.user;
 
-                                    if (container != LauncherSettings.Favorites.CONTAINER_DESKTOP &&
-                                        container != LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
-                                        Log.e(TAG, "Widget found where container != " +
+                                    if (!c.isOnWorkspaceOrHotseat()) {
+                                        c.markDeleted("Widget found where container != " +
                                                 "CONTAINER_DESKTOP nor CONTAINER_HOTSEAT - ignoring!");
-                                        itemsToRemove.add(id);
                                         continue;
                                     }
 
-                                    appWidgetInfo.container = container;
-                                    // check & update map of what's occupied
-                                    if (!checkItemPlacement(occupied, appWidgetInfo, sBgDataModel.workspaceScreens)) {
-                                        itemsToRemove.add(id);
-                                        break;
-                                    }
-
                                     if (!customWidget) {
                                         String providerName =
                                                 appWidgetInfo.providerName.flattenToString();
                                         if (!providerName.equals(savedProvider) ||
                                                 (appWidgetInfo.restoreStatus != restoreStatus)) {
-                                            ContentValues values = new ContentValues();
-                                            values.put(
-                                                    LauncherSettings.Favorites.APPWIDGET_PROVIDER,
-                                                    providerName);
-                                            values.put(LauncherSettings.Favorites.RESTORED,
-                                                    appWidgetInfo.restoreStatus);
-                                            updateItem(id, values);
+                                            c.updater()
+                                                    .put(LauncherSettings.Favorites.APPWIDGET_PROVIDER,
+                                                            providerName)
+                                                    .put(LauncherSettings.Favorites.RESTORED,
+                                                            appWidgetInfo.restoreStatus)
+                                                    .commit();
                                         }
                                     }
-                                    sBgDataModel.addItem(appWidgetInfo, false);
+                                    c.checkAndAddItem(appWidgetInfo, sBgDataModel);
                                 }
                                 break;
                             }
@@ -1724,16 +1553,8 @@
                     return;
                 }
 
-                if (itemsToRemove.size() > 0) {
-                    // Remove dead items
-                    contentResolver.delete(LauncherSettings.Favorites.CONTENT_URI,
-                            Utilities.createDbSelectionQuery(
-                                    LauncherSettings.Favorites._ID, itemsToRemove), null);
-                    if (DEBUG_LOADERS) {
-                        Log.d(TAG, "Removed = " + Utilities.createDbSelectionQuery(
-                                LauncherSettings.Favorites._ID, itemsToRemove));
-                    }
-
+                // Remove dead items
+                if (c.commitDeleted()) {
                     // Remove any empty folder
                     ArrayList<Long> deletedFolderIds = (ArrayList<Long>) LauncherSettings.Settings
                             .call(contentResolver,
@@ -1774,15 +1595,7 @@
                     }
                 }
 
-                if (restoredRows.size() > 0) {
-                    // Update restored items that no longer require special handling
-                    ContentValues values = new ContentValues();
-                    values.put(LauncherSettings.Favorites.RESTORED, 0);
-                    contentResolver.update(LauncherSettings.Favorites.CONTENT_URI, values,
-                            Utilities.createDbSelectionQuery(
-                                    LauncherSettings.Favorites._ID, restoredRows), null);
-                }
-
+                c.commitRestoredItems();
                 if (!isSdCardReady && !pendingPackages.isEmpty()) {
                     context.registerReceiver(
                             new SdCardAvailableReceiver(
@@ -1793,7 +1606,7 @@
                 }
 
                 // Remove any empty screens
-                ArrayList<Long> unusedScreens = new ArrayList<Long>(sBgDataModel.workspaceScreens);
+                ArrayList<Long> unusedScreens = new ArrayList<>(sBgDataModel.workspaceScreens);
                 for (ItemInfo item: sBgDataModel.itemsIdMap) {
                     long screenId = item.screenId;
                     if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP &&
@@ -1807,40 +1620,12 @@
                     sBgDataModel.workspaceScreens.removeAll(unusedScreens);
                     updateWorkspaceScreenOrder(context, sBgDataModel.workspaceScreens);
                 }
-
-                if (DEBUG_LOADERS) {
-                    Log.d(TAG, "loaded workspace in " + (SystemClock.uptimeMillis()-t) + "ms");
-                    Log.d(TAG, "workspace layout: ");
-                    int nScreens = occupied.size();
-                    for (int y = 0; y < countY; y++) {
-                        String line = "";
-
-                        for (int i = 0; i < nScreens; i++) {
-                            long screenId = occupied.keyAt(i);
-                            if (screenId > 0) {
-                                line += " | ";
-                            }
-                        }
-                        Log.d(TAG, "[ " + line + " ]");
-                    }
-                }
             }
             if (LauncherAppState.PROFILE_STARTUP) {
                 Trace.endSection();
             }
         }
 
-        /**
-         * Partially updates the item without any notification. Must be called on the worker thread.
-         */
-        private void updateItem(long itemId, ContentValues update) {
-            mContext.getContentResolver().update(
-                    LauncherSettings.Favorites.CONTENT_URI,
-                    update,
-                    BaseColumns._ID + "= ?",
-                    new String[]{Long.toString(itemId)});
-        }
-
         /** Filters the set of items who are directly or indirectly (via another container) on the
          * specified screen. */
         private void filterCurrentWorkspaceItems(long currentScreenId,
@@ -2482,179 +2267,6 @@
         });
     }
 
-    /**
-     * Make an ShortcutInfo object for a restored application or shortcut item that points
-     * to a package that is not yet installed on the system.
-     */
-    public ShortcutInfo getRestoredItemInfo(Cursor c, Intent intent,
-            int promiseType, int itemType, CursorIconInfo iconInfo) {
-        final ShortcutInfo info = new ShortcutInfo();
-        info.user = Process.myUserHandle();
-        info.promisedIntent = intent;
-
-        info.iconBitmap = iconInfo.loadIcon(c, info);
-        // the fallback icon
-        if (info.iconBitmap == null) {
-            mIconCache.getTitleAndIcon(info, false /* useLowResIcon */);
-        }
-
-        if ((promiseType & ShortcutInfo.FLAG_RESTORED_ICON) != 0) {
-            String title = iconInfo.getTitle(c);
-            if (!TextUtils.isEmpty(title)) {
-                info.title = Utilities.trim(title);
-            }
-        } else if  ((promiseType & ShortcutInfo.FLAG_AUTOINTALL_ICON) != 0) {
-            if (TextUtils.isEmpty(info.title)) {
-                info.title = iconInfo.getTitle(c);
-            }
-        } else {
-            throw new InvalidParameterException("Invalid restoreType " + promiseType);
-        }
-
-        info.contentDescription = mUserManager.getBadgedLabelForUser(info.title, info.user);
-        info.itemType = itemType;
-        info.status = promiseType;
-        return info;
-    }
-
-    /**
-     * Make an Intent object for a restored application or shortcut item that points
-     * to the market page for the item.
-     */
-    @Thunk Intent getRestoredItemIntent(Cursor c, Context context, Intent intent) {
-        ComponentName componentName = intent.getComponent();
-        return getMarketIntent(componentName.getPackageName());
-    }
-
-    static Intent getMarketIntent(String packageName) {
-        return new Intent(Intent.ACTION_VIEW)
-            .setData(new Uri.Builder()
-                .scheme("market")
-                .authority("details")
-                .appendQueryParameter("id", packageName)
-                .build());
-    }
-
-    /**
-     * Make an ShortcutInfo object for a shortcut that is an application.
-     *
-     * If c is not null, then it will be used to fill in missing data like the title and icon.
-     */
-    public ShortcutInfo getAppShortcutInfo(Intent intent, UserHandle user, Cursor c,
-            CursorIconInfo iconInfo, boolean allowMissingTarget, boolean useLowResIcon) {
-        if (user == null) {
-            Log.d(TAG, "Null user found in getShortcutInfo");
-            return null;
-        }
-
-        ComponentName componentName = intent.getComponent();
-        if (componentName == null) {
-            Log.d(TAG, "Missing component found in getShortcutInfo");
-            return null;
-        }
-
-        Intent newIntent = new Intent(intent.getAction(), null);
-        newIntent.addCategory(Intent.CATEGORY_LAUNCHER);
-        newIntent.setComponent(componentName);
-        LauncherActivityInfoCompat lai = mLauncherApps.resolveActivity(newIntent, user);
-        if ((lai == null) && !allowMissingTarget) {
-            Log.d(TAG, "Missing activity found in getShortcutInfo: " + componentName);
-            return null;
-        }
-
-        final ShortcutInfo info = new ShortcutInfo();
-        info.itemType = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
-        info.user = user;
-        info.intent = newIntent;
-
-        mIconCache.getTitleAndIcon(info, lai, useLowResIcon);
-        if (mIconCache.isDefaultIcon(info.iconBitmap, user) && c != null) {
-            Bitmap icon = iconInfo.loadIcon(c);
-            info.iconBitmap = icon != null ? icon : mIconCache.getDefaultIcon(user);
-        }
-
-        if (lai != null && PackageManagerHelper.isAppSuspended(lai.getApplicationInfo())) {
-            info.isDisabled = ShortcutInfo.FLAG_DISABLED_SUSPENDED;
-        }
-
-        // from the db
-        if (TextUtils.isEmpty(info.title) && c != null) {
-            info.title = iconInfo.getTitle(c);
-        }
-
-        // fall back to the class name of the activity
-        if (info.title == null) {
-            info.title = componentName.getClassName();
-        }
-
-        info.contentDescription = mUserManager.getBadgedLabelForUser(info.title, info.user);
-        return info;
-    }
-
-    /**
-     * Make an ShortcutInfo object for a shortcut that isn't an application.
-     */
-    @Thunk ShortcutInfo getShortcutInfo(Cursor c, CursorIconInfo iconInfo) {
-        final ShortcutInfo info = new ShortcutInfo();
-        // Non-app shortcuts are only supported for current user.
-        info.user = Process.myUserHandle();
-        info.itemType = LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT;
-
-        // TODO: If there's an explicit component and we can't install that, delete it.
-
-        loadInfoFromCursor(info, c, iconInfo);
-        return info;
-    }
-
-    /**
-     * Make an ShortcutInfo object for a shortcut that isn't an application.
-     */
-    public void loadInfoFromCursor(ShortcutInfo info, Cursor c, CursorIconInfo iconInfo) {
-        info.title = iconInfo.getTitle(c);
-        info.iconBitmap = iconInfo.loadIcon(c, info);
-        // the fallback icon
-        if (info.iconBitmap == null) {
-            info.iconBitmap = mIconCache.getDefaultIcon(info.user);
-        }
-    }
-
-    ShortcutInfo infoFromShortcutIntent(Context context, Intent data) {
-        Intent intent = data.getParcelableExtra(Intent.EXTRA_SHORTCUT_INTENT);
-        String name = data.getStringExtra(Intent.EXTRA_SHORTCUT_NAME);
-        Parcelable bitmap = data.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON);
-
-        if (intent == null) {
-            // If the intent is null, we can't construct a valid ShortcutInfo, so we return null
-            Log.e(TAG, "Can't construct ShorcutInfo with null intent");
-            return null;
-        }
-
-        final ShortcutInfo info = new ShortcutInfo();
-
-        // Only support intents for current user for now. Intents sent from other
-        // users wouldn't get here without intent forwarding anyway.
-        info.user = Process.myUserHandle();
-
-        if (bitmap instanceof Bitmap) {
-            info.iconBitmap = LauncherIcons.createIconBitmap((Bitmap) bitmap, context);
-        } else {
-            Parcelable extra = data.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE);
-            if (extra instanceof ShortcutIconResource) {
-                info.iconResource = (ShortcutIconResource) extra;
-                info.iconBitmap = LauncherIcons.createIconBitmap(info.iconResource, context);
-            }
-        }
-        if (info.iconBitmap == null) {
-            info.iconBitmap = mIconCache.getDefaultIcon(info.user);
-        }
-
-        info.title = Utilities.trim(name);
-        info.contentDescription = mUserManager.getBadgedLabelForUser(info.title, info.user);
-        info.intent = intent;
-
-        return info;
-    }
-
     static boolean isValidProvider(AppWidgetProviderInfo provider) {
         return (provider != null) && (provider.provider != null)
                 && (provider.provider.getPackageName() != null);
diff --git a/src/com/android/launcher3/graphics/LauncherIcons.java b/src/com/android/launcher3/graphics/LauncherIcons.java
index 3f191bd..0619187 100644
--- a/src/com/android/launcher3/graphics/LauncherIcons.java
+++ b/src/com/android/launcher3/graphics/LauncherIcons.java
@@ -20,9 +20,7 @@
 import android.content.Intent.ShortcutIconResource;
 import android.content.pm.PackageManager;
 import android.content.res.Resources;
-import android.database.Cursor;
 import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
 import android.graphics.Canvas;
 import android.graphics.Paint;
 import android.graphics.PaintFlagsDrawFilter;
@@ -51,16 +49,6 @@
                 Paint.FILTER_BITMAP_FLAG));
     }
 
-
-    public static Bitmap createIconBitmap(Cursor c, int iconIndex, Context context) {
-        byte[] data = c.getBlob(iconIndex);
-        try {
-            return createIconBitmap(BitmapFactory.decodeByteArray(data, 0, data.length), context);
-        } catch (Exception e) {
-            return null;
-        }
-    }
-
     /**
      * Returns a bitmap suitable for the all apps view. If the package or the resource do not
      * exist, it returns null.
diff --git a/src/com/android/launcher3/model/LoaderCursor.java b/src/com/android/launcher3/model/LoaderCursor.java
new file mode 100644
index 0000000..99a6cdf
--- /dev/null
+++ b/src/com/android/launcher3/model/LoaderCursor.java
@@ -0,0 +1,445 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.model;
+
+import android.content.ComponentName;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.Intent.ShortcutIconResource;
+import android.database.Cursor;
+import android.database.CursorWrapper;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.os.UserHandle;
+import android.provider.BaseColumns;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.LongSparseArray;
+
+import com.android.launcher3.IconCache;
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.ItemInfo;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.ShortcutInfo;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.Workspace;
+import com.android.launcher3.compat.LauncherActivityInfoCompat;
+import com.android.launcher3.compat.LauncherAppsCompat;
+import com.android.launcher3.compat.UserManagerCompat;
+import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.graphics.LauncherIcons;
+import com.android.launcher3.logging.FileLog;
+import com.android.launcher3.util.ContentWriter;
+import com.android.launcher3.util.GridOccupancy;
+import com.android.launcher3.util.LongArrayMap;
+import com.android.launcher3.util.PackageManagerHelper;
+
+import java.security.InvalidParameterException;
+import java.util.ArrayList;
+
+/**
+ * Extension of {@link Cursor} with utility methods for workspace loading.
+ */
+public class LoaderCursor extends CursorWrapper {
+
+    private static final String TAG = "LoaderCursor";
+
+    public final LongSparseArray<UserHandle> allUsers = new LongSparseArray<>();
+
+    private final Context mContext;
+    private final UserManagerCompat mUserManager;
+    private final IconCache mIconCache;
+    private final InvariantDeviceProfile mIDP;
+
+    private final ArrayList<Long> itemsToRemove = new ArrayList<>();
+    private final ArrayList<Long> restoredRows = new ArrayList<>();
+    private final LongArrayMap<GridOccupancy> occupied = new LongArrayMap<>();
+
+    private final int iconPackageIndex;
+    private final int iconResourceIndex;
+    private final int iconIndex;
+    public final int titleIndex;
+
+    private final int idIndex;
+    private final int containerIndex;
+    private final int itemTypeIndex;
+    private final int screenIndex;
+    private final int cellXIndex;
+    private final int cellYIndex;
+    private final int profileIdIndex;
+
+    // Properties loaded per iteration
+    public long serialNumber;
+    public UserHandle user;
+    public long id;
+    public long container;
+    public int itemType;
+
+    public LoaderCursor(Cursor c, LauncherAppState app) {
+        super(c);
+        mContext = app.getContext();
+        mIconCache = app.getIconCache();
+        mIDP = app.getInvariantDeviceProfile();
+        mUserManager = UserManagerCompat.getInstance(mContext);
+
+        // Init column indices
+        iconIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.ICON);
+        iconPackageIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_PACKAGE);
+        iconResourceIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_RESOURCE);
+        titleIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.TITLE);
+
+        idIndex = getColumnIndexOrThrow(LauncherSettings.Favorites._ID);
+        containerIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.CONTAINER);
+        itemTypeIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.ITEM_TYPE);
+        screenIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.SCREEN);
+        cellXIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.CELLX);
+        cellYIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.CELLY);
+        profileIdIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.PROFILE_ID);
+    }
+
+    @Override
+    public boolean moveToNext() {
+        boolean result = super.moveToNext();
+        if (result) {
+            // Load common properties.
+            itemType = getInt(itemTypeIndex);
+            container = getInt(containerIndex);
+            id = getLong(idIndex);
+            serialNumber = getInt(profileIdIndex);
+            user = allUsers.get(serialNumber);
+        }
+        return result;
+    }
+
+    public ShortcutInfo loadSimpleShortcut() {
+        final ShortcutInfo info = new ShortcutInfo();
+        // Non-app shortcuts are only supported for current user.
+        info.user = user;
+        info.itemType = itemType;
+        info.title = getTitle();
+        info.iconBitmap = loadIcon(info);
+        // the fallback icon
+        if (info.iconBitmap == null) {
+            info.iconBitmap = mIconCache.getDefaultIcon(info.user);
+        }
+
+        // TODO: If there's an explicit component and we can't install that, delete it.
+
+        return info;
+    }
+
+    /**
+     * Loads the icon from the cursor and updates the {@param info} if the icon is an app resource.
+     */
+    protected Bitmap loadIcon(ShortcutInfo info) {
+        Bitmap icon = null;
+        if (itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT) {
+            String packageName = getString(iconPackageIndex);
+            String resourceName = getString(iconResourceIndex);
+            if (!TextUtils.isEmpty(packageName) || !TextUtils.isEmpty(resourceName)) {
+                info.iconResource = new ShortcutIconResource();
+                info.iconResource.packageName = packageName;
+                info.iconResource.resourceName = resourceName;
+                icon = LauncherIcons.createIconBitmap(info.iconResource, mContext);
+            }
+        }
+        if (icon == null) {
+            // Failed to load from resource, try loading from DB.
+            byte[] data = getBlob(iconIndex);
+            try {
+                icon = LauncherIcons.createIconBitmap(
+                        BitmapFactory.decodeByteArray(data, 0, data.length), mContext);
+            } catch (Exception e) {
+                return null;
+            }
+        }
+        return icon;
+    }
+
+    /**
+     * Returns the title or empty string
+     */
+    private String getTitle() {
+        String title = getString(titleIndex);
+        return TextUtils.isEmpty(title) ? "" : Utilities.trim(title);
+    }
+
+
+    /**
+     * Make an ShortcutInfo object for a restored application or shortcut item that points
+     * to a package that is not yet installed on the system.
+     */
+    public ShortcutInfo getRestoredItemInfo(Intent intent, int promiseType) {
+        final ShortcutInfo info = new ShortcutInfo();
+        info.user = user;
+        info.promisedIntent = intent;
+
+        info.iconBitmap = loadIcon(info);
+        // the fallback icon
+        if (info.iconBitmap == null) {
+            mIconCache.getTitleAndIcon(info, false /* useLowResIcon */);
+        }
+
+        if ((promiseType & ShortcutInfo.FLAG_RESTORED_ICON) != 0) {
+            String title = getTitle();
+            if (!TextUtils.isEmpty(title)) {
+                info.title = Utilities.trim(title);
+            }
+        } else if  ((promiseType & ShortcutInfo.FLAG_AUTOINTALL_ICON) != 0) {
+            if (TextUtils.isEmpty(info.title)) {
+                info.title = getTitle();
+            }
+        } else {
+            throw new InvalidParameterException("Invalid restoreType " + promiseType);
+        }
+
+        info.contentDescription = mUserManager.getBadgedLabelForUser(info.title, info.user);
+        info.itemType = itemType;
+        info.status = promiseType;
+        return info;
+    }
+
+    /**
+     * Make an ShortcutInfo object for a shortcut that is an application.
+     */
+    public ShortcutInfo getAppShortcutInfo(
+            Intent intent, boolean allowMissingTarget, boolean useLowResIcon) {
+        if (user == null) {
+            Log.d(TAG, "Null user found in getShortcutInfo");
+            return null;
+        }
+
+        ComponentName componentName = intent.getComponent();
+        if (componentName == null) {
+            Log.d(TAG, "Missing component found in getShortcutInfo");
+            return null;
+        }
+
+        Intent newIntent = new Intent(Intent.ACTION_MAIN, null);
+        newIntent.addCategory(Intent.CATEGORY_LAUNCHER);
+        newIntent.setComponent(componentName);
+        LauncherActivityInfoCompat lai = LauncherAppsCompat.getInstance(mContext)
+                .resolveActivity(newIntent, user);
+        if ((lai == null) && !allowMissingTarget) {
+            Log.d(TAG, "Missing activity found in getShortcutInfo: " + componentName);
+            return null;
+        }
+
+        final ShortcutInfo info = new ShortcutInfo();
+        info.itemType = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
+        info.user = user;
+        info.intent = newIntent;
+
+        mIconCache.getTitleAndIcon(info, lai, useLowResIcon);
+        if (mIconCache.isDefaultIcon(info.iconBitmap, user)) {
+            Bitmap icon = loadIcon(info);
+            info.iconBitmap = icon != null ? icon : info.iconBitmap;
+        }
+
+        if (lai != null && PackageManagerHelper.isAppSuspended(lai.getApplicationInfo())) {
+            info.isDisabled = ShortcutInfo.FLAG_DISABLED_SUSPENDED;
+        }
+
+        // from the db
+        if (TextUtils.isEmpty(info.title)) {
+            info.title = getTitle();
+        }
+
+        // fall back to the class name of the activity
+        if (info.title == null) {
+            info.title = componentName.getClassName();
+        }
+
+        info.contentDescription = mUserManager.getBadgedLabelForUser(info.title, info.user);
+        return info;
+    }
+
+    /**
+     * Returns a {@link ContentWriter} which can be used to update the current item.
+     */
+    public ContentWriter updater() {
+       return new ContentWriter(mContext, new ContentWriter.CommitParams(
+               BaseColumns._ID + "= ?", new String[]{Long.toString(id)}));
+    }
+
+    /**
+     * Marks the current item for removal
+     */
+    public void markDeleted(String reason) {
+        FileLog.e(TAG, reason);
+        itemsToRemove.add(id);
+    }
+
+    /**
+     * Removes any items marked for removal.
+     * @return true is any item was removed.
+     */
+    public boolean commitDeleted() {
+        if (itemsToRemove.size() > 0) {
+            // Remove dead items
+            mContext.getContentResolver().delete(LauncherSettings.Favorites.CONTENT_URI,
+                    Utilities.createDbSelectionQuery(
+                            LauncherSettings.Favorites._ID, itemsToRemove), null);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Marks the current item as restored
+     */
+    public void markRestored() {
+        restoredRows.add(id);
+    }
+
+    public void commitRestoredItems() {
+        if (restoredRows.size() > 0) {
+            // 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,
+                    Utilities.createDbSelectionQuery(
+                            LauncherSettings.Favorites._ID, restoredRows), null);
+        }
+    }
+
+    /**
+     * Returns true is the item is on workspace or hotseat
+     */
+    public boolean isOnWorkspaceOrHotseat() {
+        return container == LauncherSettings.Favorites.CONTAINER_DESKTOP ||
+                container == LauncherSettings.Favorites.CONTAINER_HOTSEAT;
+    }
+
+    /**
+     * Applies the following properties:
+     * {@link ItemInfo#id}
+     * {@link ItemInfo#container}
+     * {@link ItemInfo#screenId}
+     * {@link ItemInfo#cellX}
+     * {@link ItemInfo#cellY}
+     */
+    public void applyCommonProperties(ItemInfo info) {
+        info.id = id;
+        info.container = container;
+        info.screenId = getInt(screenIndex);
+        info.cellX = getInt(cellXIndex);
+        info.cellY = getInt(cellYIndex);
+    }
+
+    /**
+     * Adds the {@param info} to {@param dataModel} if it does not overlap with any other item,
+     * otherwise marks it for deletion.
+     */
+    public void checkAndAddItem(ItemInfo info, BgDataModel dataModel) {
+        if (checkItemPlacement(info, dataModel.workspaceScreens)) {
+            dataModel.addItem(info, false);
+        } else {
+            markDeleted("Item position overlap");
+        }
+    }
+
+    /**
+     * check & update map of what's occupied; used to discard overlapping/invalid items
+     */
+    protected boolean checkItemPlacement(ItemInfo item, ArrayList<Long> workspaceScreens) {
+        long containerIndex = item.screenId;
+        if (item.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
+            // Return early if we detect that an item is under the hotseat button
+            if (!FeatureFlags.NO_ALL_APPS_ICON &&
+                    mIDP.isAllAppsButtonRank((int) item.screenId)) {
+                Log.e(TAG, "Error loading shortcut into hotseat " + item
+                        + " into position (" + item.screenId + ":" + item.cellX + ","
+                        + item.cellY + ") occupied by all apps");
+                return false;
+            }
+
+            final GridOccupancy hotseatOccupancy =
+                    occupied.get((long) LauncherSettings.Favorites.CONTAINER_HOTSEAT);
+
+            if (item.screenId >= mIDP.numHotseatIcons) {
+                Log.e(TAG, "Error loading shortcut " + item
+                        + " into hotseat position " + item.screenId
+                        + ", position out of bounds: (0 to " + (mIDP.numHotseatIcons - 1)
+                        + ")");
+                return false;
+            }
+
+            if (hotseatOccupancy != null) {
+                if (hotseatOccupancy.cells[(int) item.screenId][0]) {
+                    Log.e(TAG, "Error loading shortcut into hotseat " + item
+                            + " into position (" + item.screenId + ":" + item.cellX + ","
+                            + item.cellY + ") already occupied");
+                    return false;
+                } else {
+                    hotseatOccupancy.cells[(int) item.screenId][0] = true;
+                    return true;
+                }
+            } else {
+                final GridOccupancy occupancy = new GridOccupancy(mIDP.numHotseatIcons, 1);
+                occupancy.cells[(int) item.screenId][0] = true;
+                occupied.put((long) LauncherSettings.Favorites.CONTAINER_HOTSEAT, occupancy);
+                return true;
+            }
+        } else if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) {
+            if (!workspaceScreens.contains((Long) item.screenId)) {
+                // The item has an invalid screen id.
+                return false;
+            }
+        } else {
+            // Skip further checking if it is not the hotseat or workspace container
+            return true;
+        }
+
+        final int countX = mIDP.numColumns;
+        final int countY = mIDP.numRows;
+        if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP &&
+                item.cellX < 0 || item.cellY < 0 ||
+                item.cellX + item.spanX > countX || item.cellY + item.spanY > countY) {
+            Log.e(TAG, "Error loading shortcut " + item
+                    + " into cell (" + containerIndex + "-" + item.screenId + ":"
+                    + item.cellX + "," + item.cellY
+                    + ") out of screen bounds ( " + countX + "x" + countY + ")");
+            return false;
+        }
+
+        if (!occupied.containsKey(item.screenId)) {
+            GridOccupancy screen = new GridOccupancy(countX + 1, countY + 1);
+            if (item.screenId == Workspace.FIRST_SCREEN_ID) {
+                // Mark the first row as occupied (if the feature is enabled)
+                // in order to account for the QSB.
+                screen.markCells(0, 0, countX + 1, 1, FeatureFlags.QSB_ON_FIRST_SCREEN);
+            }
+            occupied.put(item.screenId, screen);
+        }
+        final GridOccupancy occupancy = occupied.get(item.screenId);
+
+        // Check if any workspace icons overlap with each other
+        if (occupancy.isRegionVacant(item.cellX, item.cellY, item.spanX, item.spanY)) {
+            occupancy.markCells(item, true);
+            return true;
+        } else {
+            Log.e(TAG, "Error loading shortcut " + item
+                    + " into cell (" + containerIndex + "-" + item.screenId + ":"
+                    + item.cellX + "," + item.cellX + "," + item.spanX + "," + item.spanY
+                    + ") already occupied");
+            return false;
+        }
+    }
+}
diff --git a/src/com/android/launcher3/util/ContentWriter.java b/src/com/android/launcher3/util/ContentWriter.java
index 1c347c0..76ba9d6 100644
--- a/src/com/android/launcher3/util/ContentWriter.java
+++ b/src/com/android/launcher3/util/ContentWriter.java
@@ -20,6 +20,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.graphics.Bitmap;
+import android.net.Uri;
 import android.os.UserHandle;
 
 import com.android.launcher3.LauncherAppState;
@@ -35,9 +36,15 @@
     private final ContentValues mValues;
     private final Context mContext;
 
+    private CommitParams mCommitParams;
     private Bitmap mIcon;
     private UserHandle mUser;
 
+    public ContentWriter(Context context, CommitParams commitParams) {
+        this(context);
+        mCommitParams = commitParams;
+    }
+
     public ContentWriter(Context context) {
         this(new ContentValues(), context);
     }
@@ -95,4 +102,25 @@
         }
         return mValues;
     }
+
+    public int commit() {
+        if (mCommitParams != null) {
+            return mContext.getContentResolver().update(mCommitParams.mUri, getValues(),
+                    mCommitParams.mWhere, mCommitParams.mSelectionArgs);
+        }
+        return 0;
+    }
+
+    public static final class CommitParams {
+
+        final Uri mUri = LauncherSettings.Favorites.CONTENT_URI;
+        String mWhere;
+        String[] mSelectionArgs;
+
+        public CommitParams(String where, String[] selectionArgs) {
+            mWhere = where;
+            mSelectionArgs = selectionArgs;
+        }
+
+    }
 }
diff --git a/src/com/android/launcher3/util/CursorIconInfo.java b/src/com/android/launcher3/util/CursorIconInfo.java
deleted file mode 100644
index 3bc4eab..0000000
--- a/src/com/android/launcher3/util/CursorIconInfo.java
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.util;
-
-import android.content.Context;
-import android.content.Intent.ShortcutIconResource;
-import android.database.Cursor;
-import android.graphics.Bitmap;
-import android.text.TextUtils;
-
-import com.android.launcher3.LauncherSettings;
-import com.android.launcher3.ShortcutInfo;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.graphics.LauncherIcons;
-
-/**
- * Utility class to load icon from a cursor.
- */
-public class CursorIconInfo {
-    public final int iconPackageIndex;
-    public final int iconResourceIndex;
-    public final int iconIndex;
-
-    public final int titleIndex;
-
-    private final Context mContext;
-
-    public CursorIconInfo(Context context, Cursor c) {
-        mContext = context;
-
-        iconIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON);
-        iconPackageIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_PACKAGE);
-        iconResourceIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_RESOURCE);
-
-        titleIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.TITLE);
-    }
-
-    /**
-     * Loads the icon from the cursor and updates the {@param info} if the icon is an app resource.
-     */
-    public Bitmap loadIcon(Cursor c, ShortcutInfo info) {
-        Bitmap icon = null;
-        String packageName = c.getString(iconPackageIndex);
-        String resourceName = c.getString(iconResourceIndex);
-        if (!TextUtils.isEmpty(packageName) || !TextUtils.isEmpty(resourceName)) {
-            info.iconResource = new ShortcutIconResource();
-            info.iconResource.packageName = packageName;
-            info.iconResource.resourceName = resourceName;
-            icon = LauncherIcons.createIconBitmap(info.iconResource, mContext);
-        }
-        if (icon == null) {
-            // Failed to load from resource, try loading from DB.
-            icon = loadIcon(c);
-        }
-        return icon;
-    }
-
-    /**
-     * Loads the fixed bitmap from the icon if available.
-     */
-    public Bitmap loadIcon(Cursor c) {
-        return LauncherIcons.createIconBitmap(c, iconIndex, mContext);
-    }
-
-    /**
-     * Returns the title or empty string
-     */
-    public String getTitle(Cursor c) {
-        String title = c.getString(titleIndex);
-        return TextUtils.isEmpty(title) ? "" : Utilities.trim(c.getString(titleIndex));
-    }
-}
diff --git a/src/com/android/launcher3/util/PackageManagerHelper.java b/src/com/android/launcher3/util/PackageManagerHelper.java
index b61d609..33a9fc6 100644
--- a/src/com/android/launcher3/util/PackageManagerHelper.java
+++ b/src/com/android/launcher3/util/PackageManagerHelper.java
@@ -23,6 +23,7 @@
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.pm.ResolveInfo;
+import android.net.Uri;
 import android.os.Build;
 import android.text.TextUtils;
 
@@ -124,4 +125,13 @@
 
         return false;
     }
+
+    public static Intent getMarketIntent(String packageName) {
+        return new Intent(Intent.ACTION_VIEW)
+                .setData(new Uri.Builder()
+                        .scheme("market")
+                        .authority("details")
+                        .appendQueryParameter("id", packageName)
+                        .build());
+    }
 }