Merge "Adding support for async view loading in RemoteViewsAdapter"

This commit is contained in:
Sunny Goyal
2016-10-27 17:14:43 +00:00
committed by Android (Google) Code Review
8 changed files with 224 additions and 77 deletions

View File

@@ -377,13 +377,13 @@ public class AppWidgetHostView extends FrameLayout {
* AppWidget provider. Will animate into these new views as needed
*/
public void updateAppWidget(RemoteViews remoteViews) {
applyRemoteViews(remoteViews);
applyRemoteViews(remoteViews, true);
}
/**
* @hide
*/
protected void applyRemoteViews(RemoteViews remoteViews) {
protected void applyRemoteViews(RemoteViews remoteViews, boolean useAsyncIfPossible) {
if (LOGD) Log.d(TAG, "updateAppWidget called mOld=" + mOld);
boolean recycled = false;
@@ -423,7 +423,7 @@ public class AppWidgetHostView extends FrameLayout {
mLayoutId = -1;
mViewMode = VIEW_MODE_DEFAULT;
} else {
if (mAsyncExecutor != null) {
if (mAsyncExecutor != null && useAsyncIfPossible) {
inflateAsync(remoteViews);
return;
}

View File

@@ -6284,6 +6284,17 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te
* @param intent the intent used to identify the RemoteViewsService for the adapter to connect to.
*/
public void setRemoteViewsAdapter(Intent intent) {
setRemoteViewsAdapter(intent, false);
}
/** @hide **/
public Runnable setRemoteViewsAdapterAsync(final Intent intent) {
return new RemoteViewsAdapter.AsyncRemoteAdapterAction(this, intent);
}
/** @hide **/
@Override
public void setRemoteViewsAdapter(Intent intent, boolean isAsync) {
// Ensure that we don't already have a RemoteViewsAdapter that is bound to an existing
// service handling the specified intent.
if (mRemoteAdapter != null) {
@@ -6296,7 +6307,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te
}
mDeferNotifyDataSetChanged = false;
// Otherwise, create a new RemoteViewsAdapter for binding
mRemoteAdapter = new RemoteViewsAdapter(getContext(), intent, this);
mRemoteAdapter = new RemoteViewsAdapter(getContext(), intent, this, isAsync);
if (mRemoteAdapter.isDataReady()) {
setAdapter(mRemoteAdapter);
}

View File

@@ -975,8 +975,19 @@ public abstract class AdapterViewAnimator extends AdapterView<Adapter>
* @param intent the intent used to identify the RemoteViewsService for the adapter to
* connect to.
*/
@android.view.RemotableViewMethod
@android.view.RemotableViewMethod(asyncImpl="setRemoteViewsAdapterAsync")
public void setRemoteViewsAdapter(Intent intent) {
setRemoteViewsAdapter(intent, false);
}
/** @hide **/
public Runnable setRemoteViewsAdapterAsync(final Intent intent) {
return new RemoteViewsAdapter.AsyncRemoteAdapterAction(this, intent);
}
/** @hide **/
@Override
public void setRemoteViewsAdapter(Intent intent, boolean isAsync) {
// Ensure that we don't already have a RemoteViewsAdapter that is bound to an existing
// service handling the specified intent.
if (mRemoteViewsAdapter != null) {
@@ -989,7 +1000,7 @@ public abstract class AdapterViewAnimator extends AdapterView<Adapter>
}
mDeferNotifyDataSetChanged = false;
// Otherwise, create a new RemoteViewsAdapter for binding
mRemoteViewsAdapter = new RemoteViewsAdapter(getContext(), intent, this);
mRemoteViewsAdapter = new RemoteViewsAdapter(getContext(), intent, this, isAsync);
if (mRemoteViewsAdapter.isDataReady()) {
setAdapter(mRemoteViewsAdapter);
}

View File

@@ -175,7 +175,7 @@ public class GridView extends AbsListView {
* through the specified intent.
* @param intent the intent used to identify the RemoteViewsService for the adapter to connect to.
*/
@android.view.RemotableViewMethod
@android.view.RemotableViewMethod(asyncImpl="setRemoteViewsAdapterAsync")
public void setRemoteViewsAdapter(Intent intent) {
super.setRemoteViewsAdapter(intent);
}

View File

@@ -455,7 +455,16 @@ public class ImageView extends View {
/** @hide **/
public Runnable setImageResourceAsync(@DrawableRes int resId) {
return new ImageDrawableCallback(getContext().getDrawable(resId), null, resId);
Drawable d = null;
if (resId != 0) {
try {
d = getContext().getDrawable(resId);
} catch (Exception e) {
Log.w(LOG_TAG, "Unable to find resource: " + resId, e);
resId = 0;
}
}
return new ImageDrawableCallback(d, null, resId);
}
/**
@@ -865,7 +874,7 @@ public class ImageView extends View {
} catch (Exception e) {
Log.w(LOG_TAG, "Unable to find resource: " + mResource, e);
// Don't try again.
mUri = null;
mResource = 0;
}
} else if (mUri != null) {
d = getDrawableFromUri(mUri);

View File

@@ -448,7 +448,7 @@ public class ListView extends AbsListView {
* through the specified intent.
* @param intent the intent used to identify the RemoteViewsService for the adapter to connect to.
*/
@android.view.RemotableViewMethod
@android.view.RemotableViewMethod(asyncImpl="setRemoteViewsAdapterAsync")
public void setRemoteViewsAdapter(Intent intent) {
super.setRemoteViewsAdapter(intent);
}

View File

@@ -320,6 +320,10 @@ public class RemoteViews implements Parcelable, Filter {
return this;
}
public boolean prefersAsyncApply() {
return false;
}
int viewId;
}
@@ -715,20 +719,29 @@ public class RemoteViews implements Parcelable, Filter {
intent.putExtra(EXTRA_REMOTEADAPTER_APPWIDGET_ID, host.getAppWidgetId());
if (target instanceof AbsListView) {
AbsListView v = (AbsListView) target;
v.setRemoteViewsAdapter(intent);
v.setRemoteViewsAdapter(intent, isAsync);
v.setRemoteViewsOnClickHandler(handler);
} else if (target instanceof AdapterViewAnimator) {
AdapterViewAnimator v = (AdapterViewAnimator) target;
v.setRemoteViewsAdapter(intent);
v.setRemoteViewsAdapter(intent, isAsync);
v.setRemoteViewsOnClickHandler(handler);
}
}
@Override
public Action initActionAsync(ViewTree root, ViewGroup rootParent,
OnClickHandler handler) {
SetRemoteViewsAdapterIntent copy = new SetRemoteViewsAdapterIntent(viewId, intent);
copy.isAsync = true;
return copy;
}
public String getActionName() {
return "SetRemoteViewsAdapterIntent";
}
Intent intent;
boolean isAsync = false;
public final static int TAG = 10;
}
@@ -1461,6 +1474,11 @@ public class RemoteViews implements Parcelable, Filter {
// unique from the standpoint of merging.
return "ReflectionAction" + this.methodName + this.type;
}
@Override
public boolean prefersAsyncApply() {
return this.type == URI || this.type == ICON;
}
}
/**
@@ -1598,6 +1616,11 @@ public class RemoteViews implements Parcelable, Filter {
return MERGE_APPEND;
}
@Override
public boolean prefersAsyncApply() {
return nestedViews != null && nestedViews.prefersAsyncApply();
}
RemoteViews nestedViews;
public final static int TAG = 4;
@@ -1749,6 +1772,11 @@ public class RemoteViews implements Parcelable, Filter {
return copy;
}
@Override
public boolean prefersAsyncApply() {
return useIcons;
}
public String getActionName() {
return "TextViewDrawableAction";
}
@@ -3443,6 +3471,24 @@ public class RemoteViews implements Parcelable, Filter {
}
}
/**
* Returns true if the RemoteViews contains potentially costly operations and should be
* applied asynchronously.
*
* @hide
*/
public boolean prefersAsyncApply() {
if (mActions != null) {
final int count = mActions.size();
for (int i = 0; i < count; i++) {
if (mActions.get(i).prefersAsyncApply()) {
return true;
}
}
}
return false;
}
private Context getContextForResources(Context context) {
if (mApplication != null) {
if (context.getUserId() == UserHandle.getUserId(mApplication.uid)

View File

@@ -41,6 +41,12 @@ import android.widget.RemoteViews.OnClickHandler;
import com.android.internal.widget.IRemoteViewsAdapterConnection;
import com.android.internal.widget.IRemoteViewsFactory;
import java.lang.ref.WeakReference;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.concurrent.Executor;
import java.lang.ref.WeakReference;
import java.util.Arrays;
import java.util.HashMap;
@@ -73,7 +79,8 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback
private final Context mContext;
private final Intent mIntent;
private final int mAppWidgetId;
private LayoutInflater mLayoutInflater;
private final Executor mAsyncViewLoadExecutor;
private RemoteViewsAdapterServiceConnection mServiceConnection;
private WeakReference<RemoteAdapterConnectionCallback> mCallback;
private OnClickHandler mRemoteViewsOnClickHandler;
@@ -120,15 +127,33 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback
/**
* @return whether the adapter was set or not.
*/
public boolean onRemoteAdapterConnected();
boolean onRemoteAdapterConnected();
public void onRemoteAdapterDisconnected();
void onRemoteAdapterDisconnected();
/**
* This defers a notifyDataSetChanged on the pending RemoteViewsAdapter if it has not
* connected yet.
*/
public void deferNotifyDataSetChanged();
void deferNotifyDataSetChanged();
void setRemoteViewsAdapter(Intent intent, boolean isAsync);
}
public static class AsyncRemoteAdapterAction implements Runnable {
private final RemoteAdapterConnectionCallback mCallback;
private final Intent mIntent;
public AsyncRemoteAdapterAction(RemoteAdapterConnectionCallback callback, Intent intent) {
mCallback = callback;
mIntent = intent;
}
@Override
public void run() {
mCallback.setRemoteViewsAdapter(mIntent, true);
}
}
/**
@@ -162,7 +187,7 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback
}
mIsConnecting = true;
} catch (Exception e) {
Log.e("RemoteViewsAdapterServiceConnection", "bind(): " + e.getMessage());
Log.e("RVAServiceConnection", "bind(): " + e.getMessage());
mIsConnecting = false;
mIsConnected = false;
}
@@ -180,7 +205,7 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback
}
mIsConnecting = false;
} catch (Exception e) {
Log.e("RemoteViewsAdapterServiceConnection", "unbind(): " + e.getMessage());
Log.e("RVAServiceConnection", "unbind(): " + e.getMessage());
mIsConnecting = false;
mIsConnected = false;
}
@@ -298,15 +323,29 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback
* Updates this RemoteViewsFrameLayout depending on the view that was loaded.
* @param view the RemoteViews that was loaded. If null, the RemoteViews was not loaded
* successfully.
* @param forceApplyAsync when true, the host will always try to inflate the view
* asynchronously (for eg, when we are already showing the loading
* view)
*/
public void onRemoteViewsLoaded(RemoteViews view, OnClickHandler handler) {
public void onRemoteViewsLoaded(RemoteViews view, OnClickHandler handler,
boolean forceApplyAsync) {
setOnClickHandler(handler);
applyRemoteViews(view);
applyRemoteViews(view, forceApplyAsync || ((view != null) && view.prefersAsyncApply()));
}
/**
* Creates a default loading view. Uses the size of the first row as a guide for the
* size of the loading view.
*/
@Override
protected View getDefaultView() {
return mCache.getMetaData().createDefaultLoadingView(this);
int viewHeight = mCache.getMetaData().getLoadingTemplate(getContext()).defaultHeight;
// Compose the loading view text
TextView loadingTextView = (TextView) LayoutInflater.from(getContext()).inflate(
com.android.internal.R.layout.remote_views_adapter_default_loading_view,
this, false);
loadingTextView.setHeight(viewHeight);
return loadingTextView;
}
@Override
@@ -359,7 +398,7 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback
if (refs != null) {
// Notify all the references for that position of the newly loaded RemoteViews
for (final RemoteViewsFrameLayout ref : refs) {
ref.onRemoteViewsLoaded(view, mRemoteViewsOnClickHandler);
ref.onRemoteViewsLoaded(view, mRemoteViewsOnClickHandler, true);
if (mViewToLinkedList.containsKey(ref)) {
mViewToLinkedList.remove(ref);
}
@@ -402,9 +441,7 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback
// Used to determine how to construct loading views. If a loading view is not specified
// by the user, then we try and load the first view, and use its height as the height for
// the default loading view.
RemoteViews mUserLoadingView;
RemoteViews mFirstView;
int mFirstViewHeight;
LoadingViewTemplate loadingTemplate;
// A mapping from type id to a set of unique type ids
private final SparseIntArray mTypeIdIndexMap = new SparseIntArray();
@@ -418,7 +455,7 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback
count = d.count;
viewTypeCount = d.viewTypeCount;
hasStableIds = d.hasStableIds;
setLoadingViewTemplates(d.mUserLoadingView, d.mFirstView);
loadingTemplate = d.loadingTemplate;
}
}
@@ -428,20 +465,10 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback
// by default there is at least one dummy view type
viewTypeCount = 1;
hasStableIds = true;
mUserLoadingView = null;
mFirstView = null;
mFirstViewHeight = 0;
loadingTemplate = null;
mTypeIdIndexMap.clear();
}
public void setLoadingViewTemplates(RemoteViews loadingView, RemoteViews firstView) {
mUserLoadingView = loadingView;
if (firstView != null) {
mFirstView = firstView;
mFirstViewHeight = -1;
}
}
public int getMappedViewType(int typeId) {
int mappedTypeId = mTypeIdIndexMap.get(typeId, -1);
if (mappedTypeId == -1) {
@@ -457,33 +484,11 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback
return (mappedType < viewTypeCount);
}
/**
* Creates a default loading view. Uses the size of the first row as a guide for the
* size of the loading view.
*/
private synchronized View createDefaultLoadingView(ViewGroup parent) {
final Context context = parent.getContext();
if (mFirstViewHeight < 0) {
try {
View firstView = mFirstView.apply(parent.getContext(), parent);
firstView.measure(
MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
mFirstViewHeight = firstView.getMeasuredHeight();
} catch (Exception e) {
float density = context.getResources().getDisplayMetrics().density;
mFirstViewHeight = Math.round(sDefaultLoadingViewHeight * density);
Log.w(TAG, "Error inflating first RemoteViews" + e);
}
mFirstView = null;
public synchronized LoadingViewTemplate getLoadingTemplate(Context context) {
if (loadingTemplate == null) {
loadingTemplate = new LoadingViewTemplate(null, context);
}
// Compose the loading view text
TextView loadingTextView = (TextView) LayoutInflater.from(context).inflate(
com.android.internal.R.layout.remote_views_adapter_default_loading_view,
parent, false);
loadingTextView.setHeight(mFirstViewHeight);
return loadingTextView;
return loadingTemplate;
}
}
@@ -772,7 +777,7 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback
}
public RemoteViewsAdapter(Context context, Intent intent,
RemoteAdapterConnectionCallback callback) {
RemoteAdapterConnectionCallback callback, boolean useAsyncLoader) {
mContext = context;
mIntent = intent;
@@ -781,7 +786,6 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback
}
mAppWidgetId = intent.getIntExtra(RemoteViews.EXTRA_REMOTEADAPTER_APPWIDGET_ID, -1);
mLayoutInflater = LayoutInflater.from(context);
mRequestedViews = new RemoteViewsFrameLayoutRefSet();
// Strip the previously injected app widget id from service intent
@@ -794,6 +798,7 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback
mWorkerThread.start();
mWorkerQueue = new Handler(mWorkerThread.getLooper());
mMainQueue = new Handler(Looper.myLooper(), this);
mAsyncViewLoadExecutor = useAsyncLoader ? new HandlerThreadExecutor(mWorkerThread) : null;
if (sCacheRemovalThread == null) {
sCacheRemovalThread = new HandlerThread("RemoteViewsAdapter-cachePruner");
@@ -941,10 +946,14 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback
boolean hasStableIds = factory.hasStableIds();
int viewTypeCount = factory.getViewTypeCount();
int count = factory.getCount();
RemoteViews loadingView = factory.getLoadingView();
RemoteViews firstView = null;
if ((count > 0) && (loadingView == null)) {
firstView = factory.getViewAt(0);
LoadingViewTemplate loadingTemplate =
new LoadingViewTemplate(factory.getLoadingView(), mContext);
if ((count > 0) && (loadingTemplate.remoteViews == null)) {
RemoteViews firstView = factory.getViewAt(0);
if (firstView != null) {
loadingTemplate.loadFirstViewHeight(firstView, mContext,
new HandlerThreadExecutor(mWorkerThread));
}
}
final RemoteViewsMetaData tmpMetaData = mCache.getTemporaryMetaData();
synchronized (tmpMetaData) {
@@ -952,7 +961,7 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback
// We +1 because the base view type is the loading view
tmpMetaData.viewTypeCount = viewTypeCount + 1;
tmpMetaData.count = count;
tmpMetaData.setLoadingViewTemplates(loadingView, firstView);
tmpMetaData.loadingTemplate = loadingTemplate;
}
} catch(RemoteException e) {
processException("updateMetaData", e);
@@ -1100,18 +1109,25 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback
hasNewItems = mCache.queuePositionsToBePreloadedFromRequestedPosition(position);
}
final RemoteViewsFrameLayout layout =
(convertView instanceof RemoteViewsFrameLayout)
? (RemoteViewsFrameLayout) convertView
: new RemoteViewsFrameLayout(parent.getContext(), mCache);
final RemoteViewsFrameLayout layout;
if (convertView instanceof RemoteViewsFrameLayout) {
layout = (RemoteViewsFrameLayout) convertView;
} else {
layout = new RemoteViewsFrameLayout(parent.getContext(), mCache);
layout.setAsyncExecutor(mAsyncViewLoadExecutor);
}
if (isInCache) {
layout.onRemoteViewsLoaded(rv, mRemoteViewsOnClickHandler);
// Apply the view synchronously if possible, to avoid flickering
layout.onRemoteViewsLoaded(rv, mRemoteViewsOnClickHandler, false);
if (hasNewItems) loadNextIndexInBackground();
} else {
// If the views is not loaded, apply the loading view. If the loading view doesn't
// exist, the layout will create a default view based on the firstView height.
layout.onRemoteViewsLoaded(mCache.getMetaData().mUserLoadingView,
mRemoteViewsOnClickHandler);
layout.onRemoteViewsLoaded(
mCache.getMetaData().getLoadingTemplate(mContext).remoteViews,
mRemoteViewsOnClickHandler,
false);
mRequestedViews.add(position, layout);
mCache.queueRequestedPositionToLoad(position);
loadNextIndexInBackground();
@@ -1285,4 +1301,58 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback
mMainQueue.removeMessages(sUnbindServiceMessageType);
return mServiceConnection.isConnected();
}
private static class HandlerThreadExecutor implements Executor {
private final HandlerThread mThread;
HandlerThreadExecutor(HandlerThread thread) {
mThread = thread;
}
@Override
public void execute(Runnable runnable) {
if (Thread.currentThread().getId() == mThread.getId()) {
runnable.run();
} else {
new Handler(mThread.getLooper()).post(runnable);
}
}
}
private static class LoadingViewTemplate {
public final RemoteViews remoteViews;
public int defaultHeight;
LoadingViewTemplate(RemoteViews views, Context context) {
remoteViews = views;
float density = context.getResources().getDisplayMetrics().density;
defaultHeight = Math.round(sDefaultLoadingViewHeight * density);
}
public void loadFirstViewHeight(
RemoteViews firstView, Context context, Executor executor) {
// Inflate the first view on the worker thread
firstView.applyAsync(context, new RemoteViewsFrameLayout(context, null), executor,
new RemoteViews.OnViewAppliedListener() {
@Override
public void onViewApplied(View v) {
try {
v.measure(
MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
defaultHeight = v.getMeasuredHeight();
} catch (Exception e) {
onError(e);
}
}
@Override
public void onError(Exception e) {
// Do nothing. The default height will stay the same.
Log.w(TAG, "Error inflating first RemoteViews", e);
}
});
}
}
}