Optimize updating All Apps Predictions.

* Prefetch existing predictions in onResume.
* Only update predictions if something has been changed.
* Swap predictions in place (if possible) -- this avoids
  having to relayout all of All Apps when just the predictions
  change.

Bug: 64126588
Bug: 63711551
Change-Id: Iffa0d9d7d6f99f606dd68b7df5893d6eba1574f5
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 7b7177e..7e9c5a3 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -958,6 +958,9 @@
         } else if (mOnResumeState == State.WIDGETS) {
             showWidgetsView(false, false);
         }
+        if (mOnResumeState != State.APPS) {
+            tryAndUpdatePredictedApps();
+        }
         mOnResumeState = State.NONE;
 
         mPaused = false;
diff --git a/src/com/android/launcher3/allapps/AllAppsContainerView.java b/src/com/android/launcher3/allapps/AllAppsContainerView.java
index 0083d47..ede3bea 100644
--- a/src/com/android/launcher3/allapps/AllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsContainerView.java
@@ -224,6 +224,8 @@
         mAppsRecyclerView.setLayoutManager(mLayoutManager);
         mAppsRecyclerView.setAdapter(mAdapter);
         mAppsRecyclerView.setHasFixedSize(true);
+        // Removes the animation that can occur when updating the predicted apps in place.
+        mAppsRecyclerView.getItemAnimator().setChangeDuration(0);
         if (FeatureFlags.LAUNCHER3_PHYSICS) {
             mAppsRecyclerView.setSpringAnimationHandler(mSpringAnimationHandler);
         }
diff --git a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
index 608e898..ee2756f 100644
--- a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
+++ b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
@@ -298,14 +298,74 @@
         updateAdapterItems();
     }
 
+    private List<AppInfo> processPredictedAppComponents(List<ComponentKey> components) {
+        if (mComponentToAppMap.isEmpty()) {
+            // Apps have not been bound yet.
+            return Collections.emptyList();
+        }
+
+        List<AppInfo> predictedApps = new ArrayList<>();
+        for (ComponentKey ck : components) {
+            AppInfo info = mComponentToAppMap.get(ck);
+            if (info != null) {
+                predictedApps.add(info);
+            } else {
+                if (FeatureFlags.IS_DOGFOOD_BUILD) {
+                    Log.e(TAG, "Predicted app not found: " + ck);
+                }
+            }
+            // Stop at the number of predicted apps
+            if (predictedApps.size() == mNumPredictedAppsPerRow) {
+                break;
+            }
+        }
+        return predictedApps;
+    }
+
     /**
-     * Sets the current set of predicted apps.  Since this can be called before we get the full set
-     * of applications, we should merge the results only in onAppsUpdated() which is idempotent.
+     * Sets the current set of predicted apps.
+     *
+     * This can be called before we get the full set of applications, we should merge the results
+     * only in onAppsUpdated() which is idempotent.
+     *
+     * If the number of predicted apps is the same as the previous list of predicted apps,
+     * we can optimize by swapping them in place.
      */
     public void setPredictedApps(List<ComponentKey> apps) {
         mPredictedAppComponents.clear();
         mPredictedAppComponents.addAll(apps);
-        onAppsUpdated();
+
+        List<AppInfo> newPredictedApps = processPredictedAppComponents(apps);
+        // We only need to do work if any of the visible predicted apps have changed.
+        if (!newPredictedApps.equals(mPredictedApps)) {
+            if (newPredictedApps.size() == mPredictedApps.size()) {
+                swapInNewPredictedApps(newPredictedApps);
+            } else {
+                // We need to update the appIndex of all the items.
+                onAppsUpdated();
+            }
+        }
+    }
+
+    /**
+     * Swaps out the old predicted apps with the new predicted apps, in place. This optimization
+     * allows us to skip an entire relayout that would otherwise be called by notifyDataSetChanged.
+     *
+     * Note: This should only be called if the # of predicted apps is the same.
+     *       This method assumes that predicted apps are the first items in the adapter.
+     */
+    private void swapInNewPredictedApps(List<AppInfo> apps) {
+        mPredictedApps.clear();
+        mPredictedApps.addAll(apps);
+
+        int size = apps.size();
+        for (int i = 0; i < size; ++i) {
+            AppInfo info = apps.get(i);
+            AdapterItem appItem = AdapterItem.asPredictedApp(i, "", info, i);
+            mAdapterItems.set(i, appItem);
+            mFilteredApps.set(i, info);
+            mAdapter.notifyItemChanged(i);
+        }
     }
 
     /**
@@ -432,20 +492,7 @@
         // Process the predicted app components
         mPredictedApps.clear();
         if (mPredictedAppComponents != null && !mPredictedAppComponents.isEmpty() && !hasFilter()) {
-            for (ComponentKey ck : mPredictedAppComponents) {
-                AppInfo info = mComponentToAppMap.get(ck);
-                if (info != null) {
-                    mPredictedApps.add(info);
-                } else {
-                    if (FeatureFlags.IS_DOGFOOD_BUILD) {
-                        Log.e(TAG, "Predicted app not found: " + ck);
-                    }
-                }
-                // Stop at the number of predicted apps
-                if (mPredictedApps.size() == mNumPredictedAppsPerRow) {
-                    break;
-                }
-            }
+            mPredictedApps.addAll(processPredictedAppComponents(mPredictedAppComponents));
 
             if (!mPredictedApps.isEmpty()) {
                 // Add a section for the predictions