diff options
author | Winson Chung <winsonc@google.com> | 2010-09-23 16:40:28 -0700 |
---|---|---|
committer | Winson Chung <winsonc@google.com> | 2010-09-24 17:00:30 -0700 |
commit | 3ec9a45c36d3ca5ffbc6e85bbeb497b065e14155 (patch) | |
tree | cefe7f8198b857a8d6edd038f3abdfc2c4247f10 /core/java | |
parent | 8cb9e9c5c71dabba826545b3d1fa0cd675f95100 (diff) | |
download | frameworks_base-3ec9a45c36d3ca5ffbc6e85bbeb497b065e14155.zip frameworks_base-3ec9a45c36d3ca5ffbc6e85bbeb497b065e14155.tar.gz frameworks_base-3ec9a45c36d3ca5ffbc6e85bbeb497b065e14155.tar.bz2 |
Re-architecting RemoteViewsAdapter internals due to new constraints.
- Respecting the AdapterView/Adapter contract using FrameLayout notification to update individual items
- Initial changes to allow for keeping (currently bitmap only) memory in check when loading numerous RemoteViews
- Fixing issue with leaking activities due to extra service connection references
- Fixing issue with multiple RemoteViewsAdapters being set on the same AdapterView
- Fixing small issue with StackView sometimes requesting indices out of bounds
- Removing background loading, reverting to previous message-queuing method (seems to be performant now)
Change-Id: I42313767aa791dfe35c247c97ae5d64389e6bb4c
Diffstat (limited to 'core/java')
-rw-r--r-- | core/java/android/widget/AbsListView.java | 10 | ||||
-rw-r--r-- | core/java/android/widget/AdapterViewAnimator.java | 19 | ||||
-rw-r--r-- | core/java/android/widget/RemoteViews.java | 110 | ||||
-rw-r--r-- | core/java/android/widget/RemoteViewsAdapter.java | 1169 |
4 files changed, 774 insertions, 534 deletions
diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java index bb13f1d..c694ff1 100644 --- a/core/java/android/widget/AbsListView.java +++ b/core/java/android/widget/AbsListView.java @@ -4367,9 +4367,13 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te public void setRemoteViewsAdapter(Intent intent) { // Ensure that we don't already have a RemoteViewsAdapter that is bound to an existing // service handling the specified intent. - Intent.FilterComparison fc = new Intent.FilterComparison(intent); - if (mRemoteAdapter != null && fc.equals(mRemoteAdapter.getRemoteViewsServiceIntent())) { - return; + if (mRemoteAdapter != null) { + Intent.FilterComparison fcNew = new Intent.FilterComparison(intent); + Intent.FilterComparison fcOld = new Intent.FilterComparison( + mRemoteAdapter.getRemoteViewsServiceIntent()); + if (fcNew.equals(fcOld)) { + return; + } } // Otherwise, create a new RemoteViewsAdapter for binding diff --git a/core/java/android/widget/AdapterViewAnimator.java b/core/java/android/widget/AdapterViewAnimator.java index c08adb2..f245933 100644 --- a/core/java/android/widget/AdapterViewAnimator.java +++ b/core/java/android/widget/AdapterViewAnimator.java @@ -389,7 +389,7 @@ public abstract class AdapterViewAnimator extends AdapterView<Adapter> int newWindowStartUnbounded = childIndex - mActiveOffset; int newWindowEndUnbounded = newWindowStartUnbounded + mNumActiveViews - 1; int newWindowStart = Math.max(0, newWindowStartUnbounded); - int newWindowEnd = Math.min(mAdapter.getCount(), newWindowEndUnbounded); + int newWindowEnd = Math.min(mAdapter.getCount() - 1, newWindowEndUnbounded); // This section clears out any items that are in our mActiveViews list // but are outside the effective bounds of our window (this is becomes an issue @@ -592,18 +592,18 @@ public abstract class AdapterViewAnimator extends AdapterView<Adapter> */ private SavedState(Parcel in) { super(in); - whichChild = in.readInt(); + this.whichChild = in.readInt(); } @Override public void writeToParcel(Parcel out, int flags) { super.writeToParcel(out, flags); - out.writeInt(whichChild); + out.writeInt(this.whichChild); } @Override public String toString() { - return "AdapterViewAnimator.SavedState{ whichChild = " + whichChild + " }"; + return "AdapterViewAnimator.SavedState{ whichChild = " + this.whichChild + " }"; } public static final Parcelable.Creator<SavedState> CREATOR @@ -781,10 +781,13 @@ public abstract class AdapterViewAnimator extends AdapterView<Adapter> public void setRemoteViewsAdapter(Intent intent) { // Ensure that we don't already have a RemoteViewsAdapter that is bound to an existing // service handling the specified intent. - Intent.FilterComparison fc = new Intent.FilterComparison(intent); - if (mRemoteViewsAdapter != null && - fc.equals(mRemoteViewsAdapter.getRemoteViewsServiceIntent())) { - return; + if (mRemoteViewsAdapter != null) { + Intent.FilterComparison fcNew = new Intent.FilterComparison(intent); + Intent.FilterComparison fcOld = new Intent.FilterComparison( + mRemoteViewsAdapter.getRemoteViewsServiceIntent()); + if (fcNew.equals(fcOld)) { + return; + } } // Otherwise, create a new RemoteViewsAdapter for binding diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java index 945ffeb..9d214fc 100644 --- a/core/java/android/widget/RemoteViews.java +++ b/core/java/android/widget/RemoteViews.java @@ -74,6 +74,11 @@ public class RemoteViews implements Parcelable, Filter { */ private ArrayList<Action> mActions; + /** + * A class to keep track of memory usage by this RemoteViews + */ + private MemoryUsageCounter mMemoryUsageCounter; + /** * This flag indicates whether this RemoteViews object is being created from a @@ -118,6 +123,15 @@ public class RemoteViews implements Parcelable, Filter { public int describeContents() { return 0; } + + /** + * Overridden by each class to report on it's own memory usage + */ + public void updateMemoryUsageEstimate(MemoryUsageCounter counter) { + // We currently only calculate Bitmap memory usage, so by default, don't do anything + // here + return; + } } private class SetEmptyView extends Action { @@ -525,7 +539,7 @@ public class RemoteViews implements Parcelable, Filter { } } } - + int viewId; boolean targetBackground; int alpha; @@ -817,6 +831,35 @@ public class RemoteViews implements Parcelable, Filter { throw new ActionException(ex); } } + + @Override + public void updateMemoryUsageEstimate(MemoryUsageCounter counter) { + // We currently only calculate Bitmap memory usage + switch (this.type) { + case BITMAP: + if (this.value != null) { + final Bitmap b = (Bitmap) this.value; + final Bitmap.Config c = b.getConfig(); + int bpp = 4; + switch (c) { + case ALPHA_8: + bpp = 1; + break; + case RGB_565: + case ARGB_4444: + bpp = 2; + break; + case ARGB_8888: + bpp = 4; + break; + } + counter.bitmapIncrement(b.getWidth() * b.getHeight() * bpp); + } + break; + default: + break; + } + } } /** @@ -854,6 +897,13 @@ public class RemoteViews implements Parcelable, Filter { } } + @Override + public void updateMemoryUsageEstimate(MemoryUsageCounter counter) { + if (nestedViews != null) { + counter.bitmapIncrement(nestedViews.estimateBitmapMemoryUsage()); + } + } + int viewId; RemoteViews nestedViews; @@ -861,6 +911,26 @@ public class RemoteViews implements Parcelable, Filter { } /** + * Simple class used to keep track of memory usage in a RemoteViews. + * + */ + private class MemoryUsageCounter { + public void clear() { + mBitmapHeapMemoryUsage = 0; + } + + public void bitmapIncrement(int numBytes) { + mBitmapHeapMemoryUsage += numBytes; + } + + public int getBitmapHeapMemoryUsage() { + return mBitmapHeapMemoryUsage; + } + + int mBitmapHeapMemoryUsage; + } + + /** * Create a new RemoteViews object that will display the views contained * in the specified layout file. * @@ -870,6 +940,10 @@ public class RemoteViews implements Parcelable, Filter { public RemoteViews(String packageName, int layoutId) { mPackage = packageName; mLayoutId = layoutId; + + // setup the memory usage statistics + mMemoryUsageCounter = new MemoryUsageCounter(); + recalculateMemoryUsage(); } /** @@ -920,6 +994,10 @@ public class RemoteViews implements Parcelable, Filter { } } } + + // setup the memory usage statistics + mMemoryUsageCounter = new MemoryUsageCounter(); + recalculateMemoryUsage(); } @Override @@ -928,6 +1006,9 @@ public class RemoteViews implements Parcelable, Filter { if (mActions != null) { that.mActions = (ArrayList<Action>)mActions.clone(); } + + // update the memory usage stats of the cloned RemoteViews + that.recalculateMemoryUsage(); return that; } @@ -939,7 +1020,7 @@ public class RemoteViews implements Parcelable, Filter { return mLayoutId; } - /** + /* * This flag indicates whether this RemoteViews object is being created from a * RemoteViewsService for use as a child of a widget collection. This flag is used * to determine whether or not certain features are available, in particular, @@ -951,6 +1032,28 @@ public class RemoteViews implements Parcelable, Filter { } /** + * Updates the memory usage statistics. + */ + private void recalculateMemoryUsage() { + mMemoryUsageCounter.clear(); + + // Accumulate the memory usage for each action + if (mActions != null) { + final int count = mActions.size(); + for (int i= 0; i < count; ++i) { + mActions.get(i).updateMemoryUsageEstimate(mMemoryUsageCounter); + } + } + } + + /** + * Returns an estimate of the bitmap heap memory usage for this RemoteViews. + */ + int estimateBitmapMemoryUsage() { + return mMemoryUsageCounter.getBitmapHeapMemoryUsage(); + } + + /** * Add an action to be executed on the remote side when apply is called. * * @param a The action to add @@ -960,6 +1063,9 @@ public class RemoteViews implements Parcelable, Filter { mActions = new ArrayList<Action>(); } mActions.add(a); + + // update the memory usage stats + a.updateMemoryUsageEstimate(mMemoryUsageCounter); } /** diff --git a/core/java/android/widget/RemoteViewsAdapter.java b/core/java/android/widget/RemoteViewsAdapter.java index 91b4afa..afb56fc 100644 --- a/core/java/android/widget/RemoteViewsAdapter.java +++ b/core/java/android/widget/RemoteViewsAdapter.java @@ -16,7 +16,9 @@ package android.widget; +import java.lang.ref.WeakReference; import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedList; import java.util.Map; @@ -32,8 +34,8 @@ import android.os.Looper; import android.util.Log; import android.view.Gravity; import android.view.View; -import android.view.ViewGroup; import android.view.View.MeasureSpec; +import android.view.ViewGroup; import com.android.internal.widget.IRemoteViewsFactory; @@ -48,15 +50,17 @@ public class RemoteViewsAdapter extends BaseAdapter { private Context mContext; private Intent mIntent; private RemoteViewsAdapterServiceConnection mServiceConnection; - private RemoteViewsCache mViewCache; + private WeakReference<RemoteAdapterConnectionCallback> mCallback; + private FixedSizeRemoteViewsCache mCache; + + // The set of requested views that are to be notified when the associated RemoteViews are + // loaded. + private RemoteViewsFrameLayoutRefSet mRequestedViews; private HandlerThread mWorkerThread; // items may be interrupted within the normally processed queues private Handler mWorkerQueue; private Handler mMainQueue; - // items are never dequeued from the priority queue and must run - private Handler mWorkerPriorityQueue; - private Handler mMainPriorityQueue; /** * An interface for the RemoteAdapter to notify other classes when adapters @@ -70,60 +74,89 @@ public class RemoteViewsAdapter extends BaseAdapter { /** * The service connection that gets populated when the RemoteViewsService is - * bound. + * bound. This must be a static inner class to ensure that no references to the outer + * RemoteViewsAdapter instance is retained (this would prevent the RemoteViewsAdapter from being + * garbage collected, and would cause us to leak activities due to the caching mechanism for + * FrameLayouts in the adapter). */ - private class RemoteViewsAdapterServiceConnection implements ServiceConnection { + private static class RemoteViewsAdapterServiceConnection implements ServiceConnection { private boolean mConnected; + private WeakReference<RemoteViewsAdapter> mAdapter; private IRemoteViewsFactory mRemoteViewsFactory; - private RemoteAdapterConnectionCallback mCallback; - public RemoteViewsAdapterServiceConnection(RemoteAdapterConnectionCallback callback) { - mCallback = callback; + public RemoteViewsAdapterServiceConnection(RemoteViewsAdapter adapter) { + mAdapter = new WeakReference<RemoteViewsAdapter>(adapter); } - public void onServiceConnected(ComponentName name, IBinder service) { + public void onServiceConnected(ComponentName name, + IBinder service) { mRemoteViewsFactory = IRemoteViewsFactory.Stub.asInterface(service); mConnected = true; - // notifyDataSetChanged should be called first, to ensure that the - // views are not updated twice - notifyDataSetChanged(); - - // post a new runnable to load the appropriate data, then callback - mWorkerPriorityQueue.post(new Runnable() { + // Queue up work that we need to do for the callback to run + final RemoteViewsAdapter adapter = mAdapter.get(); + if (adapter == null) return; + adapter.mWorkerQueue.post(new Runnable() { @Override public void run() { - // we need to get the viewTypeCount specifically, so just get all the - // metadata - mViewCache.requestMetaData(); - - // post a runnable to call the callback on the main thread - mMainPriorityQueue.post(new Runnable() { - @Override - public void run() { - if (mCallback != null) - mCallback.onRemoteAdapterConnected(); + // Call back to the service to notify that the data set changed + if (adapter.mServiceConnection.isConnected()) { + IRemoteViewsFactory factory = + adapter.mServiceConnection.getRemoteViewsFactory(); + try { + // call back to the factory + factory.onDataSetChanged(); + } catch (Exception e) { + Log.e(TAG, "Error notifying factory of data set changed in " + + "onServiceConnected(): " + e.getMessage()); + e.printStackTrace(); + + // Return early to prevent anything further from being notified + // (effectively nothing has changed) + return; } - }); + + // Request meta data so that we have up to date data when calling back to + // the remote adapter callback + adapter.updateMetaData(); + + // Post a runnable to call back to the view to notify it that we have + // connected + adapter. mMainQueue.post(new Runnable() { + @Override + public void run() { + final RemoteAdapterConnectionCallback callback = + adapter.mCallback.get(); + if (callback != null) { + callback.onRemoteAdapterConnected(); + } + } + }); + } } }); - - // start the background loader - mViewCache.startBackgroundLoader(); } public void onServiceDisconnected(ComponentName name) { - mRemoteViewsFactory = null; mConnected = false; + mRemoteViewsFactory = null; - // clear the main/worker queues - mMainQueue.removeMessages(0); + final RemoteViewsAdapter adapter = mAdapter.get(); + if (adapter == null) return; - // stop the background loader - mViewCache.stopBackgroundLoader(); + // Clear the main/worker queues + adapter.mMainQueue.removeMessages(0); + adapter.mWorkerQueue.removeMessages(0); + + // Clear the cache + synchronized (adapter.mCache) { + adapter.mCache.reset(); + } - if (mCallback != null) - mCallback.onRemoteAdapterDisconnected(); + final RemoteAdapterConnectionCallback callback = adapter.mCallback.get(); + if (callback != null) { + callback.onRemoteAdapterDisconnected(); + } } public IRemoteViewsFactory getRemoteViewsFactory() { @@ -136,532 +169,400 @@ public class RemoteViewsAdapter extends BaseAdapter { } /** - * An internal cache of remote views. + * A FrameLayout which contains a loading view, and manages the re/applying of RemoteViews when + * they are loaded. */ - private class RemoteViewsCache { - private static final String TAG = "RemoteViewsCache"; - - private RemoteViewsInfo mViewCacheInfo; - private RemoteViewsIndexInfo[] mViewCache; - private int[] mTmpViewCacheLoadIndices; - private LinkedList<Integer> mViewCacheLoadIndices; - private boolean mBackgroundLoaderEnabled; - - // if a user loading view is not provided, then we create a temporary one - // for the user using the height of the first view - private RemoteViews mUserLoadingView; - private RemoteViews mFirstView; - private int mFirstViewHeight; - - // determines when the current cache window needs to be updated with new - // items (ie. when there is not enough slack) - private int mViewCacheStartPosition; - private int mViewCacheEndPosition; - private int mHalfCacheSize; - private int mCacheSlack; - private final float mCacheSlackPercentage = 0.75f; + private class RemoteViewsFrameLayout extends FrameLayout { + public RemoteViewsFrameLayout(Context context) { + super(context); + } /** - * The data structure stored at each index of the cache. Any member - * that is not invalidated persists throughout the lifetime of the cache. + * 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. */ - private class RemoteViewsIndexInfo { - FrameLayout flipper; - RemoteViews view; - long itemId; - int typeId; - - RemoteViewsIndexInfo() { - invalidate(); - } - - void set(RemoteViews v, long id) { - view = v; - itemId = id; - if (v != null) - typeId = v.getLayoutId(); - else - typeId = 0; - } + public void onRemoteViewsLoaded(RemoteViews view) { + // Remove all the children of this layout first + removeAllViews(); + addView(view.apply(getContext(), this)); + } + } - void invalidate() { - view = null; - itemId = 0; - typeId = 0; - } + /** + * Stores the references of all the RemoteViewsFrameLayouts that have been returned by the + * adapter that have not yet had their RemoteViews loaded. + */ + private class RemoteViewsFrameLayoutRefSet { + private HashMap<Integer, LinkedList<RemoteViewsFrameLayout>> mReferences; - final boolean isValid() { - return (view != null); - } + public RemoteViewsFrameLayoutRefSet() { + mReferences = new HashMap<Integer, LinkedList<RemoteViewsFrameLayout>>(); } /** - * Remote adapter metadata. Useful for when we have to lock on something - * before updating the metadata. + * Adds a new reference to a RemoteViewsFrameLayout returned by the adapter. */ - private class RemoteViewsInfo { - int count; - int viewTypeCount; - boolean hasStableIds; - boolean isDataDirty; - Map<Integer, Integer> mTypeIdIndexMap; - - RemoteViewsInfo() { - count = 0; - // by default there is at least one dummy view type - viewTypeCount = 1; - hasStableIds = true; - isDataDirty = false; - mTypeIdIndexMap = new HashMap<Integer, Integer>(); + public void add(int position, RemoteViewsFrameLayout layout) { + final Integer pos = position; + LinkedList<RemoteViewsFrameLayout> refs; + + // Create the list if necessary + if (mReferences.containsKey(pos)) { + refs = mReferences.get(pos); + } else { + refs = new LinkedList<RemoteViewsFrameLayout>(); + mReferences.put(pos, refs); } + + // Add the references to the list + refs.add(layout); } - public RemoteViewsCache(int halfCacheSize) { - mHalfCacheSize = halfCacheSize; - mCacheSlack = Math.round(mCacheSlackPercentage * mHalfCacheSize); - mViewCacheStartPosition = 0; - mViewCacheEndPosition = -1; - mBackgroundLoaderEnabled = false; + /** + * Notifies each of the RemoteViewsFrameLayouts associated with a particular position that + * the associated RemoteViews has loaded. + */ + public void notifyOnRemoteViewsLoaded(int position, RemoteViews view, int typeId) { + final Integer pos = position; + if (mReferences.containsKey(pos)) { + // Notify all the references for that position of the newly loaded RemoteViews + final LinkedList<RemoteViewsFrameLayout> refs = mReferences.get(pos); + for (final RemoteViewsFrameLayout ref : refs) { + ref.onRemoteViewsLoaded(view); + } + refs.clear(); - // initialize the cache - int cacheSize = 2 * mHalfCacheSize + 1; - mViewCacheInfo = new RemoteViewsInfo(); - mViewCache = new RemoteViewsIndexInfo[cacheSize]; - for (int i = 0; i < mViewCache.length; ++i) { - mViewCache[i] = new RemoteViewsIndexInfo(); + // Remove this set from the original mapping + mReferences.remove(pos); } - mTmpViewCacheLoadIndices = new int[cacheSize]; - mViewCacheLoadIndices = new LinkedList<Integer>(); } - private final boolean contains(int position) { - return (mViewCacheStartPosition <= position) && (position <= mViewCacheEndPosition); + /** + * Removes all references to all RemoteViewsFrameLayouts returned by the adapter. + */ + public void clear() { + // We currently just clear the references, and leave all the previous layouts returned + // in their default state of the loading view. + mReferences.clear(); } + } - private final boolean containsAndIsValid(int position) { - if (contains(position)) { - RemoteViewsIndexInfo indexInfo = mViewCache[getCacheIndex(position)]; - if (indexInfo.isValid()) { - return true; - } - } - return false; + /** + * The meta-data associated with the cache in it's current state. + */ + private class RemoteViewsMetaData { + int count; + int viewTypeCount; + boolean hasStableIds; + boolean isDataDirty; + + // 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; + + // A mapping from type id to a set of unique type ids + private Map<Integer, Integer> mTypeIdIndexMap; + + public RemoteViewsMetaData() { + reset(); } - private final int getCacheIndex(int position) { - // take the modulo of the position - final int cacheSize = mViewCache.length; - return (cacheSize + (position % cacheSize)) % cacheSize; + public void reset() { + count = 0; + // by default there is at least one dummy view type + viewTypeCount = 1; + hasStableIds = true; + isDataDirty = false; + mUserLoadingView = null; + mFirstView = null; + mFirstViewHeight = 0; + mTypeIdIndexMap = new HashMap<Integer, Integer>(); } - public void requestMetaData() { - if (mServiceConnection.isConnected()) { - try { - IRemoteViewsFactory factory = mServiceConnection.getRemoteViewsFactory(); - - // get the properties/first view (so that we can use it to - // measure our dummy views) - 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); - } - synchronized (mViewCacheInfo) { - RemoteViewsInfo info = mViewCacheInfo; - info.hasStableIds = hasStableIds; - info.viewTypeCount = viewTypeCount + 1; - info.count = count; - mUserLoadingView = loadingView; - if (firstView != null) { - mFirstView = firstView; - mFirstViewHeight = -1; - } - } - } catch (Exception e) { - // print the error - Log.e(TAG, "Error in requestMetaData(): " + e.getMessage()); - - // reset any members after the failed call - synchronized (mViewCacheInfo) { - RemoteViewsInfo info = mViewCacheInfo; - info.hasStableIds = false; - info.viewTypeCount = 1; - info.count = 0; - mUserLoadingView = null; - mFirstView = null; - mFirstViewHeight = -1; - } - } + public void setLoadingViewTemplates(RemoteViews loadingView, RemoteViews firstView) { + mUserLoadingView = loadingView; + if (firstView != null) { + mFirstView = firstView; + mFirstViewHeight = -1; } } - protected void onNotifyDataSetChanged() { - // we mark the data as dirty so that the next call to fetch views will result in - // an onDataSetDirty() call from the adapter - synchronized (mViewCacheInfo) { - mViewCacheInfo.isDataDirty = true; + public int getMappedViewType(int typeId) { + if (mTypeIdIndexMap.containsKey(typeId)) { + return mTypeIdIndexMap.get(typeId); + } else { + // We +1 because the loading view always has view type id of 0 + int incrementalTypeId = mTypeIdIndexMap.size() + 1; + mTypeIdIndexMap.put(typeId, incrementalTypeId); + return incrementalTypeId; } } - private void updateNotifyDataSetChanged() { - // actually calls through to the factory to notify it to update - if (mServiceConnection.isConnected()) { - IRemoteViewsFactory factory = mServiceConnection.getRemoteViewsFactory(); - try { - // call back to the factory - factory.onDataSetChanged(); - } catch (Exception e) { - // print the error - Log.e(TAG, "Error in updateNotifyDataSetChanged(): " + e.getMessage()); - - // return early to prevent container from being notified (nothing has changed) - return; + private RemoteViewsFrameLayout createLoadingView(int position, View convertView, + ViewGroup parent) { + // Create and return a new FrameLayout, and setup the references for this position + final Context context = parent.getContext(); + RemoteViewsFrameLayout layout = new RemoteViewsFrameLayout(context); + + // Create a new loading view + synchronized (mCache) { + if (mUserLoadingView != null) { + // A user-specified loading view + View loadingView = mUserLoadingView.apply(parent.getContext(), parent); + loadingView.setTag(new Integer(0)); + layout.addView(loadingView); + } else { + // A default loading view + // Use the size of the first row as a guide for the size of the loading view + if (mFirstViewHeight < 0) { + View firstView = mFirstView.apply(parent.getContext(), parent); + firstView.measure( + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); + mFirstViewHeight = firstView.getMeasuredHeight(); + mFirstView = null; + } + + // Compose the loading view text + TextView textView = new TextView(parent.getContext()); + textView.setText(com.android.internal.R.string.loading); + textView.setHeight(mFirstViewHeight); + textView.setGravity(Gravity.CENTER_HORIZONTAL | Gravity.CENTER_VERTICAL); + textView.setTextSize(18.0f); + textView.setTextColor(Color.argb(96, 255, 255, 255)); + textView.setShadowLayer(2.0f, 0.0f, 1.0f, Color.BLACK); + textView.setTag(new Integer(0)); + + layout.addView(textView); } } - // re-request the new metadata (only after the notification to the factory) - requestMetaData(); - - // post a new runnable on the main thread to propagate the notification back - // to the base adapter - mMainQueue.post(new Runnable() { - @Override - public void run() { - completeNotifyDataSetChanged(); - } - }); + return layout; } + } - protected void updateRemoteViewsInfo(int position) { - if (mServiceConnection.isConnected()) { - IRemoteViewsFactory factory = mServiceConnection.getRemoteViewsFactory(); - - // load the item information - RemoteViews remoteView = null; - long itemId = 0; - try { - remoteView = factory.getViewAt(position); - itemId = factory.getItemId(position); - } catch (Exception e) { - // print the error - Log.e(TAG, "Error in updateRemoteViewsInfo(" + position + "): " + - e.getMessage()); - e.printStackTrace(); - - // return early to prevent additional work in re-centering the view cache, and - // swapping from the loading view - return; - } + /** + * The meta-data associated with a single item in the cache. + */ + private class RemoteViewsIndexMetaData { + int typeId; + long itemId; - synchronized (mViewCache) { - // skip if the window has moved - if (position < mViewCacheStartPosition || position > mViewCacheEndPosition) - return; - - final int positionIndex = position; - final int cacheIndex = getCacheIndex(position); - mViewCache[cacheIndex].set(remoteView, itemId); - - // notify the main thread when done loading - // flush pending updates - mMainQueue.post(new Runnable() { - @Override - public void run() { - // swap the loader view for this view - synchronized (mViewCache) { - if (containsAndIsValid(positionIndex)) { - RemoteViewsIndexInfo indexInfo = mViewCache[cacheIndex]; - FrameLayout flipper = indexInfo.flipper; - - // update the flipper - flipper.getChildAt(0).setVisibility(View.GONE); - boolean addNewView = true; - if (flipper.getChildCount() > 1) { - View v = flipper.getChildAt(1); - int typeId = ((Integer) v.getTag()).intValue(); - if (typeId == indexInfo.typeId) { - // we can reapply since it is the same type - indexInfo.view.reapply(mContext, v); - v.setVisibility(View.VISIBLE); - if (v.getAnimation() != null) - v.buildDrawingCache(); - addNewView = false; - } else { - flipper.removeViewAt(1); - } - } - if (addNewView) { - View v = indexInfo.view.apply(mContext, flipper); - v.setTag(new Integer(indexInfo.typeId)); - flipper.addView(v); - } - } - } - } - }); - } - } + public RemoteViewsIndexMetaData(RemoteViews v, long itemId) { + set(v, itemId); } - private RemoteViewsIndexInfo requestCachedIndexInfo(final int position) { - int indicesToLoadCount = 0; - - synchronized (mViewCache) { - if (containsAndIsValid(position)) { - // return the info if it exists in the window and is loaded - return mViewCache[getCacheIndex(position)]; - } + public void set(RemoteViews v, long id) { + itemId = id; + if (v != null) + typeId = v.getLayoutId(); + else + typeId = 0; + } + } - // if necessary update the window and load the new information - int centerPosition = (mViewCacheEndPosition + mViewCacheStartPosition) / 2; - if ((mViewCacheEndPosition <= mViewCacheStartPosition) || (Math.abs(position - centerPosition) > mCacheSlack)) { - int newStartPosition = position - mHalfCacheSize; - int newEndPosition = position + mHalfCacheSize; - int frameSize = mHalfCacheSize / 4; - int frameCount = (int) Math.ceil(mViewCache.length / (float) frameSize); - - // prune/add before the current start position - int effectiveStart = Math.max(newStartPosition, 0); - int effectiveEnd = Math.min(newEndPosition, getCount() - 1); - - // invalidate items in the queue - int overlapStart = Math.max(mViewCacheStartPosition, effectiveStart); - int overlapEnd = Math.min(Math.max(mViewCacheStartPosition, mViewCacheEndPosition), effectiveEnd); - for (int i = 0; i < (frameSize * frameCount); ++i) { - int index = newStartPosition + ((i % frameSize) * frameCount + (i / frameSize)); - - if (index <= newEndPosition) { - if ((overlapStart <= index) && (index <= overlapEnd)) { - // load the stuff in the middle that has not already - // been loaded - if (!mViewCache[getCacheIndex(index)].isValid()) { - mTmpViewCacheLoadIndices[indicesToLoadCount++] = index; - } - } else if ((effectiveStart <= index) && (index <= effectiveEnd)) { - // invalidate and load all new effective items - mViewCache[getCacheIndex(index)].invalidate(); - mTmpViewCacheLoadIndices[indicesToLoadCount++] = index; - } else { - // invalidate all other cache indices (outside the effective start/end) - // but don't load - mViewCache[getCacheIndex(index)].invalidate(); - } - } - } + /** + * + */ + private class FixedSizeRemoteViewsCache { + private static final String TAG = "FixedSizeRemoteViewsCache"; + + // The meta data related to all the RemoteViews, ie. count, is stable, etc. + private RemoteViewsMetaData mMetaData; + + // The cache/mapping of position to RemoteViewsMetaData. This set is guaranteed to be + // greater than or equal to the set of RemoteViews. + // Note: The reason that we keep this separate from the RemoteViews cache below is that this + // we still need to be able to access the mapping of position to meta data, without keeping + // the heavy RemoteViews around. The RemoteViews cache is trimmed to fixed constraints wrt. + // memory and size, but this metadata cache will retain information until the data at the + // position is guaranteed as not being necessary any more (usually on notifyDataSetChanged). + private HashMap<Integer, RemoteViewsIndexMetaData> mIndexMetaData; + + // The cache of actual RemoteViews, which may be pruned if the cache gets too large, or uses + // too much memory. + private HashMap<Integer, RemoteViews> mIndexRemoteViews; + + // The set of indices that have been explicitly requested by the collection view + private HashSet<Integer> mRequestedIndices; + + // The set of indices to load, including those explicitly requested, as well as those + // determined by the preloading algorithm to be prefetched + private HashSet<Integer> mLoadIndices; + + // The lower and upper bounds of the preloaded range + private int mPreloadLowerBound; + private int mPreloadUpperBound; + + // The bounds of this fixed cache, we will try and fill as many items into the cache up to + // the maxCount number of items, or the maxSize memory usage. + // The maxCountSlack is used to determine if a new position in the cache to be loaded is + // sufficiently ouside the old set, prompting a shifting of the "window" of items to be + // preloaded. + private int mMaxCount; + private int mMaxCountSlack; + private static final float sMaxCountSlackPercent = 0.75f; + private static final int sMaxMemoryUsage = 1024 * 1024; + + public FixedSizeRemoteViewsCache(int maxCacheSize) { + mMaxCount = maxCacheSize; + mMaxCountSlack = Math.round(sMaxCountSlackPercent * (mMaxCount / 2)); + mPreloadLowerBound = 0; + mPreloadUpperBound = -1; + mMetaData = new RemoteViewsMetaData(); + mIndexMetaData = new HashMap<Integer, RemoteViewsIndexMetaData>(); + mIndexRemoteViews = new HashMap<Integer, RemoteViews>(); + mRequestedIndices = new HashSet<Integer>(); + mLoadIndices = new HashSet<Integer>(); + } - mViewCacheStartPosition = newStartPosition; - mViewCacheEndPosition = newEndPosition; - } + public void insert(int position, RemoteViews v, long itemId) { + // Trim the cache if we go beyond the count + if (mIndexRemoteViews.size() >= mMaxCount) { + mIndexRemoteViews.remove(getFarthestPositionFrom(position)); } - // post items to be loaded - int length = 0; - synchronized (mViewCacheInfo) { - length = mViewCacheInfo.count; + // Trim the cache if we go beyond the available memory size constraints + while (getRemoteViewsBitmapMemoryUsage() >= sMaxMemoryUsage) { + // Note: This is currently the most naive mechanism for deciding what to prune when + // we hit the memory limit. In the future, we may want to calculate which index to + // remove based on both its position as well as it's current memory usage, as well + // as whether it was directly requested vs. whether it was preloaded by our caching + // mechanism. + mIndexRemoteViews.remove(getFarthestPositionFrom(position)); } - if (indicesToLoadCount > 0) { - synchronized (mViewCacheLoadIndices) { - mViewCacheLoadIndices.clear(); - for (int i = 0; i < indicesToLoadCount; ++i) { - final int index = mTmpViewCacheLoadIndices[i]; - if (0 <= index && index < length) { - mViewCacheLoadIndices.addLast(index); - } - } - } + + // Update the metadata cache + if (mIndexMetaData.containsKey(position)) { + final RemoteViewsIndexMetaData metaData = mIndexMetaData.get(position); + metaData.set(v, itemId); + } else { + mIndexMetaData.put(position, new RemoteViewsIndexMetaData(v, itemId)); } + mIndexRemoteViews.put(position, v); + } - // return null so that a dummy view can be retrieved + public RemoteViewsMetaData getMetaData() { + return mMetaData; + } + public RemoteViews getRemoteViewsAt(int position) { + if (mIndexRemoteViews.containsKey(position)) { + return mIndexRemoteViews.get(position); + } return null; } - - public View getView(int position, View convertView, ViewGroup parent) { - if (mServiceConnection.isConnected()) { - // create the flipper views if necessary (we have to do this now - // for all the flippers while we have the reference to the parent) - initializeLoadingViews(parent); - - // request the item from the cache (queueing it to load if not - // in the cache already) - RemoteViewsIndexInfo indexInfo = requestCachedIndexInfo(position); - - // update the flipper appropriately - synchronized (mViewCache) { - int cacheIndex = getCacheIndex(position); - FrameLayout flipper = mViewCache[cacheIndex].flipper; - flipper.setVisibility(View.VISIBLE); - flipper.setAlpha(1.0f); - - if (indexInfo == null) { - // hide the item view and show the loading view - flipper.getChildAt(0).setVisibility(View.VISIBLE); - for (int i = 1; i < flipper.getChildCount(); ++i) { - flipper.getChildAt(i).setVisibility(View.GONE); - } - } else { - // hide the loading view and show the item view - for (int i = 0; i < flipper.getChildCount() - 1; ++i) { - flipper.getChildAt(i).setVisibility(View.GONE); - } - flipper.getChildAt(flipper.getChildCount() - 1).setVisibility(View.VISIBLE); - } - return flipper; - } + public RemoteViewsIndexMetaData getMetaDataAt(int position) { + if (mIndexMetaData.containsKey(position)) { + return mIndexMetaData.get(position); } - return new View(mContext); - } - - private void initializeLoadingViews(ViewGroup parent) { - // ensure that the cache has the appropriate initial flipper - synchronized (mViewCache) { - if (mViewCache[0].flipper == null) { - for (int i = 0; i < mViewCache.length; ++i) { - FrameLayout flipper = new FrameLayout(mContext); - if (mUserLoadingView != null) { - // use the user-specified loading view - flipper.addView(mUserLoadingView.apply(mContext, parent)); - } else { - // calculate the original size of the first row for the loader view - synchronized (mViewCacheInfo) { - if (mFirstViewHeight < 0) { - View firstView = mFirstView.apply(mContext, parent); - firstView.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), - MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); - mFirstViewHeight = firstView.getMeasuredHeight(); - } - } + return null; + } - // construct a new loader and add it to the flipper as the fallback - // default view - TextView textView = new TextView(mContext); - textView.setText("Loading..."); - textView.setHeight(mFirstViewHeight); - textView.setGravity(Gravity.CENTER_HORIZONTAL | Gravity.CENTER_VERTICAL); - textView.setTextSize(18.0f); - textView.setTextColor(Color.argb(96, 255, 255, 255)); - textView.setShadowLayer(2.0f, 0.0f, 1.0f, Color.BLACK); - - flipper.addView(textView); - } - mViewCache[i].flipper = flipper; - } - } + private int getRemoteViewsBitmapMemoryUsage() { + // Calculate the memory usage of all the RemoteViews bitmaps being cached + int mem = 0; + for (Integer i : mIndexRemoteViews.keySet()) { + final RemoteViews v = mIndexRemoteViews.get(i); + mem += v.estimateBitmapMemoryUsage(); } + return mem; } - - public void startBackgroundLoader() { - // initialize the worker runnable - mBackgroundLoaderEnabled = true; - mWorkerQueue.post(new Runnable() { - @Override - public void run() { - while (mBackgroundLoaderEnabled) { - // notify the RemoteViews factory if necessary - boolean isDataDirty = false; - synchronized (mViewCacheInfo) { - isDataDirty = mViewCacheInfo.isDataDirty; - mViewCacheInfo.isDataDirty = false; - } - if (isDataDirty) { - updateNotifyDataSetChanged(); - } - - int index = -1; - synchronized (mViewCacheLoadIndices) { - if (!mViewCacheLoadIndices.isEmpty()) { - index = mViewCacheLoadIndices.removeFirst(); - } - } - if (index < 0) { - // there were no items to load, so sleep for a bit - try { - Thread.sleep(10); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } else { - // otherwise, try and load the item - updateRemoteViewsInfo(index); - } - } + private int getFarthestPositionFrom(int pos) { + // Find the index farthest away and remove that + int maxDist = 0; + int maxDistIndex = -1; + for (int i : mIndexRemoteViews.keySet()) { + int dist = Math.abs(i-pos); + if (dist > maxDist) { + maxDistIndex = i; + maxDist = dist; } - }); - } - - public void stopBackgroundLoader() { - // clear the items to be loaded - mBackgroundLoaderEnabled = false; - synchronized (mViewCacheLoadIndices) { - mViewCacheLoadIndices.clear(); } + return maxDistIndex; } - public long getItemId(int position) { - synchronized (mViewCache) { - if (containsAndIsValid(position)) { - return mViewCache[getCacheIndex(position)].itemId; - } + public void queueRequestedPositionToLoad(int position) { + synchronized (mLoadIndices) { + mRequestedIndices.add(position); + mLoadIndices.add(position); } - return 0; } - - public int getItemViewType(int position) { - // synchronize to ensure that the type id/index map is updated synchronously - synchronized (mViewCache) { - if (containsAndIsValid(position)) { - int viewId = mViewCache[getCacheIndex(position)].typeId; - Map<Integer, Integer> typeMap = mViewCacheInfo.mTypeIdIndexMap; - // we +1 because the default dummy view get view type 0 - if (typeMap.containsKey(viewId)) { - return typeMap.get(viewId); - } else { - int newIndex = typeMap.size() + 1; - typeMap.put(viewId, newIndex); - return newIndex; - } + public void queuePositionsToBePreloadedFromRequestedPosition(int position) { + // Check if we need to preload any items + if (mPreloadLowerBound <= position && position <= mPreloadUpperBound) { + int center = (mPreloadUpperBound + mPreloadLowerBound) / 2; + if (Math.abs(position - center) < mMaxCountSlack) { + return; } } - // return the type of the default item - return 0; - } - public int getCount() { - synchronized (mViewCacheInfo) { - return mViewCacheInfo.count; + int count = 0; + synchronized (mMetaData) { + count = mMetaData.count; } - } + synchronized (mLoadIndices) { + mLoadIndices.clear(); + + // Add all the requested indices + mLoadIndices.addAll(mRequestedIndices); + + // Add all the preload indices + int halfMaxCount = mMaxCount / 2; + mPreloadLowerBound = position - halfMaxCount; + mPreloadUpperBound = position + halfMaxCount; + int effectiveLowerBound = Math.max(0, mPreloadLowerBound); + int effectiveUpperBound = Math.min(mPreloadUpperBound, count - 1); + for (int i = effectiveLowerBound; i <= effectiveUpperBound; ++i) { + mLoadIndices.add(i); + } - public int getViewTypeCount() { - synchronized (mViewCacheInfo) { - return mViewCacheInfo.viewTypeCount; + // But remove all the indices that have already been loaded and are cached + mLoadIndices.removeAll(mIndexRemoteViews.keySet()); } } + public int getNextIndexToLoad() { + // We try and prioritize items that have been requested directly, instead + // of items that are loaded as a result of the caching mechanism + synchronized (mLoadIndices) { + // Prioritize requested indices to be loaded first + if (!mRequestedIndices.isEmpty()) { + Integer i = mRequestedIndices.iterator().next(); + mRequestedIndices.remove(i); + mLoadIndices.remove(i); + return i.intValue(); + } - public boolean hasStableIds() { - synchronized (mViewCacheInfo) { - return mViewCacheInfo.hasStableIds; - } - } + // Otherwise, preload other indices as necessary + if (!mLoadIndices.isEmpty()) { + Integer i = mLoadIndices.iterator().next(); + mLoadIndices.remove(i); + return i.intValue(); + } - public void flushCache() { - // clear the items to be loaded - synchronized (mViewCacheLoadIndices) { - mViewCacheLoadIndices.clear(); + return -1; } + } - synchronized (mViewCache) { - // flush the internal cache and invalidate the adapter for future loads - mMainQueue.removeMessages(0); - - for (int i = 0; i < mViewCache.length; ++i) { - mViewCache[i].invalidate(); - } + public boolean containsRemoteViewAt(int position) { + return mIndexRemoteViews.containsKey(position); + } + public boolean containsMetaDataAt(int position) { + return mIndexMetaData.containsKey(position); + } - mViewCacheStartPosition = 0; - mViewCacheEndPosition = -1; + public void reset() { + mPreloadLowerBound = 0; + mPreloadUpperBound = -1; + mIndexRemoteViews.clear(); + mIndexMetaData.clear(); + mMetaData.reset(); + synchronized (mLoadIndices) { + mRequestedIndices.clear(); + mLoadIndices.clear(); } } } @@ -672,24 +573,126 @@ public class RemoteViewsAdapter extends BaseAdapter { if (mIntent == null) { throw new IllegalArgumentException("Non-null Intent must be specified."); } + mRequestedViews = new RemoteViewsFrameLayoutRefSet(); // initialize the worker thread mWorkerThread = new HandlerThread("RemoteViewsCache-loader"); mWorkerThread.start(); mWorkerQueue = new Handler(mWorkerThread.getLooper()); - mWorkerPriorityQueue = new Handler(mWorkerThread.getLooper()); mMainQueue = new Handler(Looper.myLooper()); - mMainPriorityQueue = new Handler(Looper.myLooper()); // initialize the cache and the service connection on startup - mViewCache = new RemoteViewsCache(25); - mServiceConnection = new RemoteViewsAdapterServiceConnection(callback); + mCache = new FixedSizeRemoteViewsCache(50); + mCallback = new WeakReference<RemoteAdapterConnectionCallback>(callback); + mServiceConnection = new RemoteViewsAdapterServiceConnection(this); requestBindService(); } - protected void finalize() throws Throwable { - // remember to unbind from the service when finalizing - unbindService(); + private void loadNextIndexInBackground() { + mWorkerQueue.post(new Runnable() { + @Override + public void run() { + boolean isDataDirty = false; + + // If the data set has changed, then notify the remote factory so that it can + // update its internals first. + final RemoteViewsMetaData metaData = mCache.getMetaData(); + synchronized (metaData) { + isDataDirty = metaData.isDataDirty; + metaData.isDataDirty = false; + } + if (isDataDirty) { + completeNotifyDataSetChanged(); + } + + // Get the next index to load + int position = -1; + synchronized (mCache) { + position = mCache.getNextIndexToLoad(); + } + if (position > -1) { + // Load the item, and notify any existing RemoteViewsFrameLayouts + updateRemoteViews(position); + + // Queue up for the next one to load + loadNextIndexInBackground(); + } + } + }); + } + + private void updateMetaData() { + if (mServiceConnection.isConnected()) { + try { + IRemoteViewsFactory factory = mServiceConnection.getRemoteViewsFactory(); + + // get the properties/first view (so that we can use it to + // measure our dummy views) + 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); + } + final RemoteViewsMetaData metaData = mCache.getMetaData(); + synchronized (metaData) { + metaData.hasStableIds = hasStableIds; + metaData.viewTypeCount = viewTypeCount + 1; + metaData.count = count; + metaData.setLoadingViewTemplates(loadingView, firstView); + } + } catch (Exception e) { + // print the error + Log.e(TAG, "Error in requestMetaData(): " + e.getMessage()); + + // reset any members after the failed call + final RemoteViewsMetaData metaData = mCache.getMetaData(); + synchronized (metaData) { + metaData.reset(); + } + } + } + } + + private void updateRemoteViews(final int position) { + if (mServiceConnection.isConnected()) { + IRemoteViewsFactory factory = mServiceConnection.getRemoteViewsFactory(); + + // Load the item information from the remote service + RemoteViews remoteViews = null; + long itemId = 0; + try { + remoteViews = factory.getViewAt(position); + itemId = factory.getItemId(position); + } catch (Exception e) { + // Print the error + Log.e(TAG, "Error in updateRemoteViewsInfo(" + position + "): " + + e.getMessage()); + e.printStackTrace(); + + // Return early to prevent additional work in re-centering the view cache, and + // swapping from the loading view + return; + } + + synchronized (mCache) { + // Cache the RemoteViews we loaded + mCache.insert(position, remoteViews, itemId); + + // Notify all the views that we have previously returned for this index that + // there is new data for it. + final RemoteViews rv = remoteViews; + final int typeId = mCache.getMetaDataAt(position).typeId; + mMainQueue.post(new Runnable() { + @Override + public void run() { + mRequestedViews.notifyOnRemoteViewsLoaded(position, rv, typeId); + } + }); + } + } } public Intent getRemoteViewsServiceIntent() { @@ -698,37 +701,132 @@ public class RemoteViewsAdapter extends BaseAdapter { public int getCount() { requestBindService(); - return mViewCache.getCount(); + final RemoteViewsMetaData metaData = mCache.getMetaData(); + synchronized (metaData) { + return metaData.count; + } } public Object getItem(int position) { - // disallow arbitrary object to be associated with an item for the time being + // Disallow arbitrary object to be associated with an item for the time being return null; } public long getItemId(int position) { requestBindService(); - return mViewCache.getItemId(position); + synchronized (mCache) { + if (mCache.containsMetaDataAt(position)) { + return mCache.getMetaDataAt(position).itemId; + } + return 0; + } } public int getItemViewType(int position) { requestBindService(); - return mViewCache.getItemViewType(position); + int typeId = 0; + synchronized (mCache) { + if (mCache.containsMetaDataAt(position)) { + typeId = mCache.getMetaDataAt(position).typeId; + } else { + return 0; + } + } + + final RemoteViewsMetaData metaData = mCache.getMetaData(); + synchronized (metaData) { + return metaData.getMappedViewType(typeId); + } + } + + /** + * Returns the item type id for the specified convert view. Returns -1 if the convert view + * is invalid. + */ + private int getConvertViewTypeId(View convertView) { + int typeId = -1; + if (convertView != null && convertView.getTag() != null) { + typeId = (Integer) convertView.getTag(); + } + return typeId; } public View getView(int position, View convertView, ViewGroup parent) { requestBindService(); - return mViewCache.getView(position, convertView, parent); + if (mServiceConnection.isConnected()) { + // "Request" an index so that we can queue it for loading, initiate subsequent + // preloading, etc. + synchronized (mCache) { + // Queue up other indices to be preloaded based on this position + mCache.queuePositionsToBePreloadedFromRequestedPosition(position); + + RemoteViewsFrameLayout layout = (RemoteViewsFrameLayout) convertView; + View convertViewChild = null; + int convertViewTypeId = 0; + if (convertView != null) { + convertViewChild = layout.getChildAt(0); + convertViewTypeId = getConvertViewTypeId(convertViewChild); + } + + // Second, we try and retrieve the RemoteViews from the cache, returning a loading + // view and queueing it to be loaded if it has not already been loaded. + if (mCache.containsRemoteViewAt(position)) { + Context context = parent.getContext(); + RemoteViews rv = mCache.getRemoteViewsAt(position); + int typeId = mCache.getMetaDataAt(position).typeId; + + // Reuse the convert view where possible + if (convertView != null) { + if (convertViewTypeId == typeId) { + rv.reapply(context, convertViewChild); + return convertView; + } + } + + // Otherwise, create a new view to be returned + View newView = rv.apply(context, parent); + newView.setTag(new Integer(typeId)); + if (convertView != null) { + layout.removeAllViews(); + } else { + layout = new RemoteViewsFrameLayout(context); + } + layout.addView(newView); + return layout; + } else { + // If the cache does not have the RemoteViews at this position, then create a + // loading view and queue the actual position to be loaded in the background + RemoteViewsFrameLayout loadingView = null; + final RemoteViewsMetaData metaData = mCache.getMetaData(); + synchronized (metaData) { + loadingView = metaData.createLoadingView(position, convertView, parent); + } + + mRequestedViews.add(position, loadingView); + mCache.queueRequestedPositionToLoad(position); + loadNextIndexInBackground(); + + return loadingView; + } + } + } + return new View(parent.getContext()); } public int getViewTypeCount() { requestBindService(); - return mViewCache.getViewTypeCount(); + final RemoteViewsMetaData metaData = mCache.getMetaData(); + synchronized (metaData) { + return metaData.viewTypeCount; + } } public boolean hasStableIds() { requestBindService(); - return mViewCache.hasStableIds(); + final RemoteViewsMetaData metaData = mCache.getMetaData(); + synchronized (metaData) { + return metaData.hasStableIds; + } } public boolean isEmpty() { @@ -736,14 +834,49 @@ public class RemoteViewsAdapter extends BaseAdapter { } public void notifyDataSetChanged() { - // flush the cache so that we can reload new items from the service - mViewCache.flushCache(); + synchronized (mCache) { + // Flush the cache so that we can reload new items from the service + mCache.reset(); + } + + final RemoteViewsMetaData metaData = mCache.getMetaData(); + synchronized (metaData) { + // Set flag to calls the remote factory's onDataSetChanged() on the next worker loop + metaData.isDataDirty = true; + } - // notify the factory that it's data may no longer be valid - mViewCache.onNotifyDataSetChanged(); + // Note: we do not call super.notifyDataSetChanged() until the RemoteViewsFactory has had + // a chance to update itself, and return new meta data associated with the new data. After + // which completeNotifyDataSetChanged() is called. } - public void completeNotifyDataSetChanged() { + private void completeNotifyDataSetChanged() { + // Complete the actual notifyDataSetChanged() call initiated earlier + if (mServiceConnection.isConnected()) { + IRemoteViewsFactory factory = mServiceConnection.getRemoteViewsFactory(); + try { + factory.onDataSetChanged(); + } catch (Exception e) { + Log.e(TAG, "Error in updateNotifyDataSetChanged(): " + e.getMessage()); + + // Return early to prevent from further being notified (since nothing has changed) + return; + } + } + + // Re-request the new metadata (only after the notification to the factory) + updateMetaData(); + + // Propagate the notification back to the base adapter + mMainQueue.post(new Runnable() { + @Override + public void run() { + superNotifyDataSetChanged(); + } + }); + } + + private void superNotifyDataSetChanged() { super.notifyDataSetChanged(); } @@ -755,10 +888,4 @@ public class RemoteViewsAdapter extends BaseAdapter { return mServiceConnection.isConnected(); } - - private void unbindService() { - if (mServiceConnection.isConnected()) { - mContext.unbindService(mServiceConnection); - } - } } |