Merge "Added support for async inflation of RemoteViews"

This commit is contained in:
Sunny Goyal
2016-02-03 19:31:09 +00:00
committed by Android (Google) Code Review
4 changed files with 642 additions and 45 deletions

View File

@@ -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.
*/
@@ -339,6 +345,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.
@@ -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.

View File

@@ -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 "";
}

View File

@@ -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) {

View File

@@ -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,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 <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.
*
@@ -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));
}
}
}
}
}
}