RemoteViewsAdapter.java revision ec84c3a189e4aa70aa6ea8ba712e5a4f260a153b
1/*
2 * Copyright (C) 2007 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.widget;
18
19import java.lang.ref.WeakReference;
20import java.util.HashMap;
21import java.util.HashSet;
22import java.util.LinkedList;
23import java.util.Map;
24
25import android.content.ComponentName;
26import android.content.Context;
27import android.content.Intent;
28import android.content.ServiceConnection;
29import android.os.Handler;
30import android.os.HandlerThread;
31import android.os.IBinder;
32import android.os.Looper;
33import android.util.Log;
34import android.view.LayoutInflater;
35import android.view.View;
36import android.view.View.MeasureSpec;
37import android.view.ViewGroup;
38
39import com.android.internal.widget.IRemoteViewsFactory;
40
41/**
42 * An adapter to a RemoteViewsService which fetches and caches RemoteViews
43 * to be later inflated as child views.
44 */
45/** @hide */
46public class RemoteViewsAdapter extends BaseAdapter {
47    private static final String TAG = "RemoteViewsAdapter";
48
49    private Context mContext;
50    private Intent mIntent;
51    private LayoutInflater mLayoutInflater;
52    private RemoteViewsAdapterServiceConnection mServiceConnection;
53    private WeakReference<RemoteAdapterConnectionCallback> mCallback;
54    private FixedSizeRemoteViewsCache mCache;
55
56    // The set of requested views that are to be notified when the associated RemoteViews are
57    // loaded.
58    private RemoteViewsFrameLayoutRefSet mRequestedViews;
59
60    private HandlerThread mWorkerThread;
61    // items may be interrupted within the normally processed queues
62    private Handler mWorkerQueue;
63    private Handler mMainQueue;
64
65    /**
66     * An interface for the RemoteAdapter to notify other classes when adapters
67     * are actually connected to/disconnected from their actual services.
68     */
69    public interface RemoteAdapterConnectionCallback {
70        public void onRemoteAdapterConnected();
71
72        public void onRemoteAdapterDisconnected();
73    }
74
75    /**
76     * The service connection that gets populated when the RemoteViewsService is
77     * bound.  This must be a static inner class to ensure that no references to the outer
78     * RemoteViewsAdapter instance is retained (this would prevent the RemoteViewsAdapter from being
79     * garbage collected, and would cause us to leak activities due to the caching mechanism for
80     * FrameLayouts in the adapter).
81     */
82    private static class RemoteViewsAdapterServiceConnection implements ServiceConnection {
83        private boolean mConnected;
84        private WeakReference<RemoteViewsAdapter> mAdapter;
85        private IRemoteViewsFactory mRemoteViewsFactory;
86
87        public RemoteViewsAdapterServiceConnection(RemoteViewsAdapter adapter) {
88            mAdapter = new WeakReference<RemoteViewsAdapter>(adapter);
89        }
90
91        public void onServiceConnected(ComponentName name,
92                IBinder service) {
93            mRemoteViewsFactory = IRemoteViewsFactory.Stub.asInterface(service);
94            mConnected = true;
95
96            // Queue up work that we need to do for the callback to run
97            final RemoteViewsAdapter adapter = mAdapter.get();
98            if (adapter == null) return;
99            adapter.mWorkerQueue.post(new Runnable() {
100                @Override
101                public void run() {
102                    // Call back to the service to notify that the data set changed
103                    if (adapter.mServiceConnection.isConnected()) {
104                        IRemoteViewsFactory factory =
105                            adapter.mServiceConnection.getRemoteViewsFactory();
106                        try {
107                            // call back to the factory
108                            factory.onDataSetChanged();
109                        } catch (Exception e) {
110                            Log.e(TAG, "Error notifying factory of data set changed in " +
111                                        "onServiceConnected(): " + e.getMessage());
112                            e.printStackTrace();
113
114                            // Return early to prevent anything further from being notified
115                            // (effectively nothing has changed)
116                            return;
117                        }
118
119                        // Request meta data so that we have up to date data when calling back to
120                        // the remote adapter callback
121                        adapter.updateMetaData();
122
123                        // Post a runnable to call back to the view to notify it that we have
124                        // connected
125                        adapter.mMainQueue.post(new Runnable() {
126                            @Override
127                            public void run() {
128                                final RemoteAdapterConnectionCallback callback =
129                                    adapter.mCallback.get();
130                                if (callback != null) {
131                                    callback.onRemoteAdapterConnected();
132                                }
133                            }
134                        });
135                    }
136                }
137            });
138        }
139
140        public void onServiceDisconnected(ComponentName name) {
141            mConnected = false;
142            mRemoteViewsFactory = null;
143
144            final RemoteViewsAdapter adapter = mAdapter.get();
145            if (adapter == null) return;
146
147            // Clear the main/worker queues
148            adapter.mMainQueue.removeMessages(0);
149            adapter.mWorkerQueue.removeMessages(0);
150
151            final RemoteAdapterConnectionCallback callback = adapter.mCallback.get();
152            if (callback != null) {
153                callback.onRemoteAdapterDisconnected();
154            }
155            adapter.mCache.reset();
156        }
157
158        public IRemoteViewsFactory getRemoteViewsFactory() {
159            return mRemoteViewsFactory;
160        }
161
162        public boolean isConnected() {
163            return mConnected;
164        }
165    }
166
167    /**
168     * A FrameLayout which contains a loading view, and manages the re/applying of RemoteViews when
169     * they are loaded.
170     */
171    private class RemoteViewsFrameLayout extends FrameLayout {
172        public RemoteViewsFrameLayout(Context context) {
173            super(context);
174        }
175
176        /**
177         * Updates this RemoteViewsFrameLayout depending on the view that was loaded.
178         * @param view the RemoteViews that was loaded. If null, the RemoteViews was not loaded
179         *             successfully.
180         */
181        public void onRemoteViewsLoaded(RemoteViews view) {
182            try {
183                // Remove all the children of this layout first
184                removeAllViews();
185                addView(view.apply(getContext(), this));
186            } catch (Exception e) {
187                Log.e(TAG, "Failed to apply RemoteViews.");
188            }
189        }
190    }
191
192    /**
193     * Stores the references of all the RemoteViewsFrameLayouts that have been returned by the
194     * adapter that have not yet had their RemoteViews loaded.
195     */
196    private class RemoteViewsFrameLayoutRefSet {
197        private HashMap<Integer, LinkedList<RemoteViewsFrameLayout>> mReferences;
198
199        public RemoteViewsFrameLayoutRefSet() {
200            mReferences = new HashMap<Integer, LinkedList<RemoteViewsFrameLayout>>();
201        }
202
203        /**
204         * Adds a new reference to a RemoteViewsFrameLayout returned by the adapter.
205         */
206        public void add(int position, RemoteViewsFrameLayout layout) {
207            final Integer pos = position;
208            LinkedList<RemoteViewsFrameLayout> refs;
209
210            // Create the list if necessary
211            if (mReferences.containsKey(pos)) {
212                refs = mReferences.get(pos);
213            } else {
214                refs = new LinkedList<RemoteViewsFrameLayout>();
215                mReferences.put(pos, refs);
216            }
217
218            // Add the references to the list
219            refs.add(layout);
220        }
221
222        /**
223         * Notifies each of the RemoteViewsFrameLayouts associated with a particular position that
224         * the associated RemoteViews has loaded.
225         */
226        public void notifyOnRemoteViewsLoaded(int position, RemoteViews view, int typeId) {
227            if (view == null) return;
228
229            final Integer pos = position;
230            if (mReferences.containsKey(pos)) {
231                // Notify all the references for that position of the newly loaded RemoteViews
232                final LinkedList<RemoteViewsFrameLayout> refs = mReferences.get(pos);
233                for (final RemoteViewsFrameLayout ref : refs) {
234                    ref.onRemoteViewsLoaded(view);
235                }
236                refs.clear();
237
238                // Remove this set from the original mapping
239                mReferences.remove(pos);
240            }
241        }
242
243        /**
244         * Removes all references to all RemoteViewsFrameLayouts returned by the adapter.
245         */
246        public void clear() {
247            // We currently just clear the references, and leave all the previous layouts returned
248            // in their default state of the loading view.
249            mReferences.clear();
250        }
251    }
252
253    /**
254     * The meta-data associated with the cache in it's current state.
255     */
256    private class RemoteViewsMetaData {
257        int count;
258        int viewTypeCount;
259        boolean hasStableIds;
260        boolean isDataDirty;
261
262        // Used to determine how to construct loading views.  If a loading view is not specified
263        // by the user, then we try and load the first view, and use its height as the height for
264        // the default loading view.
265        RemoteViews mUserLoadingView;
266        RemoteViews mFirstView;
267        int mFirstViewHeight;
268
269        // A mapping from type id to a set of unique type ids
270        private Map<Integer, Integer> mTypeIdIndexMap;
271
272        public RemoteViewsMetaData() {
273            reset();
274        }
275
276        public void reset() {
277            count = 0;
278            // by default there is at least one dummy view type
279            viewTypeCount = 1;
280            hasStableIds = true;
281            isDataDirty = false;
282            mUserLoadingView = null;
283            mFirstView = null;
284            mFirstViewHeight = 0;
285            mTypeIdIndexMap = new HashMap<Integer, Integer>();
286        }
287
288        public void setLoadingViewTemplates(RemoteViews loadingView, RemoteViews firstView) {
289            mUserLoadingView = loadingView;
290            if (firstView != null) {
291                mFirstView = firstView;
292                mFirstViewHeight = -1;
293            }
294        }
295
296        public int getMappedViewType(int typeId) {
297            if (mTypeIdIndexMap.containsKey(typeId)) {
298                return mTypeIdIndexMap.get(typeId);
299            } else {
300                // We +1 because the loading view always has view type id of 0
301                int incrementalTypeId = mTypeIdIndexMap.size() + 1;
302                mTypeIdIndexMap.put(typeId, incrementalTypeId);
303                return incrementalTypeId;
304            }
305        }
306
307        private RemoteViewsFrameLayout createLoadingView(int position, View convertView,
308                ViewGroup parent) {
309            // Create and return a new FrameLayout, and setup the references for this position
310            final Context context = parent.getContext();
311            RemoteViewsFrameLayout layout = new RemoteViewsFrameLayout(context);
312
313            // Create a new loading view
314            synchronized (mCache) {
315                if (mUserLoadingView != null) {
316                    // A user-specified loading view
317                    View loadingView = mUserLoadingView.apply(parent.getContext(), parent);
318                    loadingView.setTagInternal(com.android.internal.R.id.rowTypeId, new Integer(0));
319                    layout.addView(loadingView);
320                } else {
321                    // A default loading view
322                    // Use the size of the first row as a guide for the size of the loading view
323                    if (mFirstViewHeight < 0) {
324                        View firstView = mFirstView.apply(parent.getContext(), parent);
325                        firstView.measure(
326                                MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
327                                MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
328                        mFirstViewHeight = firstView.getMeasuredHeight();
329                        mFirstView = null;
330                    }
331
332                    // Compose the loading view text
333                    TextView loadingTextView = (TextView) mLayoutInflater.inflate(
334                            com.android.internal.R.layout.remote_views_adapter_default_loading_view,
335                            layout, false);
336                    loadingTextView.setHeight(mFirstViewHeight);
337                    loadingTextView.setTag(new Integer(0));
338
339                    layout.addView(loadingTextView);
340                }
341            }
342
343            return layout;
344        }
345    }
346
347    /**
348     * The meta-data associated with a single item in the cache.
349     */
350    private class RemoteViewsIndexMetaData {
351        int typeId;
352        long itemId;
353
354        public RemoteViewsIndexMetaData(RemoteViews v, long itemId) {
355            set(v, itemId);
356        }
357
358        public void set(RemoteViews v, long id) {
359            itemId = id;
360            if (v != null)
361                typeId = v.getLayoutId();
362            else
363                typeId = 0;
364        }
365    }
366
367    /**
368     *
369     */
370    private class FixedSizeRemoteViewsCache {
371        private static final String TAG = "FixedSizeRemoteViewsCache";
372
373        // The meta data related to all the RemoteViews, ie. count, is stable, etc.
374        private RemoteViewsMetaData mMetaData;
375
376        // The cache/mapping of position to RemoteViewsMetaData.  This set is guaranteed to be
377        // greater than or equal to the set of RemoteViews.
378        // Note: The reason that we keep this separate from the RemoteViews cache below is that this
379        // we still need to be able to access the mapping of position to meta data, without keeping
380        // the heavy RemoteViews around.  The RemoteViews cache is trimmed to fixed constraints wrt.
381        // memory and size, but this metadata cache will retain information until the data at the
382        // position is guaranteed as not being necessary any more (usually on notifyDataSetChanged).
383        private HashMap<Integer, RemoteViewsIndexMetaData> mIndexMetaData;
384
385        // The cache of actual RemoteViews, which may be pruned if the cache gets too large, or uses
386        // too much memory.
387        private HashMap<Integer, RemoteViews> mIndexRemoteViews;
388
389        // The set of indices that have been explicitly requested by the collection view
390        private HashSet<Integer> mRequestedIndices;
391
392        // The set of indices to load, including those explicitly requested, as well as those
393        // determined by the preloading algorithm to be prefetched
394        private HashSet<Integer> mLoadIndices;
395
396        // The lower and upper bounds of the preloaded range
397        private int mPreloadLowerBound;
398        private int mPreloadUpperBound;
399
400        // The bounds of this fixed cache, we will try and fill as many items into the cache up to
401        // the maxCount number of items, or the maxSize memory usage.
402        // The maxCountSlack is used to determine if a new position in the cache to be loaded is
403        // sufficiently ouside the old set, prompting a shifting of the "window" of items to be
404        // preloaded.
405        private int mMaxCount;
406        private int mMaxCountSlack;
407        private static final float sMaxCountSlackPercent = 0.75f;
408        private static final int sMaxMemoryUsage = 1024 * 1024;
409
410        public FixedSizeRemoteViewsCache(int maxCacheSize) {
411            mMaxCount = maxCacheSize;
412            mMaxCountSlack = Math.round(sMaxCountSlackPercent * (mMaxCount / 2));
413            mPreloadLowerBound = 0;
414            mPreloadUpperBound = -1;
415            mMetaData = new RemoteViewsMetaData();
416            mIndexMetaData = new HashMap<Integer, RemoteViewsIndexMetaData>();
417            mIndexRemoteViews = new HashMap<Integer, RemoteViews>();
418            mRequestedIndices = new HashSet<Integer>();
419            mLoadIndices = new HashSet<Integer>();
420        }
421
422        public void insert(int position, RemoteViews v, long itemId) {
423            // Trim the cache if we go beyond the count
424            if (mIndexRemoteViews.size() >= mMaxCount) {
425                mIndexRemoteViews.remove(getFarthestPositionFrom(position));
426            }
427
428            // Trim the cache if we go beyond the available memory size constraints
429            while (getRemoteViewsBitmapMemoryUsage() >= sMaxMemoryUsage) {
430                // Note: This is currently the most naive mechanism for deciding what to prune when
431                // we hit the memory limit.  In the future, we may want to calculate which index to
432                // remove based on both its position as well as it's current memory usage, as well
433                // as whether it was directly requested vs. whether it was preloaded by our caching
434                // mechanism.
435                mIndexRemoteViews.remove(getFarthestPositionFrom(position));
436            }
437
438            // Update the metadata cache
439            if (mIndexMetaData.containsKey(position)) {
440                final RemoteViewsIndexMetaData metaData = mIndexMetaData.get(position);
441                metaData.set(v, itemId);
442            } else {
443                mIndexMetaData.put(position, new RemoteViewsIndexMetaData(v, itemId));
444            }
445            mIndexRemoteViews.put(position, v);
446        }
447
448        public RemoteViewsMetaData getMetaData() {
449            return mMetaData;
450        }
451        public RemoteViews getRemoteViewsAt(int position) {
452            if (mIndexRemoteViews.containsKey(position)) {
453                return mIndexRemoteViews.get(position);
454            }
455            return null;
456        }
457        public RemoteViewsIndexMetaData getMetaDataAt(int position) {
458            if (mIndexMetaData.containsKey(position)) {
459                return mIndexMetaData.get(position);
460            }
461            return null;
462        }
463
464        private int getRemoteViewsBitmapMemoryUsage() {
465            // Calculate the memory usage of all the RemoteViews bitmaps being cached
466            int mem = 0;
467            for (Integer i : mIndexRemoteViews.keySet()) {
468                final RemoteViews v = mIndexRemoteViews.get(i);
469                if (v != null) {
470                    mem += v.estimateBitmapMemoryUsage();
471                }
472            }
473            return mem;
474        }
475        private int getFarthestPositionFrom(int pos) {
476            // Find the index farthest away and remove that
477            int maxDist = 0;
478            int maxDistIndex = -1;
479            for (int i : mIndexRemoteViews.keySet()) {
480                int dist = Math.abs(i-pos);
481                if (dist > maxDist) {
482                    maxDistIndex = i;
483                    maxDist = dist;
484                }
485            }
486            return maxDistIndex;
487        }
488
489        public void queueRequestedPositionToLoad(int position) {
490            synchronized (mLoadIndices) {
491                mRequestedIndices.add(position);
492                mLoadIndices.add(position);
493            }
494        }
495        public void queuePositionsToBePreloadedFromRequestedPosition(int position) {
496            // Check if we need to preload any items
497            if (mPreloadLowerBound <= position && position <= mPreloadUpperBound) {
498                int center = (mPreloadUpperBound + mPreloadLowerBound) / 2;
499                if (Math.abs(position - center) < mMaxCountSlack) {
500                    return;
501                }
502            }
503
504            int count = 0;
505            synchronized (mMetaData) {
506                count = mMetaData.count;
507            }
508            synchronized (mLoadIndices) {
509                mLoadIndices.clear();
510
511                // Add all the requested indices
512                mLoadIndices.addAll(mRequestedIndices);
513
514                // Add all the preload indices
515                int halfMaxCount = mMaxCount / 2;
516                mPreloadLowerBound = position - halfMaxCount;
517                mPreloadUpperBound = position + halfMaxCount;
518                int effectiveLowerBound = Math.max(0, mPreloadLowerBound);
519                int effectiveUpperBound = Math.min(mPreloadUpperBound, count - 1);
520                for (int i = effectiveLowerBound; i <= effectiveUpperBound; ++i) {
521                    mLoadIndices.add(i);
522                }
523
524                // But remove all the indices that have already been loaded and are cached
525                mLoadIndices.removeAll(mIndexRemoteViews.keySet());
526            }
527        }
528        public int getNextIndexToLoad() {
529            // We try and prioritize items that have been requested directly, instead
530            // of items that are loaded as a result of the caching mechanism
531            synchronized (mLoadIndices) {
532                // Prioritize requested indices to be loaded first
533                if (!mRequestedIndices.isEmpty()) {
534                    Integer i = mRequestedIndices.iterator().next();
535                    mRequestedIndices.remove(i);
536                    mLoadIndices.remove(i);
537                    return i.intValue();
538                }
539
540                // Otherwise, preload other indices as necessary
541                if (!mLoadIndices.isEmpty()) {
542                    Integer i = mLoadIndices.iterator().next();
543                    mLoadIndices.remove(i);
544                    return i.intValue();
545                }
546
547                return -1;
548            }
549        }
550
551        public boolean containsRemoteViewAt(int position) {
552            return mIndexRemoteViews.containsKey(position);
553        }
554        public boolean containsMetaDataAt(int position) {
555            return mIndexMetaData.containsKey(position);
556        }
557
558        public void reset() {
559            // Note: We do not try and reset the meta data, since that information is still used by
560            // collection views to validate it's own contents (and will be re-requested if the data
561            // is invalidated through the notifyDataSetChanged() flow).
562
563            mPreloadLowerBound = 0;
564            mPreloadUpperBound = -1;
565            mIndexRemoteViews.clear();
566            mIndexMetaData.clear();
567            synchronized (mLoadIndices) {
568                mRequestedIndices.clear();
569                mLoadIndices.clear();
570            }
571        }
572    }
573
574    public RemoteViewsAdapter(Context context, Intent intent, RemoteAdapterConnectionCallback callback) {
575        mContext = context;
576        mIntent = intent;
577        mLayoutInflater = LayoutInflater.from(context);
578        if (mIntent == null) {
579            throw new IllegalArgumentException("Non-null Intent must be specified.");
580        }
581        mRequestedViews = new RemoteViewsFrameLayoutRefSet();
582
583        // initialize the worker thread
584        mWorkerThread = new HandlerThread("RemoteViewsCache-loader");
585        mWorkerThread.start();
586        mWorkerQueue = new Handler(mWorkerThread.getLooper());
587        mMainQueue = new Handler(Looper.myLooper());
588
589        // initialize the cache and the service connection on startup
590        mCache = new FixedSizeRemoteViewsCache(50);
591        mCallback = new WeakReference<RemoteAdapterConnectionCallback>(callback);
592        mServiceConnection = new RemoteViewsAdapterServiceConnection(this);
593        requestBindService();
594    }
595
596    private void loadNextIndexInBackground() {
597        mWorkerQueue.post(new Runnable() {
598            @Override
599            public void run() {
600                // Get the next index to load
601                int position = -1;
602                synchronized (mCache) {
603                    position = mCache.getNextIndexToLoad();
604                }
605                if (position > -1) {
606                    // Load the item, and notify any existing RemoteViewsFrameLayouts
607                    updateRemoteViews(position);
608
609                    // Queue up for the next one to load
610                    loadNextIndexInBackground();
611                }
612            }
613        });
614    }
615
616    private void updateMetaData() {
617        if (mServiceConnection.isConnected()) {
618            try {
619                IRemoteViewsFactory factory = mServiceConnection.getRemoteViewsFactory();
620
621                // get the properties/first view (so that we can use it to
622                // measure our dummy views)
623                boolean hasStableIds = factory.hasStableIds();
624                int viewTypeCount = factory.getViewTypeCount();
625                int count = factory.getCount();
626                RemoteViews loadingView = factory.getLoadingView();
627                RemoteViews firstView = null;
628                if ((count > 0) && (loadingView == null)) {
629                    firstView = factory.getViewAt(0);
630                }
631                final RemoteViewsMetaData metaData = mCache.getMetaData();
632                synchronized (metaData) {
633                    metaData.hasStableIds = hasStableIds;
634                    metaData.viewTypeCount = viewTypeCount + 1;
635                    metaData.count = count;
636                    metaData.setLoadingViewTemplates(loadingView, firstView);
637                }
638            } catch (Exception e) {
639                // print the error
640                Log.e(TAG, "Error in requestMetaData(): " + e.getMessage());
641
642                // reset any members after the failed call
643                final RemoteViewsMetaData metaData = mCache.getMetaData();
644                synchronized (metaData) {
645                    metaData.reset();
646                }
647            }
648        }
649    }
650
651    private void updateRemoteViews(final int position) {
652        if (mServiceConnection.isConnected()) {
653            IRemoteViewsFactory factory = mServiceConnection.getRemoteViewsFactory();
654
655            // Load the item information from the remote service
656            RemoteViews remoteViews = null;
657            long itemId = 0;
658            try {
659                remoteViews = factory.getViewAt(position);
660                itemId = factory.getItemId(position);
661            } catch (Throwable t) {
662                Log.e(TAG, "Error in updateRemoteViews(" + position + "): " + t.getMessage());
663                t.printStackTrace();
664
665                // Return early to prevent additional work in re-centering the view cache, and
666                // swapping from the loading view
667                return;
668            }
669
670            if (remoteViews == null) {
671                // If a null view was returned, we break early to prevent it from getting
672                // into our cache and causing problems later. The effect is that the child  at this
673                // position will remain as a loading view until it is updated.
674                Log.e(TAG, "Error in updateRemoteViews(" + position + "): " + " null RemoteViews " +
675                        "returned from RemoteViewsFactory.");
676                return;
677            }
678            synchronized (mCache) {
679                // Cache the RemoteViews we loaded
680                mCache.insert(position, remoteViews, itemId);
681
682                // Notify all the views that we have previously returned for this index that
683                // there is new data for it.
684                final RemoteViews rv = remoteViews;
685                final int typeId = mCache.getMetaDataAt(position).typeId;
686                mMainQueue.post(new Runnable() {
687                    @Override
688                    public void run() {
689                        mRequestedViews.notifyOnRemoteViewsLoaded(position, rv, typeId);
690                    }
691                });
692            }
693        }
694    }
695
696    public Intent getRemoteViewsServiceIntent() {
697        return mIntent;
698    }
699
700    public int getCount() {
701        requestBindService();
702        final RemoteViewsMetaData metaData = mCache.getMetaData();
703        synchronized (metaData) {
704            return metaData.count;
705        }
706    }
707
708    public Object getItem(int position) {
709        // Disallow arbitrary object to be associated with an item for the time being
710        return null;
711    }
712
713    public long getItemId(int position) {
714        requestBindService();
715        synchronized (mCache) {
716            if (mCache.containsMetaDataAt(position)) {
717                return mCache.getMetaDataAt(position).itemId;
718            }
719            return 0;
720        }
721    }
722
723    public int getItemViewType(int position) {
724        requestBindService();
725        int typeId = 0;
726        synchronized (mCache) {
727            if (mCache.containsMetaDataAt(position)) {
728                typeId = mCache.getMetaDataAt(position).typeId;
729            } else {
730                return 0;
731            }
732        }
733
734        final RemoteViewsMetaData metaData = mCache.getMetaData();
735        synchronized (metaData) {
736            return metaData.getMappedViewType(typeId);
737        }
738    }
739
740    /**
741     * Returns the item type id for the specified convert view.  Returns -1 if the convert view
742     * is invalid.
743     */
744    private int getConvertViewTypeId(View convertView) {
745        int typeId = -1;
746        if (convertView != null) {
747            Object tag = convertView.getTag(com.android.internal.R.id.rowTypeId);
748            if (tag != null) {
749                typeId = (Integer) tag;
750            }
751        }
752        return typeId;
753    }
754
755    public View getView(int position, View convertView, ViewGroup parent) {
756        requestBindService();
757        if (mServiceConnection.isConnected()) {
758            // "Request" an index so that we can queue it for loading, initiate subsequent
759            // preloading, etc.
760            synchronized (mCache) {
761                // Queue up other indices to be preloaded based on this position
762                mCache.queuePositionsToBePreloadedFromRequestedPosition(position);
763
764                RemoteViewsFrameLayout layout = (RemoteViewsFrameLayout) convertView;
765                View convertViewChild = null;
766                int convertViewTypeId = 0;
767                if (convertView != null) {
768                    convertViewChild = layout.getChildAt(0);
769                    convertViewTypeId = getConvertViewTypeId(convertViewChild);
770                }
771
772                // Second, we try and retrieve the RemoteViews from the cache, returning a loading
773                // view and queueing it to be loaded if it has not already been loaded.
774                if (mCache.containsRemoteViewAt(position)) {
775                    Context context = parent.getContext();
776                    RemoteViews rv = mCache.getRemoteViewsAt(position);
777                    int typeId = mCache.getMetaDataAt(position).typeId;
778
779                    // Reuse the convert view where possible
780                    if (convertView != null) {
781                        if (convertViewTypeId == typeId) {
782                            rv.reapply(context, convertViewChild);
783                            return convertView;
784                        }
785                    }
786
787                    // Otherwise, create a new view to be returned
788                    View newView = rv.apply(context, parent);
789                    newView.setTagInternal(com.android.internal.R.id.rowTypeId, new Integer(typeId));
790                    if (convertView != null) {
791                        layout.removeAllViews();
792                    } else {
793                        layout = new RemoteViewsFrameLayout(context);
794                    }
795                    layout.addView(newView);
796                    return layout;
797                } else {
798                    // If the cache does not have the RemoteViews at this position, then create a
799                    // loading view and queue the actual position to be loaded in the background
800                    RemoteViewsFrameLayout loadingView = null;
801                    final RemoteViewsMetaData metaData = mCache.getMetaData();
802                    synchronized (metaData) {
803                        loadingView = metaData.createLoadingView(position, convertView, parent);
804                    }
805
806                    mRequestedViews.add(position, loadingView);
807                    mCache.queueRequestedPositionToLoad(position);
808                    loadNextIndexInBackground();
809
810                    return loadingView;
811                }
812            }
813        }
814        return new View(parent.getContext());
815    }
816
817    public int getViewTypeCount() {
818        requestBindService();
819        final RemoteViewsMetaData metaData = mCache.getMetaData();
820        synchronized (metaData) {
821            return metaData.viewTypeCount;
822        }
823    }
824
825    public boolean hasStableIds() {
826        requestBindService();
827        final RemoteViewsMetaData metaData = mCache.getMetaData();
828        synchronized (metaData) {
829            return metaData.hasStableIds;
830        }
831    }
832
833    public boolean isEmpty() {
834        return getCount() <= 0;
835    }
836
837    public void notifyDataSetChanged() {
838        mWorkerQueue.post(new Runnable() {
839            @Override
840            public void run() {
841                // Complete the actual notifyDataSetChanged() call initiated earlier
842                if (mServiceConnection.isConnected()) {
843                    IRemoteViewsFactory factory = mServiceConnection.getRemoteViewsFactory();
844                    try {
845                        factory.onDataSetChanged();
846                    } catch (Exception e) {
847                        Log.e(TAG, "Error in updateNotifyDataSetChanged(): " + e.getMessage());
848
849                        // Return early to prevent from further being notified (since nothing has
850                        // changed)
851                        return;
852                    }
853                }
854
855                // Flush the cache so that we can reload new items from the service
856                synchronized (mCache) {
857                    mCache.reset();
858                }
859
860                // Re-request the new metadata (only after the notification to the factory)
861                updateMetaData();
862
863                // Propagate the notification back to the base adapter
864                mMainQueue.post(new Runnable() {
865                    @Override
866                    public void run() {
867                        superNotifyDataSetChanged();
868                    }
869                });
870            }
871        });
872
873        // Note: we do not call super.notifyDataSetChanged() until the RemoteViewsFactory has had
874        // a chance to update itself and return new meta data associated with the new data.
875    }
876
877    void superNotifyDataSetChanged() {
878        super.notifyDataSetChanged();
879    }
880
881    private boolean requestBindService() {
882        // try binding the service (which will start it if it's not already running)
883        if (!mServiceConnection.isConnected()) {
884            mContext.bindService(mIntent, mServiceConnection, Context.BIND_AUTO_CREATE);
885        }
886
887        return mServiceConnection.isConnected();
888    }
889}
890