Refactoring deferred bind logic

> Using ViewTreeObserver to listen for onDraw instead of overriding onDraw in workspace
> Loader passes the list of deferrerd runnables to launcher

Change-Id: Ie4877f746c96e9497396de8089f00f70bf867e17
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index eea01d8..17b72b7 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -110,6 +110,7 @@
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.LongArrayMap;
 import com.android.launcher3.util.Thunk;
+import com.android.launcher3.util.ViewOnDrawExecutor;
 import com.android.launcher3.widget.PendingAddWidgetInfo;
 import com.android.launcher3.widget.WidgetHostViewLoader;
 import com.android.launcher3.widget.WidgetsContainerView;
@@ -273,6 +274,7 @@
 
     private ArrayList<Runnable> mBindOnResumeCallbacks = new ArrayList<Runnable>();
     private ArrayList<Runnable> mOnResumeCallbacks = new ArrayList<Runnable>();
+    private ViewOnDrawExecutor mPendingExecutor;
 
     private LauncherModel mModel;
     private IconCache mIconCache;
@@ -948,12 +950,6 @@
         mPaused = false;
         if (mRestoring || mOnResumeNeedsLoad) {
             setWorkspaceLoading(true);
-
-            // If we're starting binding all over again, clear any bind calls we'd postponed in
-            // the past (see waitUntilResume) -- we don't need them since we're starting binding
-            // from scratch again
-            mBindOnResumeCallbacks.clear();
-
             mModel.startLoader(PagedView.INVALID_RESTORE_PAGE);
             mRestoring = false;
             mOnResumeNeedsLoad = false;
@@ -3612,6 +3608,19 @@
     }
 
     /**
+     * Clear any pending bind callbacks. This is called when is loader is planning to
+     * perform a full rebind from scratch.
+     */
+    @Override
+    public void clearPendingBinds() {
+        mBindOnResumeCallbacks.clear();
+        if (mPendingExecutor != null) {
+            mPendingExecutor.markCompleted();
+            mPendingExecutor = null;
+        }
+    }
+
+    /**
      * Refreshes the shortcuts shown on the workspace.
      *
      * Implementation of the method from LauncherModel.Callbacks.
@@ -3619,11 +3628,6 @@
     public void startBinding() {
         setWorkspaceLoading(true);
 
-        // If we're starting binding all over again, clear any bind calls we'd postponed in
-        // the past (see waitUntilResume) -- we don't need them since we're starting binding
-        // from scratch again
-        mBindOnResumeCallbacks.clear();
-
         // Clear the workspace because it's going to be rebound
         mWorkspace.clearDropTargets();
         mWorkspace.removeAllWorkspaceScreens();
@@ -3988,6 +3992,21 @@
         mSynchronouslyBoundPages.add(page);
     }
 
+    @Override
+    public void executeOnNextDraw(ViewOnDrawExecutor executor) {
+        if (mPendingExecutor != null) {
+            mPendingExecutor.markCompleted();
+        }
+        mPendingExecutor = executor;
+        executor.attachTo(this);
+    }
+
+    public void clearPendingExecutor(ViewOnDrawExecutor executor) {
+        if (mPendingExecutor == executor) {
+            mPendingExecutor = null;
+        }
+    }
+
     /**
      * Callback saying that there aren't any more items to bind.
      *
diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java
index e8c5327..3007269 100644
--- a/src/com/android/launcher3/LauncherModel.java
+++ b/src/com/android/launcher3/LauncherModel.java
@@ -63,6 +63,7 @@
 import com.android.launcher3.util.LongArrayMap;
 import com.android.launcher3.util.ManagedProfileHeuristic;
 import com.android.launcher3.util.Thunk;
+import com.android.launcher3.util.ViewOnDrawExecutor;
 
 import java.lang.ref.WeakReference;
 import java.net.URISyntaxException;
@@ -78,6 +79,7 @@
 import java.util.List;
 import java.util.Map.Entry;
 import java.util.Set;
+import java.util.concurrent.Executor;
 
 /**
  * Maintains in-memory state of the Launcher. It is expected that there should be only one
@@ -123,12 +125,6 @@
     @Thunk boolean mWorkspaceLoaded;
     @Thunk boolean mAllAppsLoaded;
 
-    // When we are loading pages synchronously, we can't just post the binding of items on the side
-    // pages as this delays the rotation process.  Instead, we wait for a callback from the first
-    // draw (in Workspace) to initiate the binding of the remaining side pages.  Any time we start
-    // a normal load, we also clear this set of Runnables.
-    static final ArrayList<Runnable> mDeferredBindRunnables = new ArrayList<Runnable>();
-
     /**
      * Set of runnables to be called on the background thread after the workspace binding
      * is complete.
@@ -184,6 +180,7 @@
     public interface Callbacks {
         public boolean setLoadOnResume();
         public int getCurrentWorkspaceScreen();
+        public void clearPendingBinds();
         public void startBinding();
         public void bindItems(ArrayList<ItemInfo> shortcuts, int start, int end,
                               boolean forceAnimateIcons);
@@ -208,6 +205,7 @@
         public void bindSearchProviderChanged();
         public boolean isAllAppsButtonRank(int rank);
         public void onPageBoundSynchronously(int page);
+        public void executeOnNextDraw(ViewOnDrawExecutor executor);
         public void dumpLogsToLocalData();
     }
 
@@ -586,11 +584,6 @@
                     "main thread");
         }
 
-        // Clear any deferred bind runnables
-        synchronized (mDeferredBindRunnables) {
-            mDeferredBindRunnables.clear();
-        }
-
         // Remove any queued UI runnables
         mHandler.cancelAll();
         // Unbind all the workspace items
@@ -1338,14 +1331,16 @@
         // Enable queue before starting loader. It will get disabled in Launcher#finishBindingItems
         InstallShortcutReceiver.enableInstallQueue();
         synchronized (mLock) {
-            // Clear any deferred bind-runnables from the synchronized load process
-            // We must do this before any loading/binding is scheduled below.
-            synchronized (mDeferredBindRunnables) {
-                mDeferredBindRunnables.clear();
-            }
-
             // 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();
+                // Clear any pending bind-runnables from the synchronized load process.
+                runOnMainThread(new Runnable() {
+                    public void run() {
+                        oldCallbacks.clearPendingBinds();
+                    }
+                });
+
                 // If there is already one running, tell it to stop.
                 stopLoaderLocked();
                 mLoaderTask = new LoaderTask(mApp.getContext(), loadFlags);
@@ -1360,21 +1355,6 @@
         }
     }
 
-    void bindRemainingSynchronousPages() {
-        // Post the remaining side pages to be loaded
-        if (!mDeferredBindRunnables.isEmpty()) {
-            Runnable[] deferredBindRunnables = null;
-            synchronized (mDeferredBindRunnables) {
-                deferredBindRunnables = mDeferredBindRunnables.toArray(
-                        new Runnable[mDeferredBindRunnables.size()]);
-                mDeferredBindRunnables.clear();
-            }
-            for (final Runnable r : deferredBindRunnables) {
-                mHandler.post(r);
-            }
-        }
-    }
-
     public void stopLoader() {
         synchronized (mLock) {
             if (mLoaderTask != null) {
@@ -2500,9 +2480,7 @@
                 final ArrayList<ItemInfo> workspaceItems,
                 final ArrayList<LauncherAppWidgetInfo> appWidgets,
                 final LongArrayMap<FolderInfo> folders,
-                ArrayList<Runnable> deferredBindRunnables) {
-
-            final boolean postOnMainThread = (deferredBindRunnables != null);
+                final Executor executor) {
 
             // Bind the workspace items
             int N = workspaceItems.size();
@@ -2519,13 +2497,7 @@
                         }
                     }
                 };
-                if (postOnMainThread) {
-                    synchronized (deferredBindRunnables) {
-                        deferredBindRunnables.add(r);
-                    }
-                } else {
-                    runOnMainThread(r);
-                }
+                executor.execute(r);
             }
 
             // Bind the folders
@@ -2538,13 +2510,7 @@
                         }
                     }
                 };
-                if (postOnMainThread) {
-                    synchronized (deferredBindRunnables) {
-                        deferredBindRunnables.add(r);
-                    }
-                } else {
-                    runOnMainThread(r);
-                }
+                executor.execute(r);
             }
 
             // Bind the widgets, one at a time
@@ -2559,11 +2525,7 @@
                         }
                     }
                 };
-                if (postOnMainThread) {
-                    deferredBindRunnables.add(r);
-                } else {
-                    runOnMainThread(r);
-                }
+                executor.execute(r);
             }
         }
 
@@ -2641,6 +2603,7 @@
                 public void run() {
                     Callbacks callbacks = tryGetCallbacks(oldCallbacks);
                     if (callbacks != null) {
+                        callbacks.clearPendingBinds();
                         callbacks.startBinding();
                     }
                 }
@@ -2649,28 +2612,20 @@
 
             bindWorkspaceScreens(oldCallbacks, orderedScreenIds);
 
-            // Load items on the current page
+            Executor mainExecutor = new DeferredMainThreadExecutor();
+            // Load items on the current page.
             bindWorkspaceItems(oldCallbacks, currentWorkspaceItems, currentAppWidgets,
-                    currentFolders, null);
-            if (isLoadingSynchronously) {
-                r = new Runnable() {
-                    public void run() {
-                        Callbacks callbacks = tryGetCallbacks(oldCallbacks);
-                        if (callbacks != null && currentScreen != PagedView.INVALID_RESTORE_PAGE) {
-                            callbacks.onPageBoundSynchronously(currentScreen);
-                        }
-                    }
-                };
-                runOnMainThread(r);
-            }
+                    currentFolders, mainExecutor);
 
-            // Load all the remaining pages (if we are loading synchronously, we want to defer this
-            // work until after the first render)
-            synchronized (mDeferredBindRunnables) {
-                mDeferredBindRunnables.clear();
-            }
-            bindWorkspaceItems(oldCallbacks, otherWorkspaceItems, otherAppWidgets, otherFolders,
-                    (isLoadingSynchronously ? mDeferredBindRunnables : null));
+            // In case of isLoadingSynchronously, only bind the first screen, and defer binding the
+            // remaining screens after first onDraw is called. This ensures that the first screen
+            // is immediately visible (eg. during rotation)
+            // In case of !isLoadingSynchronously, bind all pages one after other.
+            final Executor deferredExecutor = isLoadingSynchronously ?
+                    new ViewOnDrawExecutor(mHandler) : mainExecutor;
+
+            bindWorkspaceItems(oldCallbacks, otherWorkspaceItems, otherAppWidgets,
+                    otherFolders, deferredExecutor);
 
             // Tell the workspace that we're done binding items
             r = new Runnable() {
@@ -2700,11 +2655,23 @@
 
                 }
             };
+            deferredExecutor.execute(r);
+
             if (isLoadingSynchronously) {
-                synchronized (mDeferredBindRunnables) {
-                    mDeferredBindRunnables.add(r);
-                }
-            } else {
+                r = new Runnable() {
+                    public void run() {
+                        Callbacks callbacks = tryGetCallbacks(oldCallbacks);
+                        if (callbacks != null) {
+                            // We are loading synchronously, which means, some of the pages will be
+                            // bound after first draw. Inform the callbacks that page binding is
+                            // not complete, and schedule the remaining pages.
+                            if (currentScreen != PagedView.INVALID_RESTORE_PAGE) {
+                                callbacks.onPageBoundSynchronously(currentScreen);
+                            }
+                            callbacks.executeOnNextDraw((ViewOnDrawExecutor) deferredExecutor);
+                        }
+                    }
+                };
                 runOnMainThread(r);
             }
         }
@@ -3730,6 +3697,14 @@
         }
     }
 
+    @Thunk class DeferredMainThreadExecutor implements Executor {
+
+        @Override
+        public void execute(Runnable command) {
+            runOnMainThread(command);
+        }
+    }
+
     /**
      * @return the looper for the worker thread which can be used to start background tasks.
      */
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index 18f5f7f..1539658 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -278,13 +278,6 @@
 
     private AccessibilityDelegate mPagesAccessibilityDelegate;
 
-    private final Runnable mBindPages = new Runnable() {
-        @Override
-        public void run() {
-            mLauncher.getModel().bindRemainingSynchronousPages();
-        }
-    };
-
     /**
      * Used to inflate the Workspace from XML.
      *
@@ -1719,14 +1712,6 @@
     }
 
     @Override
-    protected void onDraw(Canvas canvas) {
-        super.onDraw(canvas);
-
-        // Call back to LauncherModel to finish binding after the first draw
-        post(mBindPages);
-    }
-
-    @Override
     protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
         if (!mLauncher.isAppsViewVisible()) {
             final Folder openFolder = getOpenFolder();
diff --git a/src/com/android/launcher3/util/ViewOnDrawExecutor.java b/src/com/android/launcher3/util/ViewOnDrawExecutor.java
new file mode 100644
index 0000000..01808ba
--- /dev/null
+++ b/src/com/android/launcher3/util/ViewOnDrawExecutor.java
@@ -0,0 +1,96 @@
+/**
+ * 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.view.View;
+import android.view.View.OnAttachStateChangeListener;
+import android.view.ViewTreeObserver.OnDrawListener;
+
+import com.android.launcher3.DeferredHandler;
+import com.android.launcher3.Launcher;
+
+import java.util.ArrayList;
+import java.util.concurrent.Executor;
+
+/**
+ * An executor which runs all the tasks after the first onDraw is called on the target view.
+ */
+public class ViewOnDrawExecutor implements Executor, OnDrawListener, Runnable,
+        OnAttachStateChangeListener {
+
+    private final ArrayList<Runnable> mTasks = new ArrayList<>();
+    private final DeferredHandler mHandler;
+
+    private Launcher mLauncher;
+    private View mAttachedView;
+    private boolean mCompleted;
+
+    public ViewOnDrawExecutor(DeferredHandler handler) {
+        mHandler = handler;
+    }
+
+    public void attachTo(Launcher launcher) {
+        mLauncher = launcher;
+        mAttachedView = launcher.getWorkspace();
+        mAttachedView.addOnAttachStateChangeListener(this);
+
+        attachObserver();
+    }
+
+    private void attachObserver() {
+        if (!mCompleted) {
+            mAttachedView.getViewTreeObserver().addOnDrawListener(this);
+        }
+    }
+
+    @Override
+    public void execute(Runnable command) {
+        mTasks.add(command);
+    }
+
+    @Override
+    public void onViewAttachedToWindow(View v) {
+        attachObserver();
+    }
+
+    @Override
+    public void onViewDetachedFromWindow(View v) { }
+
+    @Override
+    public void onDraw() {
+        mAttachedView.post(this);
+    }
+
+    @Override
+    public void run() {
+        for (final Runnable r : mTasks) {
+            mHandler.post(r);
+        }
+        markCompleted();
+    }
+
+    public void markCompleted() {
+        mTasks.clear();
+        if (mAttachedView != null) {
+            mAttachedView.getViewTreeObserver().removeOnDrawListener(this);
+            mAttachedView.removeOnAttachStateChangeListener(this);
+        }
+        if (mLauncher != null) {
+            mLauncher.clearPendingExecutor(this);
+        }
+    }
+}