diff options
| author | Sunny Goyal <sunnygoyal@google.com> | 2016-02-03 19:31:09 +0000 |
|---|---|---|
| committer | Android (Google) Code Review <android-gerrit@google.com> | 2016-02-03 19:31:09 +0000 |
| commit | 40dedd5a97b03408f37384ee0db7b9cbe5159a37 (patch) | |
| tree | bd91afd594c10abf98f2e81d1f471e21077c559d /core/java | |
| parent | d7595b3151f3c54809b15018ff9bd973950016de (diff) | |
| parent | dd292f4fab52f99d0b15fcf961864cd8186d662c (diff) | |
Merge "Added support for async inflation of RemoteViews"
Diffstat (limited to 'core/java')
| -rw-r--r-- | core/java/android/appwidget/AppWidgetHostView.java | 97 | ||||
| -rw-r--r-- | core/java/android/view/RemotableViewMethod.java | 6 | ||||
| -rw-r--r-- | core/java/android/widget/ImageView.java | 119 | ||||
| -rw-r--r-- | core/java/android/widget/RemoteViews.java | 465 |
4 files changed, 642 insertions, 45 deletions
diff --git a/core/java/android/appwidget/AppWidgetHostView.java b/core/java/android/appwidget/AppWidgetHostView.java index 55b8f0367885..bed91ecafb96 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; @@ -49,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 @@ -87,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. */ @@ -340,6 +346,22 @@ public class AppWidgetHostView extends FrameLayout { } /** + * 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. */ @@ -380,6 +402,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. @@ -389,6 +416,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(); @@ -421,6 +452,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. @@ -452,6 +487,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 4318290affc4..e5cae84942f9 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 6e90baf26fa2..f601f7df1fa8 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 0dd803a23d30..dee25d35364c 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<Class<? extends View>, ArrayMap<MutablePair<String, Class<?>>, Method>> sMethods = new ArrayMap<Class<? extends View>, ArrayMap<MutablePair<String, Class<?>>, Method>>(); + private static final ArrayMap<Method, Method> sAsyncMethods = new ArrayMap<>(); + private static final ThreadLocal<Object[]> sInvokeArgsTls = new ThreadLocal<Object[]>() { @Override protected Object[] initialValue() { @@ -293,10 +298,40 @@ 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); @@ -1401,6 +1513,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) { counter.increment(nestedViews.estimateMemoryUsage()); @@ -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, @@ -2908,6 +3095,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 <strong>not</strong> 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<Void, Void, ViewTree> + 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. * * <p><strong>Caller beware: this may throw</strong> @@ -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<ViewTree> 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)); + } + } + } + } + } } |
