Adding support for async view loading in RemoteViewsAdapter

> When loadingView is no available, the FirstView is always
loaded on the background thread
> AppWidgetHostView only inflates on the background thread, if
the view has any costly operations

Test: TBD

Change-Id: I701caee7e4e6ba5972d0cf478cb57f8ec950da54
diff --git a/core/java/android/appwidget/AppWidgetHostView.java b/core/java/android/appwidget/AppWidgetHostView.java
index bb5f7a1..4c8360f 100644
--- a/core/java/android/appwidget/AppWidgetHostView.java
+++ b/core/java/android/appwidget/AppWidgetHostView.java
@@ -377,13 +377,13 @@
      * AppWidget provider. Will animate into these new views as needed
      */
     public void updateAppWidget(RemoteViews remoteViews) {
-        applyRemoteViews(remoteViews);
+        applyRemoteViews(remoteViews, true);
     }
 
     /**
      * @hide
      */
-    protected void applyRemoteViews(RemoteViews remoteViews) {
+    protected void applyRemoteViews(RemoteViews remoteViews, boolean useAsyncIfPossible) {
         if (LOGD) Log.d(TAG, "updateAppWidget called mOld=" + mOld);
 
         boolean recycled = false;
@@ -423,7 +423,7 @@
             mLayoutId = -1;
             mViewMode = VIEW_MODE_DEFAULT;
         } else {
-            if (mAsyncExecutor != null) {
+            if (mAsyncExecutor != null && useAsyncIfPossible) {
                 inflateAsync(remoteViews);
                 return;
             }
diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java
index 184453b..d5a933c 100644
--- a/core/java/android/widget/AbsListView.java
+++ b/core/java/android/widget/AbsListView.java
@@ -6284,6 +6284,17 @@
      * @param intent the intent used to identify the RemoteViewsService for the adapter to connect to.
      */
     public void setRemoteViewsAdapter(Intent intent) {
+        setRemoteViewsAdapter(intent, false);
+    }
+
+    /** @hide **/
+    public Runnable setRemoteViewsAdapterAsync(final Intent intent) {
+        return new RemoteViewsAdapter.AsyncRemoteAdapterAction(this, intent);
+    }
+
+    /** @hide **/
+    @Override
+    public void setRemoteViewsAdapter(Intent intent, boolean isAsync) {
         // Ensure that we don't already have a RemoteViewsAdapter that is bound to an existing
         // service handling the specified intent.
         if (mRemoteAdapter != null) {
@@ -6296,7 +6307,7 @@
         }
         mDeferNotifyDataSetChanged = false;
         // Otherwise, create a new RemoteViewsAdapter for binding
-        mRemoteAdapter = new RemoteViewsAdapter(getContext(), intent, this);
+        mRemoteAdapter = new RemoteViewsAdapter(getContext(), intent, this, isAsync);
         if (mRemoteAdapter.isDataReady()) {
             setAdapter(mRemoteAdapter);
         }
diff --git a/core/java/android/widget/AdapterViewAnimator.java b/core/java/android/widget/AdapterViewAnimator.java
index 0e3a69f..6f29368 100644
--- a/core/java/android/widget/AdapterViewAnimator.java
+++ b/core/java/android/widget/AdapterViewAnimator.java
@@ -975,8 +975,19 @@
      * @param intent the intent used to identify the RemoteViewsService for the adapter to
      *        connect to.
      */
-    @android.view.RemotableViewMethod
+    @android.view.RemotableViewMethod(asyncImpl="setRemoteViewsAdapterAsync")
     public void setRemoteViewsAdapter(Intent intent) {
+        setRemoteViewsAdapter(intent, false);
+    }
+
+    /** @hide **/
+    public Runnable setRemoteViewsAdapterAsync(final Intent intent) {
+        return new RemoteViewsAdapter.AsyncRemoteAdapterAction(this, intent);
+    }
+
+    /** @hide **/
+    @Override
+    public void setRemoteViewsAdapter(Intent intent, boolean isAsync) {
         // Ensure that we don't already have a RemoteViewsAdapter that is bound to an existing
         // service handling the specified intent.
         if (mRemoteViewsAdapter != null) {
@@ -989,7 +1000,7 @@
         }
         mDeferNotifyDataSetChanged = false;
         // Otherwise, create a new RemoteViewsAdapter for binding
-        mRemoteViewsAdapter = new RemoteViewsAdapter(getContext(), intent, this);
+        mRemoteViewsAdapter = new RemoteViewsAdapter(getContext(), intent, this, isAsync);
         if (mRemoteViewsAdapter.isDataReady()) {
             setAdapter(mRemoteViewsAdapter);
         }
diff --git a/core/java/android/widget/GridView.java b/core/java/android/widget/GridView.java
index b95aa52..2b822fc 100644
--- a/core/java/android/widget/GridView.java
+++ b/core/java/android/widget/GridView.java
@@ -175,7 +175,7 @@
      * through the specified intent.
      * @param intent the intent used to identify the RemoteViewsService for the adapter to connect to.
      */
-    @android.view.RemotableViewMethod
+    @android.view.RemotableViewMethod(asyncImpl="setRemoteViewsAdapterAsync")
     public void setRemoteViewsAdapter(Intent intent) {
         super.setRemoteViewsAdapter(intent);
     }
diff --git a/core/java/android/widget/ImageView.java b/core/java/android/widget/ImageView.java
index d9cb269..369fe58 100644
--- a/core/java/android/widget/ImageView.java
+++ b/core/java/android/widget/ImageView.java
@@ -455,7 +455,16 @@
 
     /** @hide **/
     public Runnable setImageResourceAsync(@DrawableRes int resId) {
-        return new ImageDrawableCallback(getContext().getDrawable(resId), null, resId);
+        Drawable d = null;
+        if (resId != 0) {
+            try {
+                d = getContext().getDrawable(resId);
+            } catch (Exception e) {
+                Log.w(LOG_TAG, "Unable to find resource: " + resId, e);
+                resId = 0;
+            }
+        }
+        return new ImageDrawableCallback(d, null, resId);
     }
 
     /**
@@ -857,7 +866,7 @@
             } catch (Exception e) {
                 Log.w(LOG_TAG, "Unable to find resource: " + mResource, e);
                 // Don't try again.
-                mUri = null;
+                mResource = 0;
             }
         } else if (mUri != null) {
             d = getDrawableFromUri(mUri);
diff --git a/core/java/android/widget/ListView.java b/core/java/android/widget/ListView.java
index b0f19d7..cfe1c09 100644
--- a/core/java/android/widget/ListView.java
+++ b/core/java/android/widget/ListView.java
@@ -448,7 +448,7 @@
      * through the specified intent.
      * @param intent the intent used to identify the RemoteViewsService for the adapter to connect to.
      */
-    @android.view.RemotableViewMethod
+    @android.view.RemotableViewMethod(asyncImpl="setRemoteViewsAdapterAsync")
     public void setRemoteViewsAdapter(Intent intent) {
         super.setRemoteViewsAdapter(intent);
     }
diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java
index 2a6e01f..ea4bd13 100644
--- a/core/java/android/widget/RemoteViews.java
+++ b/core/java/android/widget/RemoteViews.java
@@ -319,6 +319,10 @@
             return this;
         }
 
+        public boolean prefersAsyncApply() {
+            return false;
+        }
+
         int viewId;
     }
 
@@ -714,20 +718,29 @@
             intent.putExtra(EXTRA_REMOTEADAPTER_APPWIDGET_ID, host.getAppWidgetId());
             if (target instanceof AbsListView) {
                 AbsListView v = (AbsListView) target;
-                v.setRemoteViewsAdapter(intent);
+                v.setRemoteViewsAdapter(intent, isAsync);
                 v.setRemoteViewsOnClickHandler(handler);
             } else if (target instanceof AdapterViewAnimator) {
                 AdapterViewAnimator v = (AdapterViewAnimator) target;
-                v.setRemoteViewsAdapter(intent);
+                v.setRemoteViewsAdapter(intent, isAsync);
                 v.setRemoteViewsOnClickHandler(handler);
             }
         }
 
+        @Override
+        public Action initActionAsync(ViewTree root, ViewGroup rootParent,
+                OnClickHandler handler) {
+            SetRemoteViewsAdapterIntent copy = new SetRemoteViewsAdapterIntent(viewId, intent);
+            copy.isAsync = true;
+            return copy;
+        }
+
         public String getActionName() {
             return "SetRemoteViewsAdapterIntent";
         }
 
         Intent intent;
+        boolean isAsync = false;
 
         public final static int TAG = 10;
     }
@@ -1460,6 +1473,11 @@
             // unique from the standpoint of merging.
             return "ReflectionAction" + this.methodName + this.type;
         }
+
+        @Override
+        public boolean prefersAsyncApply() {
+            return this.type == URI || this.type == ICON;
+        }
     }
 
     /**
@@ -1597,6 +1615,11 @@
             return MERGE_APPEND;
         }
 
+        @Override
+        public boolean prefersAsyncApply() {
+            return nestedViews != null && nestedViews.prefersAsyncApply();
+        }
+
         RemoteViews nestedViews;
 
         public final static int TAG = 4;
@@ -1748,6 +1771,11 @@
             return copy;
         }
 
+        @Override
+        public boolean prefersAsyncApply() {
+            return useIcons;
+        }
+
         public String getActionName() {
             return "TextViewDrawableAction";
         }
@@ -3442,6 +3470,24 @@
         }
     }
 
+    /**
+     * Returns true if the RemoteViews contains potentially costly operations and should be
+     * applied asynchronously.
+     *
+     * @hide
+     */
+    public boolean prefersAsyncApply() {
+        if (mActions != null) {
+            final int count = mActions.size();
+            for (int i = 0; i < count; i++) {
+                if (mActions.get(i).prefersAsyncApply()) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
     private Context getContextForResources(Context context) {
         if (mApplication != null) {
             if (context.getUserId() == UserHandle.getUserId(mApplication.uid)
diff --git a/core/java/android/widget/RemoteViewsAdapter.java b/core/java/android/widget/RemoteViewsAdapter.java
index 10abbab..d41bd46 100644
--- a/core/java/android/widget/RemoteViewsAdapter.java
+++ b/core/java/android/widget/RemoteViewsAdapter.java
@@ -16,13 +16,6 @@
 
 package android.widget;
 
-import java.lang.ref.WeakReference;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.LinkedList;
-
 import android.Manifest;
 import android.appwidget.AppWidgetHostView;
 import android.appwidget.AppWidgetManager;
@@ -48,6 +41,12 @@
 import com.android.internal.widget.IRemoteViewsAdapterConnection;
 import com.android.internal.widget.IRemoteViewsFactory;
 
+import java.lang.ref.WeakReference;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.concurrent.Executor;
+
 /**
  * An adapter to a RemoteViewsService which fetches and caches RemoteViews
  * to be later inflated as child views.
@@ -75,7 +74,8 @@
     private final Context mContext;
     private final Intent mIntent;
     private final int mAppWidgetId;
-    private LayoutInflater mLayoutInflater;
+    private final Executor mAsyncViewLoadExecutor;
+
     private RemoteViewsAdapterServiceConnection mServiceConnection;
     private WeakReference<RemoteAdapterConnectionCallback> mCallback;
     private OnClickHandler mRemoteViewsOnClickHandler;
@@ -122,15 +122,33 @@
         /**
          * @return whether the adapter was set or not.
          */
-        public boolean onRemoteAdapterConnected();
+        boolean onRemoteAdapterConnected();
 
-        public void onRemoteAdapterDisconnected();
+        void onRemoteAdapterDisconnected();
 
         /**
          * This defers a notifyDataSetChanged on the pending RemoteViewsAdapter if it has not
          * connected yet.
          */
-        public void deferNotifyDataSetChanged();
+        void deferNotifyDataSetChanged();
+
+        void setRemoteViewsAdapter(Intent intent, boolean isAsync);
+    }
+
+    public static class AsyncRemoteAdapterAction implements Runnable {
+
+        private final RemoteAdapterConnectionCallback mCallback;
+        private final Intent mIntent;
+
+        public AsyncRemoteAdapterAction(RemoteAdapterConnectionCallback callback, Intent intent) {
+            mCallback = callback;
+            mIntent = intent;
+        }
+
+        @Override
+        public void run() {
+            mCallback.setRemoteViewsAdapter(mIntent, true);
+        }
     }
 
     /**
@@ -164,7 +182,7 @@
                     }
                     mIsConnecting = true;
                 } catch (Exception e) {
-                    Log.e("RemoteViewsAdapterServiceConnection", "bind(): " + e.getMessage());
+                    Log.e("RVAServiceConnection", "bind(): " + e.getMessage());
                     mIsConnecting = false;
                     mIsConnected = false;
                 }
@@ -182,7 +200,7 @@
                 }
                 mIsConnecting = false;
             } catch (Exception e) {
-                Log.e("RemoteViewsAdapterServiceConnection", "unbind(): " + e.getMessage());
+                Log.e("RVAServiceConnection", "unbind(): " + e.getMessage());
                 mIsConnecting = false;
                 mIsConnected = false;
             }
@@ -300,15 +318,29 @@
          * Updates this RemoteViewsFrameLayout depending on the view that was loaded.
          * @param view the RemoteViews that was loaded. If null, the RemoteViews was not loaded
          *             successfully.
+         * @param forceApplyAsync when true, the host will always try to inflate the view
+         *                        asynchronously (for eg, when we are already showing the loading
+         *                        view)
          */
-        public void onRemoteViewsLoaded(RemoteViews view, OnClickHandler handler) {
+        public void onRemoteViewsLoaded(RemoteViews view, OnClickHandler handler,
+                boolean forceApplyAsync) {
             setOnClickHandler(handler);
-            applyRemoteViews(view);
+            applyRemoteViews(view, forceApplyAsync || ((view != null) && view.prefersAsyncApply()));
         }
 
+        /**
+         * Creates a default loading view. Uses the size of the first row as a guide for the
+         * size of the loading view.
+         */
         @Override
         protected View getDefaultView() {
-            return mCache.getMetaData().createDefaultLoadingView(this);
+            int viewHeight = mCache.getMetaData().getLoadingTemplate(getContext()).defaultHeight;
+            // Compose the loading view text
+            TextView loadingTextView = (TextView) LayoutInflater.from(getContext()).inflate(
+                    com.android.internal.R.layout.remote_views_adapter_default_loading_view,
+                    this, false);
+            loadingTextView.setHeight(viewHeight);
+            return loadingTextView;
         }
 
         @Override
@@ -361,7 +393,7 @@
             if (refs != null) {
                 // Notify all the references for that position of the newly loaded RemoteViews
                 for (final RemoteViewsFrameLayout ref : refs) {
-                    ref.onRemoteViewsLoaded(view, mRemoteViewsOnClickHandler);
+                    ref.onRemoteViewsLoaded(view, mRemoteViewsOnClickHandler, true);
                     if (mViewToLinkedList.containsKey(ref)) {
                         mViewToLinkedList.remove(ref);
                     }
@@ -404,9 +436,7 @@
         // Used to determine how to construct loading views.  If a loading view is not specified
         // by the user, then we try and load the first view, and use its height as the height for
         // the default loading view.
-        RemoteViews mUserLoadingView;
-        RemoteViews mFirstView;
-        int mFirstViewHeight;
+        LoadingViewTemplate loadingTemplate;
 
         // A mapping from type id to a set of unique type ids
         private final SparseIntArray mTypeIdIndexMap = new SparseIntArray();
@@ -420,7 +450,7 @@
                 count = d.count;
                 viewTypeCount = d.viewTypeCount;
                 hasStableIds = d.hasStableIds;
-                setLoadingViewTemplates(d.mUserLoadingView, d.mFirstView);
+                loadingTemplate = d.loadingTemplate;
             }
         }
 
@@ -430,20 +460,10 @@
             // by default there is at least one dummy view type
             viewTypeCount = 1;
             hasStableIds = true;
-            mUserLoadingView = null;
-            mFirstView = null;
-            mFirstViewHeight = 0;
+            loadingTemplate = null;
             mTypeIdIndexMap.clear();
         }
 
-        public void setLoadingViewTemplates(RemoteViews loadingView, RemoteViews firstView) {
-            mUserLoadingView = loadingView;
-            if (firstView != null) {
-                mFirstView = firstView;
-                mFirstViewHeight = -1;
-            }
-        }
-
         public int getMappedViewType(int typeId) {
             int mappedTypeId = mTypeIdIndexMap.get(typeId, -1);
             if (mappedTypeId == -1) {
@@ -459,33 +479,11 @@
             return (mappedType < viewTypeCount);
         }
 
-        /**
-         * Creates a default loading view. Uses the size of the first row as a guide for the
-         * size of the loading view.
-         */
-        private synchronized View createDefaultLoadingView(ViewGroup parent) {
-            final Context context = parent.getContext();
-            if (mFirstViewHeight < 0) {
-                try {
-                    View firstView = mFirstView.apply(parent.getContext(), parent);
-                    firstView.measure(
-                            MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
-                            MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
-                    mFirstViewHeight = firstView.getMeasuredHeight();
-                } catch (Exception e) {
-                    float density = context.getResources().getDisplayMetrics().density;
-                    mFirstViewHeight = Math.round(sDefaultLoadingViewHeight * density);
-                    Log.w(TAG, "Error inflating first RemoteViews" + e);
-                }
-                mFirstView = null;
+        public synchronized LoadingViewTemplate getLoadingTemplate(Context context) {
+            if (loadingTemplate == null) {
+                loadingTemplate = new LoadingViewTemplate(null, context);
             }
-
-            // Compose the loading view text
-            TextView loadingTextView = (TextView) LayoutInflater.from(context).inflate(
-                    com.android.internal.R.layout.remote_views_adapter_default_loading_view,
-                    parent, false);
-            loadingTextView.setHeight(mFirstViewHeight);
-            return loadingTextView;
+            return loadingTemplate;
         }
     }
 
@@ -774,7 +772,7 @@
     }
 
     public RemoteViewsAdapter(Context context, Intent intent,
-            RemoteAdapterConnectionCallback callback) {
+            RemoteAdapterConnectionCallback callback, boolean useAsyncLoader) {
         mContext = context;
         mIntent = intent;
 
@@ -783,7 +781,6 @@
         }
 
         mAppWidgetId = intent.getIntExtra(RemoteViews.EXTRA_REMOTEADAPTER_APPWIDGET_ID, -1);
-        mLayoutInflater = LayoutInflater.from(context);
         mRequestedViews = new RemoteViewsFrameLayoutRefSet();
 
         // Strip the previously injected app widget id from service intent
@@ -796,6 +793,7 @@
         mWorkerThread.start();
         mWorkerQueue = new Handler(mWorkerThread.getLooper());
         mMainQueue = new Handler(Looper.myLooper(), this);
+        mAsyncViewLoadExecutor = useAsyncLoader ? new HandlerThreadExecutor(mWorkerThread) : null;
 
         if (sCacheRemovalThread == null) {
             sCacheRemovalThread = new HandlerThread("RemoteViewsAdapter-cachePruner");
@@ -943,10 +941,14 @@
             boolean hasStableIds = factory.hasStableIds();
             int viewTypeCount = factory.getViewTypeCount();
             int count = factory.getCount();
-            RemoteViews loadingView = factory.getLoadingView();
-            RemoteViews firstView = null;
-            if ((count > 0) && (loadingView == null)) {
-                firstView = factory.getViewAt(0);
+            LoadingViewTemplate loadingTemplate =
+                    new LoadingViewTemplate(factory.getLoadingView(), mContext);
+            if ((count > 0) && (loadingTemplate.remoteViews == null)) {
+                RemoteViews firstView = factory.getViewAt(0);
+                if (firstView != null) {
+                    loadingTemplate.loadFirstViewHeight(firstView, mContext,
+                            new HandlerThreadExecutor(mWorkerThread));
+                }
             }
             final RemoteViewsMetaData tmpMetaData = mCache.getTemporaryMetaData();
             synchronized (tmpMetaData) {
@@ -954,7 +956,7 @@
                 // We +1 because the base view type is the loading view
                 tmpMetaData.viewTypeCount = viewTypeCount + 1;
                 tmpMetaData.count = count;
-                tmpMetaData.setLoadingViewTemplates(loadingView, firstView);
+                tmpMetaData.loadingTemplate = loadingTemplate;
             }
         } catch(RemoteException e) {
             processException("updateMetaData", e);
@@ -1102,18 +1104,25 @@
                 hasNewItems = mCache.queuePositionsToBePreloadedFromRequestedPosition(position);
             }
 
-            final RemoteViewsFrameLayout layout =
-                    (convertView instanceof RemoteViewsFrameLayout)
-                            ? (RemoteViewsFrameLayout) convertView
-                            : new RemoteViewsFrameLayout(parent.getContext(), mCache);
+            final RemoteViewsFrameLayout layout;
+            if (convertView instanceof RemoteViewsFrameLayout) {
+                layout = (RemoteViewsFrameLayout) convertView;
+            } else {
+                layout = new RemoteViewsFrameLayout(parent.getContext(), mCache);
+                layout.setAsyncExecutor(mAsyncViewLoadExecutor);
+            }
+
             if (isInCache) {
-                layout.onRemoteViewsLoaded(rv, mRemoteViewsOnClickHandler);
+                // Apply the view synchronously if possible, to avoid flickering
+                layout.onRemoteViewsLoaded(rv, mRemoteViewsOnClickHandler, false);
                 if (hasNewItems) loadNextIndexInBackground();
             } else {
                 // If the views is not loaded, apply the loading view. If the loading view doesn't
                 // exist, the layout will create a default view based on the firstView height.
-                layout.onRemoteViewsLoaded(mCache.getMetaData().mUserLoadingView,
-                        mRemoteViewsOnClickHandler);
+                layout.onRemoteViewsLoaded(
+                        mCache.getMetaData().getLoadingTemplate(mContext).remoteViews,
+                        mRemoteViewsOnClickHandler,
+                        false);
                 mRequestedViews.add(position, layout);
                 mCache.queueRequestedPositionToLoad(position);
                 loadNextIndexInBackground();
@@ -1287,4 +1296,58 @@
         mMainQueue.removeMessages(sUnbindServiceMessageType);
         return mServiceConnection.isConnected();
     }
+
+    private static class HandlerThreadExecutor implements Executor {
+        private final HandlerThread mThread;
+
+        HandlerThreadExecutor(HandlerThread thread) {
+            mThread = thread;
+        }
+
+        @Override
+        public void execute(Runnable runnable) {
+            if (Thread.currentThread().getId() == mThread.getId()) {
+                runnable.run();
+            } else {
+                new Handler(mThread.getLooper()).post(runnable);
+            }
+        }
+    }
+
+    private static class LoadingViewTemplate {
+        public final RemoteViews remoteViews;
+        public int defaultHeight;
+
+        LoadingViewTemplate(RemoteViews views, Context context) {
+            remoteViews = views;
+
+            float density = context.getResources().getDisplayMetrics().density;
+            defaultHeight = Math.round(sDefaultLoadingViewHeight * density);
+        }
+
+        public void loadFirstViewHeight(
+                RemoteViews firstView, Context context, Executor executor) {
+            // Inflate the first view on the worker thread
+            firstView.applyAsync(context, new RemoteViewsFrameLayout(context, null), executor,
+                    new RemoteViews.OnViewAppliedListener() {
+                        @Override
+                        public void onViewApplied(View v) {
+                            try {
+                                v.measure(
+                                        MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
+                                        MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
+                                defaultHeight = v.getMeasuredHeight();
+                            } catch (Exception e) {
+                                onError(e);
+                            }
+                        }
+
+                        @Override
+                        public void onError(Exception e) {
+                            // Do nothing. The default height will stay the same.
+                            Log.w(TAG, "Error inflating first RemoteViews", e);
+                        }
+                    });
+        }
+    }
 }