diff --git a/core/java/android/appwidget/AppWidgetHostView.java b/core/java/android/appwidget/AppWidgetHostView.java index bb5f7a1d606ee..4c8360f6c345f 100644 --- a/core/java/android/appwidget/AppWidgetHostView.java +++ b/core/java/android/appwidget/AppWidgetHostView.java @@ -377,13 +377,13 @@ public class AppWidgetHostView extends FrameLayout { * 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 @@ public class AppWidgetHostView extends FrameLayout { 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 184453b3a5f21..d5a933ce4cc99 100644 --- a/core/java/android/widget/AbsListView.java +++ b/core/java/android/widget/AbsListView.java @@ -6284,6 +6284,17 @@ public abstract class AbsListView extends AdapterView implements Te * @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 @@ public abstract class AbsListView extends AdapterView implements Te } 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 0e3a69f5f0cd6..6f293684ad3c9 100644 --- a/core/java/android/widget/AdapterViewAnimator.java +++ b/core/java/android/widget/AdapterViewAnimator.java @@ -975,8 +975,19 @@ public abstract class AdapterViewAnimator extends AdapterView * @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 @@ public abstract class AdapterViewAnimator extends AdapterView } 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 20543fb9ae8a9..82071d7477758 100644 --- a/core/java/android/widget/GridView.java +++ b/core/java/android/widget/GridView.java @@ -175,7 +175,7 @@ public class GridView extends AbsListView { * 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 4d405c5a86276..cd806510ed51a 100644 --- a/core/java/android/widget/ImageView.java +++ b/core/java/android/widget/ImageView.java @@ -455,7 +455,16 @@ public class ImageView extends View { /** @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); } /** @@ -865,7 +874,7 @@ public class ImageView extends View { } 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 e88c7efe43c89..e52c13b19534b 100644 --- a/core/java/android/widget/ListView.java +++ b/core/java/android/widget/ListView.java @@ -448,7 +448,7 @@ public class ListView extends AbsListView { * 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 6543d0cf8bdd4..316dab59628a2 100644 --- a/core/java/android/widget/RemoteViews.java +++ b/core/java/android/widget/RemoteViews.java @@ -320,6 +320,10 @@ public class RemoteViews implements Parcelable, Filter { return this; } + public boolean prefersAsyncApply() { + return false; + } + int viewId; } @@ -715,20 +719,29 @@ public class RemoteViews implements Parcelable, Filter { 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; } @@ -1461,6 +1474,11 @@ public class RemoteViews implements Parcelable, Filter { // unique from the standpoint of merging. return "ReflectionAction" + this.methodName + this.type; } + + @Override + public boolean prefersAsyncApply() { + return this.type == URI || this.type == ICON; + } } /** @@ -1598,6 +1616,11 @@ public class RemoteViews implements Parcelable, Filter { return MERGE_APPEND; } + @Override + public boolean prefersAsyncApply() { + return nestedViews != null && nestedViews.prefersAsyncApply(); + } + RemoteViews nestedViews; public final static int TAG = 4; @@ -1749,6 +1772,11 @@ public class RemoteViews implements Parcelable, Filter { return copy; } + @Override + public boolean prefersAsyncApply() { + return useIcons; + } + public String getActionName() { return "TextViewDrawableAction"; } @@ -3443,6 +3471,24 @@ public class RemoteViews implements Parcelable, Filter { } } + /** + * 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 e0f94fd207d32..11e0a3fa47743 100644 --- a/core/java/android/widget/RemoteViewsAdapter.java +++ b/core/java/android/widget/RemoteViewsAdapter.java @@ -41,6 +41,12 @@ import android.widget.RemoteViews.OnClickHandler; 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; + import java.lang.ref.WeakReference; import java.util.Arrays; import java.util.HashMap; @@ -73,7 +79,8 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback private final Context mContext; private final Intent mIntent; private final int mAppWidgetId; - private LayoutInflater mLayoutInflater; + private final Executor mAsyncViewLoadExecutor; + private RemoteViewsAdapterServiceConnection mServiceConnection; private WeakReference mCallback; private OnClickHandler mRemoteViewsOnClickHandler; @@ -120,15 +127,33 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback /** * @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); + } } /** @@ -162,7 +187,7 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback } mIsConnecting = true; } catch (Exception e) { - Log.e("RemoteViewsAdapterServiceConnection", "bind(): " + e.getMessage()); + Log.e("RVAServiceConnection", "bind(): " + e.getMessage()); mIsConnecting = false; mIsConnected = false; } @@ -180,7 +205,7 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback } mIsConnecting = false; } catch (Exception e) { - Log.e("RemoteViewsAdapterServiceConnection", "unbind(): " + e.getMessage()); + Log.e("RVAServiceConnection", "unbind(): " + e.getMessage()); mIsConnecting = false; mIsConnected = false; } @@ -298,15 +323,29 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback * 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 @@ -359,7 +398,7 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback 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); } @@ -402,9 +441,7 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback // 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(); @@ -418,7 +455,7 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback count = d.count; viewTypeCount = d.viewTypeCount; hasStableIds = d.hasStableIds; - setLoadingViewTemplates(d.mUserLoadingView, d.mFirstView); + loadingTemplate = d.loadingTemplate; } } @@ -428,20 +465,10 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback // 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) { @@ -457,33 +484,11 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback 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; } } @@ -772,7 +777,7 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback } public RemoteViewsAdapter(Context context, Intent intent, - RemoteAdapterConnectionCallback callback) { + RemoteAdapterConnectionCallback callback, boolean useAsyncLoader) { mContext = context; mIntent = intent; @@ -781,7 +786,6 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback } 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 @@ -794,6 +798,7 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback 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"); @@ -941,10 +946,14 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback 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) { @@ -952,7 +961,7 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback // 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); @@ -1100,18 +1109,25 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback 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(); @@ -1285,4 +1301,58 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback 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); + } + }); + } + } }