Improve Hybird hotseat cache support

Loads list of cached apps and shows predicted app icons with the rest of workspace items.

-> Extracted prediction caching logic into a model layer

Bug: 155029837
Test: Manual

Change-Id: I6981594a910f5fe4e8e8cf8fe39db0cb856e7acd
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index c75bd95..1d9c0c3 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -2225,6 +2225,9 @@
         workspace.requestLayout();
     }
 
+    @Override
+    public void bindPredictedItems(List<AppInfo> appInfos, IntArray ranks) { }
+
     /**
      * Add the views for a widget to the workspace.
      */
diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java
index 2f38037..14e604d 100644
--- a/src/com/android/launcher3/LauncherAppState.java
+++ b/src/com/android/launcher3/LauncherAppState.java
@@ -33,6 +33,7 @@
 import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.icons.IconProvider;
 import com.android.launcher3.icons.LauncherIcons;
+import com.android.launcher3.model.PredictionModel;
 import com.android.launcher3.notification.NotificationListener;
 import com.android.launcher3.pm.InstallSessionHelper;
 import com.android.launcher3.pm.InstallSessionTracker;
@@ -57,6 +58,7 @@
     private final IconCache mIconCache;
     private final WidgetPreviewLoader mWidgetCache;
     private final InvariantDeviceProfile mInvariantDeviceProfile;
+    private final PredictionModel mPredictionModel;
 
     private SecureSettingsObserver mNotificationDotsObserver;
     private InstallSessionTracker mInstallSessionTracker;
@@ -127,6 +129,7 @@
         mIconCache = new IconCache(mContext, mInvariantDeviceProfile, iconCacheFileName);
         mWidgetCache = new WidgetPreviewLoader(mContext, mIconCache);
         mModel = new LauncherModel(this, mIconCache, AppFilter.newInstance(mContext));
+        mPredictionModel = new PredictionModel(mContext);
     }
 
     protected void onNotificationSettingsChanged(boolean areNotificationDotsEnabled) {
@@ -182,6 +185,10 @@
         return mModel;
     }
 
+    public PredictionModel getPredictionModel() {
+        return mPredictionModel;
+    }
+
     public WidgetPreviewLoader getWidgetCache() {
         return mWidgetCache;
     }
diff --git a/src/com/android/launcher3/model/BaseLoaderResults.java b/src/com/android/launcher3/model/BaseLoaderResults.java
index c98be56..1465100 100644
--- a/src/com/android/launcher3/model/BaseLoaderResults.java
+++ b/src/com/android/launcher3/model/BaseLoaderResults.java
@@ -17,6 +17,7 @@
 package com.android.launcher3.model;
 
 import static com.android.launcher3.model.ModelUtils.filterCurrentWorkspaceItems;
+import static com.android.launcher3.model.ModelUtils.getMissingHotseatRanks;
 import static com.android.launcher3.model.ModelUtils.sortWorkspaceItemsSpatially;
 
 import android.util.Log;
@@ -196,6 +197,10 @@
             // Load items on the current page.
             bindWorkspaceItems(currentWorkspaceItems, mainExecutor);
             bindAppWidgets(currentAppWidgets, mainExecutor);
+
+            // Locate available spots for prediction using currentWorkspaceItems
+            IntArray gaps = getMissingHotseatRanks(currentWorkspaceItems, idp.numHotseatIcons);
+            bindPredictedItems(gaps, mainExecutor);
             // In case of validFirstPage, only bind the first screen, and defer binding the
             // remaining screens after first onDraw (and an optional the fade animation whichever
             // happens later).
@@ -247,6 +252,11 @@
             }
         }
 
+        private void bindPredictedItems(IntArray ranks, final Executor executor) {
+            executeCallbacksTask(
+                    c -> c.bindPredictedItems(mBgDataModel.cachedPredictedItems, ranks), executor);
+        }
+
         protected void executeCallbacksTask(CallbackTask task, Executor executor) {
             executor.execute(() -> {
                 if (mMyBindingId != mBgDataModel.lastBindId) {
diff --git a/src/com/android/launcher3/model/BgDataModel.java b/src/com/android/launcher3/model/BgDataModel.java
index f79a9d1..2522a49 100644
--- a/src/com/android/launcher3/model/BgDataModel.java
+++ b/src/com/android/launcher3/model/BgDataModel.java
@@ -93,6 +93,11 @@
     public final Map<ShortcutKey, MutableInt> pinnedShortcutCounts = new HashMap<>();
 
     /**
+     * List of all cached predicted items visible on home screen
+     */
+    public final ArrayList<AppInfo> cachedPredictedItems = new ArrayList<>();
+
+    /**
      * True if the launcher has permission to access deep shortcuts.
      */
     public boolean hasShortcutHostPermission;
@@ -366,5 +371,10 @@
         void bindDeepShortcutMap(HashMap<ComponentKey, Integer> deepShortcutMap);
 
         void bindAllApplications(AppInfo[] apps);
+
+        /**
+         * Binds predicted appInfos at at available prediction slots.
+         */
+        void bindPredictedItems(List<AppInfo> appInfos, IntArray ranks);
     }
 }
diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java
index 4c02837..90aaf44 100644
--- a/src/com/android/launcher3/model/LoaderTask.java
+++ b/src/com/android/launcher3/model/LoaderTask.java
@@ -179,6 +179,7 @@
         try (LauncherModel.LoaderTransaction transaction = mApp.getModel().beginLoader(this)) {
             List<ShortcutInfo> allShortcuts = new ArrayList<>();
             loadWorkspace(allShortcuts);
+            loadCachedPredictions();
             logger.addSplit("loadWorkspace");
 
             verifyNotStopped();
@@ -849,6 +850,23 @@
         }
     }
 
+    private List<AppInfo> loadCachedPredictions() {
+        List<ComponentKey> componentKeys = mApp.getPredictionModel().getPredictionComponentKeys();
+        List<AppInfo> results = new ArrayList<>();
+        if (componentKeys == null) return results;
+        List<LauncherActivityInfo> l;
+        mBgDataModel.cachedPredictedItems.clear();
+        for (ComponentKey key : componentKeys) {
+            l = mLauncherApps.getActivityList(key.componentName.getPackageName(), key.user);
+            if (l.size() == 0) continue;
+            boolean quietMode = mUserManager.isQuietModeEnabled(key.user);
+            AppInfo info = new AppInfo(l.get(0), key.user, quietMode);
+            mBgDataModel.cachedPredictedItems.add(info);
+            mIconCache.getTitleAndIcon(info, false);
+        }
+        return results;
+    }
+
     private List<LauncherActivityInfo> loadAllApps() {
         final List<UserHandle> profiles = mUserCache.getUserProfiles();
         List<LauncherActivityInfo> allActivityList = new ArrayList<>();
@@ -880,6 +898,14 @@
                         PackageInstallInfo.fromInstallingState(info));
             }
         }
+        for (AppInfo item : mBgDataModel.cachedPredictedItems) {
+            List<LauncherActivityInfo> l = mLauncherApps.getActivityList(
+                    item.componentName.getPackageName(), item.user);
+            for (LauncherActivityInfo info : l) {
+                boolean quietMode = mUserManager.isQuietModeEnabled(item.user);
+                mBgAllAppsList.add(new AppInfo(info, item.user, quietMode), info);
+            }
+        }
 
         mBgAllAppsList.getAndResetChangeFlag();
         return allActivityList;
diff --git a/src/com/android/launcher3/model/ModelUtils.java b/src/com/android/launcher3/model/ModelUtils.java
index ef7e828..4efeba5 100644
--- a/src/com/android/launcher3/model/ModelUtils.java
+++ b/src/com/android/launcher3/model/ModelUtils.java
@@ -19,11 +19,14 @@
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.IntSet;
 
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Iterator;
+import java.util.List;
+import java.util.stream.IntStream;
 
 /**
  * Utils class for {@link com.android.launcher3.LauncherModel}.
@@ -109,4 +112,17 @@
             }
         });
     }
+
+    /**
+     * Iterates though current workspace items and returns available hotseat ranks for prediction.
+     */
+    public static IntArray getMissingHotseatRanks(List<ItemInfo> items, int len) {
+        IntSet seen = new IntSet();
+        items.stream().filter(
+                info -> info.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT)
+                .forEach(i -> seen.add(i.screenId));
+        IntArray result = new IntArray(len);
+        IntStream.range(0, len).filter(i -> !seen.contains(i)).forEach(result::add);
+        return result;
+    }
 }
diff --git a/src/com/android/launcher3/model/PredictionModel.java b/src/com/android/launcher3/model/PredictionModel.java
new file mode 100644
index 0000000..6aa41eb
--- /dev/null
+++ b/src/com/android/launcher3/model/PredictionModel.java
@@ -0,0 +1,93 @@
+/*
+ * 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.model;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.UserHandle;
+
+import com.android.launcher3.Utilities;
+import com.android.launcher3.model.data.AppInfo;
+import com.android.launcher3.util.ComponentKey;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Model helper for app predictions in workspace
+ */
+public class PredictionModel {
+    private static final String CACHED_ITEMS_KEY = "predicted_item_keys";
+    private static final int MAX_CACHE_ITEMS = 5;
+
+    private final Context mContext;
+    private final SharedPreferences mDevicePrefs;
+    private ArrayList<ComponentKey> mCachedComponentKeys;
+
+    public PredictionModel(Context context) {
+        mContext = context;
+        mDevicePrefs = Utilities.getDevicePrefs(mContext);
+    }
+
+    /**
+     * Formats and stores a list of component key in device preferences.
+     */
+    public void cachePredictionComponentKeys(List<ComponentKey> componentKeys) {
+        StringBuilder builder = new StringBuilder();
+        int count = Math.min(componentKeys.size(), MAX_CACHE_ITEMS);
+        for (int i = 0; i < count; i++) {
+            builder.append(componentKeys.get(i));
+            builder.append("\n");
+        }
+        mDevicePrefs.edit().putString(CACHED_ITEMS_KEY, builder.toString()).apply();
+        mCachedComponentKeys = null;
+    }
+
+    /**
+     * parses and returns ComponentKeys saved by
+     * {@link PredictionModel#cachePredictionComponentKeys(List)}
+     */
+    public List<ComponentKey> getPredictionComponentKeys() {
+        if (mCachedComponentKeys == null) {
+            mCachedComponentKeys = new ArrayList<>();
+
+            String cachedBlob = mDevicePrefs.getString(CACHED_ITEMS_KEY, "");
+            for (String line : cachedBlob.split("\n")) {
+                ComponentKey key = ComponentKey.fromString(line);
+                if (key != null) {
+                    mCachedComponentKeys.add(key);
+                }
+            }
+        }
+        return mCachedComponentKeys;
+    }
+
+    /**
+     * Remove uninstalled applications from model
+     */
+    public void removePackage(String pkgName, UserHandle user, ArrayList<AppInfo> ids) {
+        for (int i = ids.size() - 1; i >= 0; i--) {
+            AppInfo info = ids.get(i);
+            if (info.user.equals(user) && pkgName.equals(info.componentName.getPackageName())) {
+                ids.remove(i);
+            }
+        }
+        cachePredictionComponentKeys(getPredictionComponentKeys().stream()
+                .filter(cn -> !(cn.user.equals(user) && cn.componentName.getPackageName().equals(
+                        pkgName))).collect(Collectors.toList()));
+    }
+}
diff --git a/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java b/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java
index dd6fc49..4a15af1 100644
--- a/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java
+++ b/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java
@@ -196,6 +196,9 @@
     public void bindItems(List<ItemInfo> shortcuts, boolean forceAnimateIcons) { }
 
     @Override
+    public void bindPredictedItems(List<AppInfo> appInfos, IntArray ranks) { }
+
+    @Override
     public void bindScreens(IntArray orderedScreenIds) { }
 
     @Override