summaryrefslogtreecommitdiffstats
path: root/core/java
diff options
context:
space:
mode:
authorWinson Chung <winsonc@google.com>2010-09-23 16:40:28 -0700
committerWinson Chung <winsonc@google.com>2010-09-24 17:00:30 -0700
commit3ec9a45c36d3ca5ffbc6e85bbeb497b065e14155 (patch)
treecefe7f8198b857a8d6edd038f3abdfc2c4247f10 /core/java
parent8cb9e9c5c71dabba826545b3d1fa0cd675f95100 (diff)
downloadframeworks_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.java10
-rw-r--r--core/java/android/widget/AdapterViewAnimator.java19
-rw-r--r--core/java/android/widget/RemoteViews.java110
-rw-r--r--core/java/android/widget/RemoteViewsAdapter.java1169
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);
- }
- }
}