From dd292f4fab52f99d0b15fcf961864cd8186d662c Mon Sep 17 00:00:00 2001 From: Sunny Goyal Date: Wed, 2 Dec 2015 14:29:27 -0800 Subject: [PATCH] Added support for async inflation of RemoteViews When enabled, the View inflation as well as some of the drawable loading is done on the background thread. It also assumes that all the views support inflation on the background thread, which is true for the default views supported by the platform For ViewGroup opetations, it maintains a virtual view tree which is used for findViewById. The final operations are applied on the UI thread in the end. Change-Id: Ib3ce55182ec04a344f2b2513981ee03afc8fa2e8 --- .../android/appwidget/AppWidgetHostView.java | 98 ++++ .../android/view/RemotableViewMethod.java | 6 + core/java/android/widget/ImageView.java | 119 +++-- core/java/android/widget/RemoteViews.java | 465 +++++++++++++++++- 4 files changed, 643 insertions(+), 45 deletions(-) diff --git a/core/java/android/appwidget/AppWidgetHostView.java b/core/java/android/appwidget/AppWidgetHostView.java index 3219fe7fed603..bcfd1cf2fdf07 100644 --- a/core/java/android/appwidget/AppWidgetHostView.java +++ b/core/java/android/appwidget/AppWidgetHostView.java @@ -29,6 +29,7 @@ import android.graphics.Paint; import android.graphics.Rect; import android.os.Build; import android.os.Bundle; +import android.os.CancellationSignal; import android.os.Parcel; import android.os.Parcelable; import android.os.SystemClock; @@ -38,6 +39,7 @@ import android.util.SparseArray; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; +import android.view.ViewGroup; import android.view.accessibility.AccessibilityNodeInfo; import android.widget.Adapter; import android.widget.AdapterView; @@ -48,6 +50,8 @@ import android.widget.RemoteViews.OnClickHandler; import android.widget.RemoteViewsAdapter.RemoteAdapterConnectionCallback; import android.widget.TextView; +import java.util.concurrent.Executor; + /** * Provides the glue to show AppWidget views. This class offers automatic animation * between updates, and will try recycling old views for each incoming @@ -86,6 +90,9 @@ public class AppWidgetHostView extends FrameLayout { Paint mOldPaint = new Paint(); private OnClickHandler mOnClickHandler; + private Executor mAsyncExecutor; + private CancellationSignal mLastExecutionSignal; + /** * Create a host view. Uses default fade animations. */ @@ -319,6 +326,22 @@ public class AppWidgetHostView extends FrameLayout { return new FrameLayout.LayoutParams(context, attrs); } + /** + * Sets an executor which can be used for asynchronously inflating and applying the remoteviews. + * @see {@link RemoteViews#applyAsync(Context, ViewGroup, RemoteViews.OnViewAppliedListener, Executor)} + * + * @param executor the executor to use or null. + * @hide + */ + public void setAsyncExecutor(Executor executor) { + if (mLastExecutionSignal != null) { + mLastExecutionSignal.cancel(); + mLastExecutionSignal = null; + } + + mAsyncExecutor = executor; + } + /** * Update the AppWidgetProviderInfo for this view, and reset it to the * initial layout. @@ -360,6 +383,11 @@ public class AppWidgetHostView extends FrameLayout { } } + if (mLastExecutionSignal != null) { + mLastExecutionSignal.cancel(); + mLastExecutionSignal = null; + } + if (remoteViews == null) { if (mViewMode == VIEW_MODE_DEFAULT) { // We've already done this -- nothing to do. @@ -369,6 +397,10 @@ public class AppWidgetHostView extends FrameLayout { mLayoutId = -1; mViewMode = VIEW_MODE_DEFAULT; } else { + if (mAsyncExecutor != null) { + inflateAsync(remoteViews); + return; + } // Prepare a local reference to the remote Context so we're ready to // inflate any requested LayoutParams. mRemoteContext = getRemoteContext(); @@ -401,6 +433,10 @@ public class AppWidgetHostView extends FrameLayout { mViewMode = VIEW_MODE_CONTENT; } + applyContent(content, recycled, exception); + } + + private void applyContent(View content, boolean recycled, Exception exception) { if (content == null) { if (mViewMode == VIEW_MODE_ERROR) { // We've already done this -- nothing to do. @@ -432,6 +468,68 @@ public class AppWidgetHostView extends FrameLayout { } } + private void inflateAsync(RemoteViews remoteViews) { + // Prepare a local reference to the remote Context so we're ready to + // inflate any requested LayoutParams. + mRemoteContext = getRemoteContext(); + int layoutId = remoteViews.getLayoutId(); + + // If our stale view has been prepared to match active, and the new + // layout matches, try recycling it + if (layoutId == mLayoutId && mView != null) { + try { + mLastExecutionSignal = remoteViews.reapplyAsync(mContext, + mView, + mAsyncExecutor, + new ViewApplyListener(remoteViews, layoutId, true), + mOnClickHandler); + } catch (Exception e) { + // Reapply failed. Try apply + } + } + if (mLastExecutionSignal == null) { + mLastExecutionSignal = remoteViews.applyAsync(mContext, + this, + mAsyncExecutor, + new ViewApplyListener(remoteViews, layoutId, false), + mOnClickHandler); + } + } + + private class ViewApplyListener implements RemoteViews.OnViewAppliedListener { + private final RemoteViews mViews; + private final boolean mIsReapply; + private final int mLayoutId; + + public ViewApplyListener(RemoteViews views, int layoutId, boolean isReapply) { + mViews = views; + mLayoutId = layoutId; + mIsReapply = isReapply; + } + + @Override + public void onViewApplied(View v) { + AppWidgetHostView.this.mLayoutId = mLayoutId; + mViewMode = VIEW_MODE_CONTENT; + + applyContent(v, mIsReapply, null); + } + + @Override + public void onError(Exception e) { + if (mIsReapply) { + // Try a fresh replay + mLastExecutionSignal = mViews.applyAsync(mContext, + AppWidgetHostView.this, + mAsyncExecutor, + new ViewApplyListener(mViews, mLayoutId, false), + mOnClickHandler); + } else { + applyContent(null, false, e); + } + } + } + /** * Process data-changed notifications for the specified view in the specified * set of {@link RemoteViews} views. diff --git a/core/java/android/view/RemotableViewMethod.java b/core/java/android/view/RemotableViewMethod.java index 4318290affc47..e5cae84942f96 100644 --- a/core/java/android/view/RemotableViewMethod.java +++ b/core/java/android/view/RemotableViewMethod.java @@ -29,6 +29,12 @@ import java.lang.annotation.Target; @Target({ ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) public @interface RemotableViewMethod { + /** + * @return Method name which can be called on a background thread. It should have the + * same arguments as the original method and should return a {@link Runnable} (or null) + * which will be called on the UI thread. + */ + String asyncImpl() default ""; } diff --git a/core/java/android/widget/ImageView.java b/core/java/android/widget/ImageView.java index 6e90baf26fa2f..f601f7df1fa81 100644 --- a/core/java/android/widget/ImageView.java +++ b/core/java/android/widget/ImageView.java @@ -392,6 +392,26 @@ public class ImageView extends View { return mDrawable; } + private class ImageDrawableCallback implements Runnable { + + private final Drawable drawable; + private final Uri uri; + private final int resource; + + ImageDrawableCallback(Drawable drawable, Uri uri, int resource) { + this.drawable = drawable; + this.uri = uri; + this.resource = resource; + } + + @Override + public void run() { + setImageDrawable(drawable); + mUri = uri; + mResource = resource; + } + } + /** * Sets a drawable as the content of this ImageView. * @@ -405,7 +425,7 @@ public class ImageView extends View { * * @attr ref android.R.styleable#ImageView_src */ - @android.view.RemotableViewMethod + @android.view.RemotableViewMethod(asyncImpl="setImageResourceAsync") public void setImageResource(@DrawableRes int resId) { // The resource configuration may have changed, so we should always // try to load the resource even if the resId hasn't changed. @@ -424,6 +444,11 @@ public class ImageView extends View { invalidate(); } + /** @hide **/ + public Runnable setImageResourceAsync(@DrawableRes int resId) { + return new ImageDrawableCallback(getContext().getDrawable(resId), null, resId); + } + /** * Sets the content of this ImageView to the specified Uri. * @@ -435,7 +460,7 @@ public class ImageView extends View { * * @param uri the Uri of an image, or {@code null} to clear the content */ - @android.view.RemotableViewMethod + @android.view.RemotableViewMethod(asyncImpl="setImageURIAsync") public void setImageURI(@Nullable Uri uri) { if (mResource != 0 || (mUri != uri && (uri == null || mUri == null || !uri.equals(mUri)))) { updateDrawable(null); @@ -454,6 +479,19 @@ public class ImageView extends View { } } + /** @hide **/ + public Runnable setImageURIAsync(@Nullable Uri uri) { + if (mResource != 0 || (mUri != uri && (uri == null || mUri == null || !uri.equals(mUri)))) { + Drawable d = uri == null ? null : getDrawableFromUri(uri); + if (d == null) { + // Do not set the URI if the drawable couldn't be loaded. + uri = null; + } + return new ImageDrawableCallback(d, uri, 0); + } + return null; + } + /** * Sets a drawable as the content of this ImageView. * @@ -490,11 +528,16 @@ public class ImageView extends View { * @param icon an Icon holding the desired image, or {@code null} to clear * the content */ - @android.view.RemotableViewMethod + @android.view.RemotableViewMethod(asyncImpl="setImageIconAsync") public void setImageIcon(@Nullable Icon icon) { setImageDrawable(icon == null ? null : icon.loadDrawable(mContext)); } + /** @hide **/ + public Runnable setImageIconAsync(@Nullable Icon icon) { + return new ImageDrawableCallback(icon == null ? null : icon.loadDrawable(mContext), null, 0); + } + /** * Applies a tint to the image drawable. Does not modify the current tint * mode, which is {@link PorterDuff.Mode#SRC_IN} by default. @@ -786,8 +829,7 @@ public class ImageView extends View { return; } - final Resources res = getResources(); - if (res == null) { + if (getResources() == null) { return; } @@ -802,37 +844,7 @@ public class ImageView extends View { mUri = null; } } else if (mUri != null) { - final String scheme = mUri.getScheme(); - if (ContentResolver.SCHEME_ANDROID_RESOURCE.equals(scheme)) { - try { - // Load drawable through Resources, to get the source density information - ContentResolver.OpenResourceIdResult r = - mContext.getContentResolver().getResourceId(mUri); - d = r.r.getDrawable(r.id, mContext.getTheme()); - } catch (Exception e) { - Log.w(LOG_TAG, "Unable to open content: " + mUri, e); - } - } else if (ContentResolver.SCHEME_CONTENT.equals(scheme) - || ContentResolver.SCHEME_FILE.equals(scheme)) { - InputStream stream = null; - try { - stream = mContext.getContentResolver().openInputStream(mUri); - d = Drawable.createFromResourceStream( - mUseCorrectStreamDensity ? res : null, null, stream, null); - } catch (Exception e) { - Log.w(LOG_TAG, "Unable to open content: " + mUri, e); - } finally { - if (stream != null) { - try { - stream.close(); - } catch (IOException e) { - Log.w(LOG_TAG, "Unable to close content: " + mUri, e); - } - } - } - } else { - d = Drawable.createFromPath(mUri.toString()); - } + d = getDrawableFromUri(mUri); if (d == null) { Log.w(LOG_TAG, "resolveUri failed on bad bitmap uri: " + mUri); @@ -846,6 +858,41 @@ public class ImageView extends View { updateDrawable(d); } + private Drawable getDrawableFromUri(Uri uri) { + final String scheme = uri.getScheme(); + if (ContentResolver.SCHEME_ANDROID_RESOURCE.equals(scheme)) { + try { + // Load drawable through Resources, to get the source density information + ContentResolver.OpenResourceIdResult r = + mContext.getContentResolver().getResourceId(uri); + return r.r.getDrawable(r.id, mContext.getTheme()); + } catch (Exception e) { + Log.w(LOG_TAG, "Unable to open content: " + uri, e); + } + } else if (ContentResolver.SCHEME_CONTENT.equals(scheme) + || ContentResolver.SCHEME_FILE.equals(scheme)) { + InputStream stream = null; + try { + stream = mContext.getContentResolver().openInputStream(uri); + return Drawable.createFromResourceStream( + mUseCorrectStreamDensity ? getResources() : null, null, stream, null); + } catch (Exception e) { + Log.w(LOG_TAG, "Unable to open content: " + uri, e); + } finally { + if (stream != null) { + try { + stream.close(); + } catch (IOException e) { + Log.w(LOG_TAG, "Unable to close content: " + uri, e); + } + } + } + } else { + return Drawable.createFromPath(uri.toString()); + } + return null; + } + @Override public int[] onCreateDrawableState(int extraSpace) { if (mState == null) { diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java index 0dd803a23d308..dee25d35364c5 100644 --- a/core/java/android/widget/RemoteViews.java +++ b/core/java/android/widget/RemoteViews.java @@ -39,8 +39,10 @@ import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; import android.net.Uri; +import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; +import android.os.CancellationSignal; import android.os.Parcel; import android.os.Parcelable; import android.os.StrictMode; @@ -66,6 +68,7 @@ import java.lang.annotation.Target; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.HashMap; +import java.util.concurrent.Executor; /** * A class that describes a view hierarchy that can be displayed in @@ -146,6 +149,8 @@ public class RemoteViews implements Parcelable, Filter { private static final Object[] sMethodsLock = new Object[0]; private static final ArrayMap, ArrayMap>, Method>> sMethods = new ArrayMap, ArrayMap>, Method>>(); + private static final ArrayMap sAsyncMethods = new ArrayMap<>(); + private static final ThreadLocal sInvokeArgsTls = new ThreadLocal() { @Override protected Object[] initialValue() { @@ -293,9 +298,39 @@ public class RemoteViews implements Parcelable, Filter { return (getActionName() + viewId); } + /** + * This is called on the background thread. It should perform any non-ui computations + * and return the final action which will run on the UI thread. + * Override this if some of the tasks can be performed async. + */ + public Action initActionAsync(ViewTree root, ViewGroup rootParent, OnClickHandler handler) { + return this; + } + int viewId; } + /** + * Action class used during async inflation of RemoteViews. Subclasses are not parcelable. + */ + private static abstract class RuntimeAction extends Action { + @Override + public final String getActionName() { + return "RuntimeAction"; + } + + @Override + public final void writeToParcel(Parcel dest, int flags) { + throw new UnsupportedOperationException(); + } + } + + // Constant used during async execution. It is not parcelable. + private static final Action ACTION_NOOP = new RuntimeAction() { + @Override + public void apply(View root, ViewGroup rootParent, OnClickHandler handler) { } + }; + /** * Merges the passed RemoteViews actions with this RemoteViews actions according to * action-specific merge rules. @@ -810,6 +845,36 @@ public class RemoteViews implements Parcelable, Filter { return method; } + /** + * @return the async implementation of the provided method. + */ + private Method getAsyncMethod(Method method) { + synchronized (sAsyncMethods) { + int valueIndex = sAsyncMethods.indexOfKey(method); + if (valueIndex >= 0) { + return sAsyncMethods.valueAt(valueIndex); + } + + RemotableViewMethod annotation = method.getAnnotation(RemotableViewMethod.class); + Method asyncMethod = null; + if (!annotation.asyncImpl().isEmpty()) { + try { + asyncMethod = method.getDeclaringClass() + .getMethod(annotation.asyncImpl(), method.getParameterTypes()); + if (!asyncMethod.getReturnType().equals(Runnable.class)) { + throw new ActionException("Async implementation for " + method.getName() + + " does not return a Runnable"); + } + } catch (NoSuchMethodException ex) { + throw new ActionException("Async implementation declared but not defined for " + + method.getName()); + } + } + sAsyncMethods.put(method, asyncMethod); + return asyncMethod; + } + } + private static String getParameters(Class paramType) { if (paramType == null) return "()"; return "(" + paramType + ")"; @@ -1324,6 +1389,37 @@ public class RemoteViews implements Parcelable, Filter { } } + @Override + public Action initActionAsync(ViewTree root, ViewGroup rootParent, OnClickHandler handler) { + final View view = root.findViewById(viewId); + if (view == null) return ACTION_NOOP; + + Class param = getParameterType(); + if (param == null) { + throw new ActionException("bad type: " + this.type); + } + + try { + Method method = getMethod(view, this.methodName, param); + Method asyncMethod = getAsyncMethod(method); + + if (asyncMethod != null) { + Runnable endAction = (Runnable) asyncMethod.invoke(view, wrapArg(this.value)); + if (endAction == null) { + return ACTION_NOOP; + } else { + return new RunnableAction(endAction); + } + } + } catch (ActionException e) { + throw e; + } catch (Exception ex) { + throw new ActionException(ex); + } + + return this; + } + public int mergeBehavior() { // smoothScrollBy is cumulative, everything else overwites. if (methodName.equals("smoothScrollBy")) { @@ -1340,6 +1436,22 @@ public class RemoteViews implements Parcelable, Filter { } } + /** + * This is only used for async execution of actions and it not parcelable. + */ + private static final class RunnableAction extends RuntimeAction { + private final Runnable mRunnable; + + RunnableAction(Runnable r) { + mRunnable = r; + } + + @Override + public void apply(View root, ViewGroup rootParent, OnClickHandler handler) { + mRunnable.run(); + } + } + private void configureRemoteViewsAsChild(RemoteViews rv) { mBitmapCache.assimilate(rv.mBitmapCache); rv.setBitmapCache(mBitmapCache); @@ -1400,6 +1512,43 @@ public class RemoteViews implements Parcelable, Filter { } } + @Override + public Action initActionAsync(ViewTree root, ViewGroup rootParent, OnClickHandler handler) { + // In the async implementation, update the view tree so that subsequent calls to + // findViewById return the currect view. + root.createTree(); + ViewTree target = root.findViewTreeById(viewId); + if ((target == null) || !(target.mRoot instanceof ViewGroup)) { + return ACTION_NOOP; + } + if (nestedViews == null) { + // Clear all children when nested views omitted + target.mChildren = null; + return this; + } else { + // Inflate nested views and perform all the async tasks for the child remoteView. + final Context context = root.mRoot.getContext(); + final AsyncApplyTask task = nestedViews.getAsyncApplyTask( + context, (ViewGroup) target.mRoot, null, handler); + final ViewTree tree = task.doInBackground(); + + // Update the global view tree, so that next call to findViewTreeById + // goes through the subtree as well. + target.addChild(tree); + + return new RuntimeAction() { + + @Override + public void apply(View root, ViewGroup rootParent, OnClickHandler handler) throws ActionException { + // This view will exist as we have already made sure + final ViewGroup target = (ViewGroup) root.findViewById(viewId); + task.onPostExecute(tree); + target.addView(task.mResult); + } + }; + } + } + @Override public void updateMemoryUsageEstimate(MemoryUsageCounter counter) { if (nestedViews != null) { @@ -1520,7 +1669,13 @@ public class RemoteViews implements Parcelable, Filter { public void apply(View root, ViewGroup rootParent, OnClickHandler handler) { final TextView target = (TextView) root.findViewById(viewId); if (target == null) return; - if (useIcons) { + if (drawablesLoaded) { + if (isRelative) { + target.setCompoundDrawablesRelativeWithIntrinsicBounds(id1, id2, id3, id4); + } else { + target.setCompoundDrawablesWithIntrinsicBounds(id1, id2, id3, id4); + } + } else if (useIcons) { final Context ctx = target.getContext(); final Drawable id1 = i1 == null ? null : i1.loadDrawable(ctx); final Drawable id2 = i2 == null ? null : i2.loadDrawable(ctx); @@ -1540,6 +1695,33 @@ public class RemoteViews implements Parcelable, Filter { } } + @Override + public Action initActionAsync(ViewTree root, ViewGroup rootParent, OnClickHandler handler) { + final TextView target = (TextView) root.findViewById(viewId); + if (target == null) return ACTION_NOOP; + + TextViewDrawableAction copy = useIcons ? + new TextViewDrawableAction(viewId, isRelative, i1, i2, i3, i4) : + new TextViewDrawableAction(viewId, isRelative, d1, d2, d3, d4); + + // Load the drawables on the background thread. + copy.drawablesLoaded = true; + final Context ctx = target.getContext(); + + if (useIcons) { + copy.id1 = i1 == null ? null : i1.loadDrawable(ctx); + copy.id2 = i2 == null ? null : i2.loadDrawable(ctx); + copy.id3 = i3 == null ? null : i3.loadDrawable(ctx); + copy.id4 = i4 == null ? null : i4.loadDrawable(ctx); + } else { + copy.id1 = d1 == 0 ? null : ctx.getDrawable(d1); + copy.id2 = d2 == 0 ? null : ctx.getDrawable(d2); + copy.id3 = d3 == 0 ? null : ctx.getDrawable(d3); + copy.id4 = d4 == 0 ? null : ctx.getDrawable(d4); + } + return copy; + } + public String getActionName() { return "TextViewDrawableAction"; } @@ -1549,6 +1731,9 @@ public class RemoteViews implements Parcelable, Filter { int d1, d2, d3, d4; Icon i1, i2, i3, i4; + boolean drawablesLoaded = false; + Drawable id1, id2, id3, id4; + public final static int TAG = 11; } @@ -2852,7 +3037,15 @@ public class RemoteViews implements Parcelable, Filter { public View apply(Context context, ViewGroup parent, OnClickHandler handler) { RemoteViews rvToApply = getRemoteViewsToApply(context); - View result; + View result = inflateView(context, rvToApply, parent); + loadTransitionOverride(context, handler); + + rvToApply.performApply(result, parent, handler); + + return result; + } + + private View inflateView(Context context, RemoteViews rv, ViewGroup parent) { // RemoteViews may be built by an application installed in another // user. So build a context that loads resources from that user but // still returns the current users userId so settings like data / time formats @@ -2880,13 +3073,7 @@ public class RemoteViews implements Parcelable, Filter { // we don't add a filter to the static version returned by getSystemService. inflater = inflater.cloneInContext(inflationContext); inflater.setFilter(this); - result = inflater.inflate(rvToApply.getLayoutId(), parent, false); - - loadTransitionOverride(context, handler); - - rvToApply.performApply(result, parent, handler); - - return result; + return inflater.inflate(rv.getLayoutId(), parent, false); } private static void loadTransitionOverride(Context context, @@ -2907,6 +3094,143 @@ public class RemoteViews implements Parcelable, Filter { } } + /** + * Implement this interface to receive a callback when + * {@link #applyAsync} or {@link #reapplyAsync} is finished. + * @hide + */ + public interface OnViewAppliedListener { + void onViewApplied(View v); + + void onError(Exception e); + } + + /** + * Applies the views asynchronously, moving as much of the task on the background + * thread as possible. + * + * @see {@link #apply(Context, ViewGroup)} + * @param context Default context to use + * @param parent Parent that the resulting view hierarchy will be attached to. This method + * does not attach the hierarchy. The caller should do so when appropriate. + * @param listener the callback to run when all actions have been applied. May be null. + * @param executor The executor to use. If null {@link AsyncTask#THREAD_POOL_EXECUTOR} is used. + * @return CancellationSignal + * @hide + */ + public CancellationSignal applyAsync( + Context context, ViewGroup parent, Executor executor, OnViewAppliedListener listener) { + return applyAsync(context, parent, executor, listener, null); + } + + private CancellationSignal startTaskOnExecutor(AsyncApplyTask task, Executor executor) { + CancellationSignal cancelSignal = new CancellationSignal(); + cancelSignal.setOnCancelListener(task); + + task.executeOnExecutor(executor == null ? AsyncTask.THREAD_POOL_EXECUTOR : executor); + return cancelSignal; + } + + /** @hide */ + public CancellationSignal applyAsync(Context context, ViewGroup parent, + Executor executor, OnViewAppliedListener listener, OnClickHandler handler) { + return startTaskOnExecutor(getAsyncApplyTask(context, parent, listener, handler), executor); + } + + private AsyncApplyTask getAsyncApplyTask(Context context, ViewGroup parent, + OnViewAppliedListener listener, OnClickHandler handler) { + return new AsyncApplyTask(getRemoteViewsToApply(context), parent, context, listener, + handler, null); + } + + private class AsyncApplyTask extends AsyncTask + implements CancellationSignal.OnCancelListener { + final RemoteViews mRV; + final ViewGroup mParent; + final Context mContext; + final OnViewAppliedListener mListener; + final OnClickHandler mHandler; + + private View mResult; + private ViewTree mTree; + private Action[] mActions; + private Exception mError; + + private AsyncApplyTask( + RemoteViews rv, ViewGroup parent, Context context, OnViewAppliedListener listener, + OnClickHandler handler, View result) { + mRV = rv; + mParent = parent; + mContext = context; + mListener = listener; + mHandler = handler; + + mResult = result; + loadTransitionOverride(context, handler); + } + + @Override + protected ViewTree doInBackground(Void... params) { + try { + if (mResult == null) { + mResult = inflateView(mContext, mRV, mParent); + } + + mTree = new ViewTree(mResult); + if (mRV.mActions != null) { + int count = mRV.mActions.size(); + mActions = new Action[count]; + for (int i = 0; i < count && !isCancelled(); i++) { + // TODO: check if isCanclled in nested views. + mActions[i] = mRV.mActions.get(i).initActionAsync(mTree, mParent, mHandler); + } + } else { + mActions = null; + } + return mTree; + } catch (Exception e) { + mError = e; + return null; + } + } + + @Override + protected void onPostExecute(ViewTree viewTree) { + if (mError == null) { + try { + if (mActions != null) { + OnClickHandler handler = mHandler == null + ? DEFAULT_ON_CLICK_HANDLER : mHandler; + for (Action a : mActions) { + a.apply(viewTree.mRoot, mParent, handler); + } + } + } catch (Exception e) { + mError = e; + } + } + + if (mListener != null) { + if (mError != null) { + mListener.onError(mError); + } else { + mListener.onViewApplied(viewTree.mRoot); + } + } else if (mError != null) { + if (mError instanceof ActionException) { + throw (ActionException) mError; + } else { + throw new ActionException(mError); + } + } + } + + @Override + public void onCancel() { + cancel(true); + } + } + /** * Applies all of the actions to the provided view. * @@ -2936,6 +3260,43 @@ public class RemoteViews implements Parcelable, Filter { rvToApply.performApply(v, (ViewGroup) v.getParent(), handler); } + /** + * Applies all the actions to the provided view, moving as much of the task on the background + * thread as possible. + * + * @see {@link #reapply(Context, View)} + * @param context Default context to use + * @param v The view to apply the actions to. This should be the result of + * the {@link #apply(Context,ViewGroup)} call. + * @param listener the callback to run when all actions have been applied. May be null. + * @param executor The executor to use. If null {@link AsyncTask#THREAD_POOL_EXECUTOR} is used + * @return CancellationSignal + * @hide + */ + public CancellationSignal reapplyAsync( + Context context, View v, Executor executor, OnViewAppliedListener listener) { + return reapplyAsync(context, v, executor, listener, null); + } + + /** @hide */ + public CancellationSignal reapplyAsync(Context context, View v, Executor executor, + OnViewAppliedListener listener, OnClickHandler handler) { + RemoteViews rvToApply = getRemoteViewsToApply(context); + + // In the case that a view has this RemoteViews applied in one orientation, is persisted + // across orientation change, and has the RemoteViews re-applied in the new orientation, + // we throw an exception, since the layouts may be completely unrelated. + if (hasLandscapeAndPortraitLayouts()) { + if (v.getId() != rvToApply.getLayoutId()) { + throw new RuntimeException("Attempting to re-apply RemoteViews to a view that" + + " that does not share the same root layout id."); + } + } + + return startTaskOnExecutor(new AsyncApplyTask(rvToApply, (ViewGroup) v.getParent(), + context, listener, handler, v), executor); + } + private void performApply(View v, ViewGroup parent, OnClickHandler handler) { if (mActions != null) { handler = handler == null ? DEFAULT_ON_CLICK_HANDLER : handler; @@ -3058,4 +3419,90 @@ public class RemoteViews implements Parcelable, Filter { return new RemoteViews[size]; } }; + + /** + * A representation of the view hierarchy. Only views which have a valid ID are added + * and can be searched. + */ + private static class ViewTree { + private final View mRoot; + + private ArrayList mChildren; + + private ViewTree(View root) { + mRoot = root; + } + + public void createTree() { + if (mChildren != null) { + return; + } + + mChildren = new ArrayList<>(); + if (mRoot instanceof ViewGroup && mRoot.isRootNamespace()) { + ViewGroup vg = (ViewGroup) mRoot; + int count = vg.getChildCount(); + for (int i = 0; i < count; i++) { + addViewChild(vg.getChildAt(i)); + } + } + } + + public ViewTree findViewTreeById(int id) { + if (mRoot.getId() == id) { + return this; + } + if (mChildren == null) { + return null; + } + for (ViewTree tree : mChildren) { + ViewTree result = tree.findViewTreeById(id); + if (result != null) { + return result; + } + } + return null; + } + + public View findViewById(int id) { + if (mChildren == null) { + return mRoot.findViewById(id); + } + ViewTree tree = findViewTreeById(id); + return tree == null ? null : tree.mRoot; + } + + public void addChild(ViewTree child) { + if (mChildren == null) { + mChildren = new ArrayList<>(); + } + child.createTree(); + mChildren.add(child); + } + + private void addViewChild(View v) { + final ViewTree target; + + // If the view has a valid id, i.e., if can be found using findViewById, add it to the + // tree, otherwise skip this view and add its children instead. + if (v.getId() != 0) { + ViewTree tree = new ViewTree(v); + mChildren.add(tree); + target = tree; + } else { + target = this; + } + + if (v instanceof ViewGroup && v.isRootNamespace()) { + if (target.mChildren == null) { + target.mChildren = new ArrayList<>(); + ViewGroup vg = (ViewGroup) v; + int count = vg.getChildCount(); + for (int i = 0; i < count; i++) { + target.addViewChild(vg.getChildAt(i)); + } + } + } + } + } }