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