diff options
| author | Sunny Goyal <sunnygoyal@google.com> | 2015-12-02 14:29:27 -0800 |
|---|---|---|
| committer | Sunny Goyal <sunnygoyal@google.com> | 2016-01-11 15:15:42 -0800 |
| commit | dd292f4fab52f99d0b15fcf961864cd8186d662c (patch) | |
| tree | 84a5ddd3f92dbb697a9f310b2845ab5570bb40c9 /core/java/android/widget/RemoteViews.java | |
| parent | 5a5d6aeec0ee6b3f9ffea55a4f015edc84828b1e (diff) | |
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
Diffstat (limited to 'core/java/android/widget/RemoteViews.java')
| -rw-r--r-- | core/java/android/widget/RemoteViews.java | 465 |
1 files changed, 456 insertions, 9 deletions
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)); + } + } + } + } + } } |
