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);
+ }
+ });
+ }
+ }
}