Caching last predictions and loading it with model

Adding support for persisting itemInfos on disk. This uses
a separate xml file. Unlike prefs, it does not keep the items
in memory and is just a wraper over reading/writing a file.

Bug: 160748731
Change-Id: Iaccab9928ab8f30127fb3c2d630ca8ca83f0bd05
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/model/PredictionUpdateTask.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/model/PredictionUpdateTask.java
index 721e2be..b0fba3d 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/model/PredictionUpdateTask.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/model/PredictionUpdateTask.java
@@ -21,6 +21,7 @@
 
 import android.app.prediction.AppTarget;
 import android.content.ComponentName;
+import android.content.Context;
 import android.content.pm.LauncherActivityInfo;
 import android.content.pm.LauncherApps;
 import android.content.pm.ShortcutInfo;
@@ -29,6 +30,7 @@
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.model.BgDataModel.FixedContainerItems;
+import com.android.launcher3.model.QuickstepModelDelegate.PredictorState;
 import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 
@@ -43,27 +45,22 @@
 public class PredictionUpdateTask extends BaseModelUpdateTask {
 
     private final List<AppTarget> mTargets;
-    private final int mContainerId;
+    private final PredictorState mPredictorState;
 
-    PredictionUpdateTask(int containerId, List<AppTarget> targets) {
-        mContainerId = containerId;
+    PredictionUpdateTask(PredictorState predictorState, List<AppTarget> targets) {
+        mPredictorState = predictorState;
         mTargets = targets;
     }
 
     @Override
     public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList apps) {
-        // TODO: persist the whole list
-        Utilities.getDevicePrefs(app.getContext()).edit()
+        Context context = app.getContext();
+
+        // TODO: remove this
+        Utilities.getDevicePrefs(context).edit()
                 .putBoolean(LAST_PREDICTION_ENABLED_STATE, !mTargets.isEmpty()).apply();
 
-        FixedContainerItems fci;
-        synchronized (dataModel) {
-            fci = dataModel.extraItems.get(mContainerId);
-            if (fci == null) {
-                return;
-            }
-        }
-
+        FixedContainerItems fci = mPredictorState.items;
         Set<UserHandle> usersForChangedShortcuts = new HashSet<>(fci.items.stream()
                 .filter(info -> info.itemType == ITEM_TYPE_DEEP_SHORTCUT)
                 .map(info -> info.user)
@@ -75,7 +72,7 @@
             ShortcutInfo si = target.getShortcutInfo();
             if (si != null) {
                 usersForChangedShortcuts.add(si.getUserHandle());
-                itemInfo = new WorkspaceItemInfo(si, app.getContext());
+                itemInfo = new WorkspaceItemInfo(si, context);
                 app.getIconCache().getShortcutIcon(itemInfo, si);
             } else {
                 String className = target.getClassName();
@@ -87,16 +84,18 @@
                 UserHandle user = target.getUser();
                 itemInfo = apps.data.stream()
                         .filter(info -> user.equals(info.user) && cn.equals(info.componentName))
-                        .map(AppInfo::makeWorkspaceItem)
+                        .map(ai -> {
+                            app.getIconCache().getTitleAndIcon(ai, false);
+                            return ai.makeWorkspaceItem();
+                        })
                         .findAny()
                         .orElseGet(() -> {
-                            LauncherActivityInfo lai = app.getContext()
-                                    .getSystemService(LauncherApps.class)
+                            LauncherActivityInfo lai = context.getSystemService(LauncherApps.class)
                                     .resolveActivity(AppInfo.makeLaunchIntent(cn), user);
                             if (lai == null) {
                                 return null;
                             }
-                            AppInfo ai = new AppInfo(app.getContext(), lai, user);
+                            AppInfo ai = new AppInfo(context, lai, user);
                             app.getIconCache().getTitleAndIcon(ai, lai, false);
                             return ai.makeWorkspaceItem();
                         });
@@ -106,12 +105,15 @@
                 }
             }
 
-            itemInfo.container = mContainerId;
+            itemInfo.container = fci.containerId;
             fci.items.add(itemInfo);
         }
 
         bindExtraContainerItems(fci);
         usersForChangedShortcuts.forEach(
                 u -> dataModel.updateShortcutPinnedState(app.getContext(), u));
+
+        // Save to disk
+        mPredictorState.storage.write(context, fci.items);
     }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/model/QuickstepModelDelegate.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/model/QuickstepModelDelegate.java
index b516469..166cb6c 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/model/QuickstepModelDelegate.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/model/QuickstepModelDelegate.java
@@ -17,6 +17,8 @@
 
 import static com.android.launcher3.InvariantDeviceProfile.CHANGE_FLAG_GRID;
 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_PREDICTION;
+import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
+import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT;
 
 import android.app.prediction.AppPredictionContext;
 import android.app.prediction.AppPredictionManager;
@@ -24,16 +26,32 @@
 import android.app.prediction.AppTarget;
 import android.app.prediction.AppTargetEvent;
 import android.content.Context;
+import android.content.Intent;
+import android.content.pm.LauncherActivityInfo;
+import android.content.pm.LauncherApps;
+import android.content.pm.ShortcutInfo;
+import android.os.UserHandle;
 
+import androidx.annotation.Nullable;
 import androidx.annotation.WorkerThread;
 
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.InvariantDeviceProfile.OnIDPChangeListener;
+import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.model.BgDataModel.FixedContainerItems;
+import com.android.launcher3.model.data.AppInfo;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.shortcuts.ShortcutKey;
 import com.android.launcher3.util.Executors;
+import com.android.launcher3.util.PersistedItemArray;
 import com.android.quickstep.logging.StatsLogCompatManager;
 
+import java.util.Collections;
 import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.IntStream;
 
 /**
  * Model delegate which loads prediction items
@@ -42,10 +60,12 @@
 
     public static final String LAST_PREDICTION_ENABLED_STATE = "last_prediction_enabled_state";
 
+    private final PredictorState mAllAppsState =
+            new PredictorState(CONTAINER_PREDICTION, "all_apps_predictions");
+
     private final InvariantDeviceProfile mIDP;
     private final AppEventProducer mAppEventProducer;
 
-    private AppPredictor mAllAppsPredictor;
     private boolean mActive = false;
 
     public QuickstepModelDelegate(Context context) {
@@ -57,11 +77,15 @@
     }
 
     @Override
-    public void loadItems() {
+    public void loadItems(UserManagerState ums, Map<ShortcutKey, ShortcutInfo> pinnedShortcuts) {
         // TODO: Implement caching and preloading
-        super.loadItems();
-        mDataModel.extraItems.put(
-                CONTAINER_PREDICTION, new FixedContainerItems(CONTAINER_PREDICTION));
+        super.loadItems(ums, pinnedShortcuts);
+
+        WorkspaceItemFactory factory =
+                new WorkspaceItemFactory(mApp, ums, pinnedShortcuts, mIDP.numAllAppsColumns);
+        mAllAppsState.items.setItems(
+                mAllAppsState.storage.read(mApp.getContext(), factory, ums.allUsers::get));
+        mDataModel.extraItems.put(CONTAINER_PREDICTION, mAllAppsState.items);
 
         mActive = true;
         recreatePredictors();
@@ -70,8 +94,8 @@
     @Override
     public void validateData() {
         super.validateData();
-        if (mAllAppsPredictor != null) {
-            mAllAppsPredictor.requestPredictionUpdate();
+        if (mAllAppsState.predictor != null) {
+            mAllAppsState.predictor.requestPredictionUpdate();
         }
     }
 
@@ -86,10 +110,7 @@
     }
 
     private void destroyPredictors() {
-        if (mAllAppsPredictor != null) {
-            mAllAppsPredictor.destroy();
-            mAllAppsPredictor = null;
-        }
+        mAllAppsState.destroyPredictor();
     }
 
     @WorkerThread
@@ -98,7 +119,6 @@
         if (!mActive) {
             return;
         }
-
         Context context = mApp.getContext();
         AppPredictionManager apm = context.getSystemService(AppPredictionManager.class);
         if (apm == null) {
@@ -107,19 +127,23 @@
 
         int count = mIDP.numAllAppsColumns;
 
-        mAllAppsPredictor = apm.createAppPredictionSession(
+        mAllAppsState.predictor = apm.createAppPredictionSession(
                 new AppPredictionContext.Builder(context)
                         .setUiSurface("home")
                         .setPredictedTargetCount(count)
                         .build());
-        mAllAppsPredictor.registerPredictionUpdates(
-                Executors.MODEL_EXECUTOR, this::onAllAppsPredictionChanged);
-        mAllAppsPredictor.requestPredictionUpdate();
+        mAllAppsState.predictor.registerPredictionUpdates(
+                Executors.MODEL_EXECUTOR, t -> handleUpdate(mAllAppsState, t));
+        mAllAppsState.predictor.requestPredictionUpdate();
     }
 
-    private void onAllAppsPredictionChanged(List<AppTarget> targets) {
-        mApp.getModel().enqueueModelUpdateTask(
-                new PredictionUpdateTask(CONTAINER_PREDICTION, targets));
+
+    private void handleUpdate(PredictorState state, List<AppTarget> targets) {
+        if (state.setTargets(targets)) {
+            // No diff, skip
+            return;
+        }
+        mApp.getModel().enqueueModelUpdateTask(new PredictionUpdateTask(state, targets));
     }
 
     @Override
@@ -131,8 +155,119 @@
     }
 
     private void onAppTargetEvent(AppTargetEvent event) {
-        if (mAllAppsPredictor != null) {
-            mAllAppsPredictor.notifyAppTargetEvent(event);
+        if (mAllAppsState.predictor != null) {
+            mAllAppsState.predictor.notifyAppTargetEvent(event);
+        }
+    }
+
+    static class PredictorState {
+
+        public final FixedContainerItems items;
+        public final PersistedItemArray storage;
+        public AppPredictor predictor;
+
+        private List<AppTarget> mLastTargets;
+
+        PredictorState(int container, String storageName) {
+            items = new FixedContainerItems(container);
+            storage = new PersistedItemArray(storageName);
+            mLastTargets = Collections.emptyList();
+        }
+
+        public void destroyPredictor() {
+            if (predictor != null) {
+                predictor.destroy();
+                predictor = null;
+            }
+        }
+
+        /**
+         * Sets the new targets and returns true if it was different than before.
+         */
+        boolean setTargets(List<AppTarget> newTargets) {
+            List<AppTarget> oldTargets = mLastTargets;
+            mLastTargets = newTargets;
+
+            int size = oldTargets.size();
+            return size == newTargets.size() && IntStream.range(0, size)
+                    .allMatch(i -> areAppTargetsSame(oldTargets.get(i), newTargets.get(i)));
+        }
+    }
+
+    /**
+     * Compares two targets for the properties which we care about
+     */
+    private static boolean areAppTargetsSame(AppTarget t1, AppTarget t2) {
+        if (!Objects.equals(t1.getPackageName(), t2.getPackageName())
+                || !Objects.equals(t1.getUser(), t2.getUser())
+                || !Objects.equals(t1.getClassName(), t2.getClassName())) {
+            return false;
+        }
+
+        ShortcutInfo s1 = t1.getShortcutInfo();
+        ShortcutInfo s2 = t2.getShortcutInfo();
+        if (s1 != null) {
+            if (s2 == null || !Objects.equals(s1.getId(), s2.getId())) {
+                return false;
+            }
+        } else if (s2 != null) {
+            return false;
+        }
+        return true;
+    }
+
+    private static class WorkspaceItemFactory implements PersistedItemArray.ItemFactory {
+
+        private final LauncherAppState mAppState;
+        private final UserManagerState mUMS;
+        private final Map<ShortcutKey, ShortcutInfo> mPinnedShortcuts;
+        private final int mMaxCount;
+
+        private int mReadCount = 0;
+
+        protected WorkspaceItemFactory(LauncherAppState appState, UserManagerState ums,
+                Map<ShortcutKey, ShortcutInfo> pinnedShortcuts, int maxCount) {
+            mAppState = appState;
+            mUMS = ums;
+            mPinnedShortcuts = pinnedShortcuts;
+            mMaxCount = maxCount;
+        }
+
+        @Nullable
+        @Override
+        public ItemInfo createInfo(int itemType, UserHandle user, Intent intent) {
+            if (mReadCount >= mMaxCount) {
+                return null;
+            }
+            switch (itemType) {
+                case ITEM_TYPE_APPLICATION: {
+                    LauncherActivityInfo lai = mAppState.getContext()
+                            .getSystemService(LauncherApps.class)
+                            .resolveActivity(intent, user);
+                    if (lai == null) {
+                        return null;
+                    }
+                    AppInfo info = new AppInfo(lai, user, mUMS.isUserQuiet(user));
+                    mAppState.getIconCache().getTitleAndIcon(info, lai, false);
+                    mReadCount++;
+                    return info.makeWorkspaceItem();
+                }
+                case ITEM_TYPE_DEEP_SHORTCUT: {
+                    ShortcutKey key = ShortcutKey.fromIntent(intent, user);
+                    if (key == null) {
+                        return null;
+                    }
+                    ShortcutInfo si = mPinnedShortcuts.get(key);
+                    if (si == null) {
+                        return null;
+                    }
+                    WorkspaceItemInfo wii = new WorkspaceItemInfo(si, mAppState.getContext());
+                    mAppState.getIconCache().getShortcutIcon(wii, si);
+                    mReadCount++;
+                    return wii;
+                }
+            }
+            return null;
         }
     }
 }
diff --git a/robolectric_tests/src/com/android/launcher3/model/LoaderCursorTest.java b/robolectric_tests/src/com/android/launcher3/model/LoaderCursorTest.java
index 0e760f9..fb08c56 100644
--- a/robolectric_tests/src/com/android/launcher3/model/LoaderCursorTest.java
+++ b/robolectric_tests/src/com/android/launcher3/model/LoaderCursorTest.java
@@ -51,7 +51,7 @@
 
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.util.Executors;
@@ -92,9 +92,9 @@
                 SCREEN, CELLX, CELLY, RESTORED, INTENT
         });
 
-        mLoaderCursor = new LoaderCursor(mCursor, LauncherSettings.Favorites.CONTENT_URI, mApp,
-                new UserManagerState());
-        mLoaderCursor.allUsers.put(0, Process.myUserHandle());
+        UserManagerState ums = new UserManagerState();
+        mLoaderCursor = new LoaderCursor(mCursor, Favorites.CONTENT_URI, mApp, ums);
+        ums.allUsers.put(0, Process.myUserHandle());
     }
 
     private void initCursor(int itemType, String title) {
diff --git a/src/com/android/launcher3/AutoInstallsLayout.java b/src/com/android/launcher3/AutoInstallsLayout.java
index 432073e..f61bc05 100644
--- a/src/com/android/launcher3/AutoInstallsLayout.java
+++ b/src/com/android/launcher3/AutoInstallsLayout.java
@@ -658,7 +658,7 @@
         }
     }
 
-    protected static void beginDocument(XmlPullParser parser, String firstElementName)
+    public static void beginDocument(XmlPullParser parser, String firstElementName)
             throws XmlPullParserException, IOException {
         int type;
         while ((type = parser.next()) != XmlPullParser.START_TAG
diff --git a/src/com/android/launcher3/model/BaseLoaderResults.java b/src/com/android/launcher3/model/BaseLoaderResults.java
index 8b0ef7b..586333f 100644
--- a/src/com/android/launcher3/model/BaseLoaderResults.java
+++ b/src/com/android/launcher3/model/BaseLoaderResults.java
@@ -27,6 +27,7 @@
 import com.android.launcher3.LauncherModel.CallbackTask;
 import com.android.launcher3.PagedView;
 import com.android.launcher3.model.BgDataModel.Callbacks;
+import com.android.launcher3.model.BgDataModel.FixedContainerItems;
 import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
@@ -76,18 +77,20 @@
         ArrayList<ItemInfo> workspaceItems = new ArrayList<>();
         ArrayList<LauncherAppWidgetInfo> appWidgets = new ArrayList<>();
         final IntArray orderedScreenIds = new IntArray();
+        ArrayList<FixedContainerItems> extraItems = new ArrayList<>();
 
         synchronized (mBgDataModel) {
             workspaceItems.addAll(mBgDataModel.workspaceItems);
             appWidgets.addAll(mBgDataModel.appWidgets);
             orderedScreenIds.addAll(mBgDataModel.collectWorkspaceScreens());
+            mBgDataModel.extraItems.forEach(extraItems::add);
             mBgDataModel.lastBindId++;
             mMyBindingId = mBgDataModel.lastBindId;
         }
 
         for (Callbacks cb : mCallbacksList) {
             new WorkspaceBinder(cb, mUiExecutor, mApp, mBgDataModel, mMyBindingId,
-                    workspaceItems, appWidgets, orderedScreenIds).bind();
+                    workspaceItems, appWidgets, extraItems, orderedScreenIds).bind();
         }
     }
 
@@ -135,7 +138,7 @@
         private final ArrayList<ItemInfo> mWorkspaceItems;
         private final ArrayList<LauncherAppWidgetInfo> mAppWidgets;
         private final IntArray mOrderedScreenIds;
-
+        private final ArrayList<FixedContainerItems> mExtraItems;
 
         WorkspaceBinder(Callbacks callbacks,
                 Executor uiExecutor,
@@ -144,6 +147,7 @@
                 int myBindingId,
                 ArrayList<ItemInfo> workspaceItems,
                 ArrayList<LauncherAppWidgetInfo> appWidgets,
+                ArrayList<FixedContainerItems> extraItems,
                 IntArray orderedScreenIds) {
             mCallbacks = callbacks;
             mUiExecutor = uiExecutor;
@@ -152,6 +156,7 @@
             mMyBindingId = myBindingId;
             mWorkspaceItems = workspaceItems;
             mAppWidgets = appWidgets;
+            mExtraItems = extraItems;
             mOrderedScreenIds = orderedScreenIds;
         }
 
@@ -198,6 +203,8 @@
             // Load items on the current page.
             bindWorkspaceItems(currentWorkspaceItems, mainExecutor);
             bindAppWidgets(currentAppWidgets, mainExecutor);
+            mExtraItems.forEach(item ->
+                    executeCallbacksTask(c -> c.bindExtraContainerItems(item), mainExecutor));
 
             // Locate available spots for prediction using currentWorkspaceItems
             IntArray gaps = getMissingHotseatRanks(currentWorkspaceItems, idp.numHotseatIcons);
@@ -207,6 +214,7 @@
             // happens later).
             // This ensures that the first screen is immediately visible (eg. during rotation)
             // In case of !validFirstPage, bind all pages one after other.
+
             final Executor deferredExecutor =
                     validFirstPage ? new ViewOnDrawExecutor() : mainExecutor;
 
diff --git a/src/com/android/launcher3/model/BgDataModel.java b/src/com/android/launcher3/model/BgDataModel.java
index fd8520d..140342f 100644
--- a/src/com/android/launcher3/model/BgDataModel.java
+++ b/src/com/android/launcher3/model/BgDataModel.java
@@ -424,6 +424,14 @@
         public FixedContainerItems clone() {
             return new FixedContainerItems(containerId, new ArrayList<>(items));
         }
+
+        public void setItems(List<ItemInfo> newItems) {
+            items.clear();
+            newItems.forEach(item -> {
+                item.container = containerId;
+                items.add(item);
+            });
+        }
     }
 
 
diff --git a/src/com/android/launcher3/model/LoaderCursor.java b/src/com/android/launcher3/model/LoaderCursor.java
index 165d1ea..a27ac23 100644
--- a/src/com/android/launcher3/model/LoaderCursor.java
+++ b/src/com/android/launcher3/model/LoaderCursor.java
@@ -65,7 +65,7 @@
 
     private static final String TAG = "LoaderCursor";
 
-    public final LongSparseArray<UserHandle> allUsers;
+    private final LongSparseArray<UserHandle> allUsers;
 
     private final Uri mContentUri;
     private final Context mContext;
diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java
index e89031e..1dd8c11 100644
--- a/src/com/android/launcher3/model/LoaderTask.java
+++ b/src/com/android/launcher3/model/LoaderTask.java
@@ -360,15 +360,12 @@
                 final int optionsIndex = c.getColumnIndexOrThrow(
                         LauncherSettings.Favorites.OPTIONS);
 
-                final LongSparseArray<UserHandle> allUsers = c.allUsers;
                 final LongSparseArray<Boolean> unlockedUsers = new LongSparseArray<>();
 
                 mUserManagerState.init(mUserCache, mUserManager);
 
                 for (UserHandle user : mUserCache.getUserProfiles()) {
                     long serialNo = mUserCache.getSerialNumberForUser(user);
-                    allUsers.put(serialNo, user);
-
                     boolean userUnlocked = mUserManager.isUserUnlocked(user);
 
                     // We can only query for shortcuts when the user is unlocked.
@@ -419,16 +416,6 @@
                             ComponentName cn = intent.getComponent();
                             targetPkg = cn == null ? intent.getPackage() : cn.getPackageName();
 
-                            if (allUsers.indexOfValue(c.user) < 0) {
-                                if (c.itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT) {
-                                    c.markDeleted("Legacy shortcuts are only allowed for current users");
-                                    continue;
-                                } else if (c.restoreFlag != 0) {
-                                    // Don't restore items for other profiles.
-                                    c.markDeleted("Restore from other profiles not supported");
-                                    continue;
-                                }
-                            }
                             if (TextUtils.isEmpty(targetPkg) &&
                                     c.itemType != LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT) {
                                 c.markDeleted("Only legacy shortcuts can have null package");
@@ -773,7 +760,7 @@
             }
 
             // Load delegate items
-            mModelDelegate.loadItems();
+            mModelDelegate.loadItems(mUserManagerState, shortcutKeyToPinnedShortcuts);
 
             // Break early if we've stopped loading
             if (mStopped) {
diff --git a/src/com/android/launcher3/model/ModelDelegate.java b/src/com/android/launcher3/model/ModelDelegate.java
index ce4eed5..53e8a86 100644
--- a/src/com/android/launcher3/model/ModelDelegate.java
+++ b/src/com/android/launcher3/model/ModelDelegate.java
@@ -18,13 +18,17 @@
 import static com.android.launcher3.util.PackageManagerHelper.hasShortcutsPermission;
 
 import android.content.Context;
+import android.content.pm.ShortcutInfo;
 
 import androidx.annotation.WorkerThread;
 
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.R;
+import com.android.launcher3.shortcuts.ShortcutKey;
 import com.android.launcher3.util.ResourceBasedOverride;
 
+import java.util.Map;
+
 /**
  * Class to extend LauncherModel functionality to provide extra data
  */
@@ -65,11 +69,12 @@
      * Load delegate items if any in the data model
      */
     @WorkerThread
-    public void loadItems() { }
+    public void loadItems(UserManagerState ums, Map<ShortcutKey, ShortcutInfo> pinnedShortcuts) { }
 
     /**
      * Called when the delegate is no loner needed
      */
     @WorkerThread
     public void destroy() { }
+
 }
diff --git a/src/com/android/launcher3/model/data/ItemInfo.java b/src/com/android/launcher3/model/data/ItemInfo.java
index 59233cd..e03fd72 100644
--- a/src/com/android/launcher3/model/data/ItemInfo.java
+++ b/src/com/android/launcher3/model/data/ItemInfo.java
@@ -258,12 +258,6 @@
     }
 
     /**
-     * Can be overridden by inherited classes to fill in {@link LauncherAtom.ItemInfo}
-     */
-    public void setItemBuilder(LauncherAtom.ItemInfo.Builder builder) {
-    }
-
-    /**
      * Creates {@link LauncherAtom.ItemInfo} with important fields and parent container info.
      */
     public LauncherAtom.ItemInfo buildProto() {
diff --git a/src/com/android/launcher3/model/data/LauncherAppWidgetInfo.java b/src/com/android/launcher3/model/data/LauncherAppWidgetInfo.java
index b0d19a6..c04b7f0 100644
--- a/src/com/android/launcher3/model/data/LauncherAppWidgetInfo.java
+++ b/src/com/android/launcher3/model/data/LauncherAppWidgetInfo.java
@@ -26,7 +26,6 @@
 import com.android.launcher3.AppWidgetResizeFrame;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherSettings;
-import com.android.launcher3.logger.LauncherAtom;
 import com.android.launcher3.util.ContentWriter;
 
 /**
@@ -195,13 +194,4 @@
     public final boolean hasOptionFlag(int option) {
         return (options & option) != 0;
     }
-
-    @Override
-    public void setItemBuilder(LauncherAtom.ItemInfo.Builder builder) {
-        builder.setWidget(LauncherAtom.Widget.newBuilder()
-                .setSpanX(spanX)
-                .setSpanY(spanY)
-                .setComponentName(providerName.toString())
-                .setPackageName(providerName.getPackageName()));
-    }
 }
diff --git a/src/com/android/launcher3/util/IOUtils.java b/src/com/android/launcher3/util/IOUtils.java
index fcb96d7..1cec0ec 100644
--- a/src/com/android/launcher3/util/IOUtils.java
+++ b/src/com/android/launcher3/util/IOUtils.java
@@ -16,20 +16,19 @@
 
 package com.android.launcher3.util;
 
-import android.content.Context;
+import android.os.FileUtils;
 import android.util.Log;
 
+import com.android.launcher3.Utilities;
 import com.android.launcher3.config.FeatureFlags;
 
 import java.io.ByteArrayOutputStream;
 import java.io.Closeable;
 import java.io.File;
 import java.io.FileInputStream;
-import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
-import java.util.UUID;
 
 /**
  * Supports various IO utility functions
@@ -52,6 +51,9 @@
     }
 
     public static long copy(InputStream from, OutputStream to) throws IOException {
+        if (Utilities.ATLEAST_Q) {
+            return FileUtils.copy(from, to);
+        }
         byte[] buf = new byte[BUF_SIZE];
         long total = 0;
         int r;
@@ -62,25 +64,6 @@
         return total;
     }
 
-    /**
-     * Utility method to debug binary data
-     */
-    public static String createTempFile(Context context, byte[] data) {
-        if (!FeatureFlags.IS_STUDIO_BUILD) {
-            throw new IllegalStateException("Method only allowed in development mode");
-        }
-
-        String name = UUID.randomUUID().toString();
-        File file = new File(context.getCacheDir(), name);
-        try (FileOutputStream fo = new FileOutputStream(file)) {
-            fo.write(data);
-            fo.flush();
-        } catch (Exception e) {
-            throw new RuntimeException(e);
-        }
-        return file.getAbsolutePath();
-    }
-
     public static void closeSilently(Closeable c) {
         if (c != null) {
             try {
diff --git a/src/com/android/launcher3/util/PersistedItemArray.java b/src/com/android/launcher3/util/PersistedItemArray.java
new file mode 100644
index 0000000..ae20638
--- /dev/null
+++ b/src/com/android/launcher3/util/PersistedItemArray.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2020 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;
+import android.os.UserHandle;
+import android.util.AtomicFile;
+import android.util.Log;
+import android.util.Xml;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.WorkerThread;
+
+import com.android.launcher3.AutoInstallsLayout;
+import com.android.launcher3.LauncherSettings.Favorites;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.pm.UserCache;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.LongFunction;
+
+/**
+ * Utility class to read/write a list of {@link com.android.launcher3.model.data.ItemInfo} on disk.
+ * This class is not thread safe, the caller should ensure proper threading
+ */
+public class PersistedItemArray<T extends ItemInfo> {
+
+    private static final String TAG = "PersistedItemArray";
+
+    private static final String TAG_ROOT = "items";
+    private static final String TAG_ENTRY = "entry";
+
+    private final String mFileName;
+
+    public PersistedItemArray(String fileName) {
+        mFileName = fileName + ".xml";
+    }
+
+    /**
+     * Writes the provided list of items on the disk
+     */
+    @WorkerThread
+    public void write(Context context, List<T> items) {
+        AtomicFile file = new AtomicFile(context.getFileStreamPath(mFileName));
+
+        FileOutputStream fos;
+        try {
+            fos = file.startWrite();
+        } catch (IOException e) {
+            Log.e(TAG, "Unable to persist items in " + mFileName, e);
+            return;
+        }
+
+        UserCache userCache = UserCache.INSTANCE.get(context);
+
+        try {
+            XmlSerializer out = Xml.newSerializer();
+            out.setOutput(fos, StandardCharsets.UTF_8.name());
+            out.startDocument(null, true);
+            out.startTag(null, TAG_ROOT);
+            for (T item : items) {
+                Intent intent = item.getIntent();
+                if (intent == null) {
+                    continue;
+                }
+
+                out.startTag(null, TAG_ENTRY);
+                out.attribute(null, Favorites.ITEM_TYPE, Integer.toString(item.itemType));
+                out.attribute(null, Favorites.PROFILE_ID,
+                        Long.toString(userCache.getSerialNumberForUser(item.user)));
+                out.attribute(null, Favorites.INTENT, intent.toUri(0));
+                out.endTag(null, TAG_ENTRY);
+            }
+            out.endTag(null, TAG_ROOT);
+            out.endDocument();
+        } catch (IOException e) {
+            file.failWrite(fos);
+            Log.e(TAG, "Unable to persist items in " + mFileName, e);
+            return;
+        }
+
+        file.finishWrite(fos);
+    }
+
+    /**
+     * Reads the items from the disk
+     */
+    @WorkerThread
+    public List<T> read(Context context, ItemFactory<T> factory) {
+        return read(context, factory, UserCache.INSTANCE.get(context)::getUserForSerialNumber);
+    }
+
+    /**
+     * Reads the items from the disk
+     * @param userFn method to provide user handle for a given user serial
+     */
+    @WorkerThread
+    public List<T> read(Context context, ItemFactory<T> factory, LongFunction<UserHandle> userFn) {
+        List<T> result = new ArrayList<>();
+        AtomicFile file = new AtomicFile(context.getFileStreamPath(mFileName));
+
+        try (FileInputStream fis = file.openRead()) {
+            XmlPullParser parser = Xml.newPullParser();
+            parser.setInput(new InputStreamReader(fis, StandardCharsets.UTF_8));
+
+            AutoInstallsLayout.beginDocument(parser, TAG_ROOT);
+            final int depth = parser.getDepth();
+
+            int type;
+            while (((type = parser.next()) != XmlPullParser.END_TAG
+                    || parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
+                if (type != XmlPullParser.START_TAG || !TAG_ENTRY.equals(parser.getName())) {
+                    continue;
+                }
+                try {
+                    int itemType = Integer.parseInt(
+                            parser.getAttributeValue(null, Favorites.ITEM_TYPE));
+                    UserHandle user = userFn.apply(Long.parseLong(
+                            parser.getAttributeValue(null, Favorites.PROFILE_ID)));
+                    Intent intent = Intent.parseUri(
+                            parser.getAttributeValue(null, Favorites.INTENT), 0);
+
+                    if (user != null && intent != null) {
+                        T item = factory.createInfo(itemType, user, intent);
+                        if (item != null) {
+                            result.add(item);
+                        }
+                    }
+                } catch (Exception e) {
+                    // Ignore this entry
+                }
+            }
+        } catch (FileNotFoundException e) {
+            // Ignore
+        } catch (IOException | XmlPullParserException e) {
+            Log.e(TAG, "Unable to read items in " + mFileName, e);
+            return Collections.emptyList();
+        }
+        return result;
+    }
+
+    /**
+     * Interface to create an ItemInfo during parsing
+     */
+    public interface ItemFactory<T extends ItemInfo> {
+
+        /**
+         * Returns an item info or null in which case the entry is ignored
+         */
+        @Nullable
+        T createInfo(int itemType, UserHandle user, Intent intent);
+    }
+}