Adding support for multiple Model clients

Bug: 137568159
Change-Id: Ia4db800b19cc80c695fcb9ea28e07709dfd08c6a
diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java
index 63b0e1e..cf978b5 100644
--- a/src/com/android/launcher3/LauncherModel.java
+++ b/src/com/android/launcher3/LauncherModel.java
@@ -16,6 +16,12 @@
 
 package com.android.launcher3;
 
+import static com.android.launcher3.LauncherAppState.ACTION_FORCE_ROLOAD;
+import static com.android.launcher3.config.FeatureFlags.IS_DOGFOOD_BUILD;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
+import static com.android.launcher3.util.PackageManagerHelper.hasShortcutsPermission;
+
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.LauncherApps;
@@ -52,13 +58,12 @@
 import com.android.launcher3.shortcuts.ShortcutRequest;
 import com.android.launcher3.util.IntSparseArrayMap;
 import com.android.launcher3.util.ItemInfoMatcher;
+import com.android.launcher3.util.LooperExecutor;
 import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.util.Preconditions;
-import com.android.launcher3.util.Thunk;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
-import java.lang.ref.WeakReference;
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
@@ -67,12 +72,6 @@
 import java.util.concurrent.Executor;
 import java.util.function.Supplier;
 
-import static com.android.launcher3.LauncherAppState.ACTION_FORCE_ROLOAD;
-import static com.android.launcher3.config.FeatureFlags.IS_DOGFOOD_BUILD;
-import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
-import static com.android.launcher3.util.PackageManagerHelper.hasShortcutsPermission;
-
 /**
  * Maintains in-memory state of the Launcher. It is expected that there should be only one
  * LauncherModel object held in a static. Also provide APIs for updating the database state
@@ -83,11 +82,12 @@
 
     static final String TAG = "Launcher.Model";
 
-    @Thunk final LauncherAppState mApp;
-    @Thunk final Object mLock = new Object();
-    @Thunk
-    LoaderTask mLoaderTask;
-    @Thunk boolean mIsLoaderTaskRunning;
+    private final LauncherAppState mApp;
+    private final Object mLock = new Object();
+    private final LooperExecutor mMainExecutor = MAIN_EXECUTOR;
+
+    private LoaderTask mLoaderTask;
+    private boolean mIsLoaderTaskRunning;
 
     // Indicates whether the current model data is valid or not.
     // We start off with everything not loaded. After that, we assume that
@@ -100,7 +100,7 @@
         }
     }
 
-    @Thunk WeakReference<Callbacks> mCallbacks;
+    private final ArrayList<Callbacks> mCallbacksList = new ArrayList<>(1);
 
     // < only access in worker thread >
     private final AllAppsList mBgAllAppsList;
@@ -141,9 +141,8 @@
      * Adds the provided items to the workspace.
      */
     public void addAndBindAddedWorkspaceItems(List<Pair<ItemInfo, Object>> itemList) {
-        Callbacks callbacks = getCallback();
-        if (callbacks != null) {
-            callbacks.preAddApps();
+        for (Callbacks cb : getCallbacks()) {
+            cb.preAddApps();
         }
         enqueueModelUpdateTask(new AddWorkspaceItemsTask(itemList));
     }
@@ -153,16 +152,6 @@
                 hasVerticalHotseat, verifyChanges);
     }
 
-    /**
-     * Set this as the current Launcher activity object for the loader.
-     */
-    public void initialize(Callbacks callbacks) {
-        synchronized (mLock) {
-            Preconditions.assertUIThread();
-            mCallbacks = new WeakReference<>(callbacks);
-        }
-    }
-
     @Override
     public void onPackageChanged(String packageName, UserHandle user) {
         int op = PackageUpdatedTask.OP_UPDATE;
@@ -262,21 +251,19 @@
                 }
             }
         } else if (IS_DOGFOOD_BUILD && ACTION_FORCE_ROLOAD.equals(action)) {
-            Launcher l = (Launcher) getCallback();
-            l.reload();
+            for (Callbacks cb : getCallbacks()) {
+                if (cb instanceof Launcher) {
+                    ((Launcher) cb).recreate();
+                }
+            }
         }
     }
 
-    public void forceReload() {
-        forceReload(-1);
-    }
-
     /**
      * Reloads the workspace items from the DB and re-binds the workspace. This should generally
      * not be called as DB updates are automatically followed by UI update
-     * @param synchronousBindPage The page to bind first. Can pass -1 to use the current page.
      */
-    public void forceReload(int synchronousBindPage) {
+    public void forceReload() {
         synchronized (mLock) {
             // Stop any existing loaders first, so they don't set mModelLoaded to true later
             stopLoader();
@@ -285,37 +272,77 @@
 
         // Start the loader if launcher is already running, otherwise the loader will run,
         // the next time launcher starts
-        Callbacks callbacks = getCallback();
-        if (callbacks != null) {
-            if (synchronousBindPage < 0) {
-                synchronousBindPage = callbacks.getCurrentWorkspaceScreen();
-            }
-            startLoader(synchronousBindPage);
+        if (hasCallbacks()) {
+            startLoader();
         }
     }
 
-    public boolean isCurrentCallbacks(Callbacks callbacks) {
-        return (mCallbacks != null && mCallbacks.get() == callbacks);
+    /**
+     * Rebinds all existing callbacks with already loaded model
+     */
+    public void rebindCallbacks() {
+        if (hasCallbacks()) {
+            startLoader();
+        }
+    }
+
+    /**
+     * Removes an existing callback
+     */
+    public void removeCallbacks(Callbacks callbacks) {
+        synchronized (mCallbacksList) {
+            Preconditions.assertUIThread();
+            if (mCallbacksList.remove(callbacks)) {
+                if (stopLoader()) {
+                    // Rebind existing callbacks
+                    startLoader();
+                }
+            }
+        }
+    }
+
+    /**
+     * Adds a callbacks to receive model updates
+     * @return true if workspace load was performed synchronously
+     */
+    public boolean addCallbacksAndLoad(Callbacks callbacks) {
+        synchronized (mLock) {
+            addCallbacks(callbacks);
+            return startLoader();
+
+        }
+    }
+
+    /**
+     * Adds a callbacks to receive model updates
+     */
+    public void addCallbacks(Callbacks callbacks) {
+        Preconditions.assertUIThread();
+        synchronized (mCallbacksList) {
+            mCallbacksList.add(callbacks);
+        }
     }
 
     /**
      * Starts the loader. Tries to bind {@params synchronousBindPage} synchronously if possible.
      * @return true if the page could be bound synchronously.
      */
-    public boolean startLoader(int synchronousBindPage) {
+    public boolean startLoader() {
         // Enable queue before starting loader. It will get disabled in Launcher#finishBindingItems
         InstallShortcutReceiver.enableInstallQueue(InstallShortcutReceiver.FLAG_LOADER_RUNNING);
         synchronized (mLock) {
             // Don't bother to start the thread if we know it's not going to do anything
-            if (mCallbacks != null && mCallbacks.get() != null) {
-                final Callbacks oldCallbacks = mCallbacks.get();
+            final Callbacks[] callbacksList = getCallbacks();
+            if (callbacksList.length > 0) {
                 // Clear any pending bind-runnables from the synchronized load process.
-                MAIN_EXECUTOR.execute(oldCallbacks::clearPendingBinds);
+                for (Callbacks cb : callbacksList) {
+                    mMainExecutor.execute(cb::clearPendingBinds);
+                }
 
                 // If there is already one running, tell it to stop.
                 stopLoader();
-                LoaderResults loaderResults = new LoaderResults(mApp, mBgDataModel,
-                        mBgAllAppsList, synchronousBindPage, mCallbacks);
+                LoaderResults loaderResults = new LoaderResults(
+                        mApp, mBgDataModel, mBgAllAppsList, callbacksList, mMainExecutor);
                 if (mModelLoaded && !mIsLoaderTaskRunning) {
                     // Divide the set of loaded items into those that we are binding synchronously,
                     // and everything else that is to be bound normally (asynchronously).
@@ -336,14 +363,17 @@
 
     /**
      * If there is already a loader task running, tell it to stop.
+     * @return true if an existing loader was stopped.
      */
-    public void stopLoader() {
+    public boolean stopLoader() {
         synchronized (mLock) {
             LoaderTask oldTask = mLoaderTask;
             mLoaderTask = null;
             if (oldTask != null) {
                 oldTask.stopLocked();
+                return true;
             }
+            return false;
         }
     }
 
@@ -498,7 +528,7 @@
     }
 
     public void enqueueModelUpdateTask(ModelUpdateTask task) {
-        task.init(mApp, this, mBgDataModel, mBgAllAppsList, MAIN_EXECUTOR);
+        task.init(mApp, this, mBgDataModel, mBgAllAppsList, mMainExecutor);
         MODEL_EXECUTOR.execute(task);
     }
 
@@ -572,7 +602,21 @@
         mBgDataModel.dump(prefix, fd, writer, args);
     }
 
-    public Callbacks getCallback() {
-        return mCallbacks != null ? mCallbacks.get() : null;
+    /**
+     * Returns true if there are any callbacks attached to the model
+     */
+    public boolean hasCallbacks() {
+        synchronized (mCallbacksList) {
+            return !mCallbacksList.isEmpty();
+        }
+    }
+
+    /**
+     * Returns an array of currently attached callbacks
+     */
+    public Callbacks[] getCallbacks() {
+        synchronized (mCallbacksList) {
+            return mCallbacksList.toArray(new Callbacks[mCallbacksList.size()]);
+        }
     }
 }