RemoteViewsAdapter.java revision 9b3a2cf2a0a482ce8212eb2775176dd4c23e8e9a
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.util.HashMap;
20import java.util.LinkedList;
21import java.util.Map;
22
23import android.content.ComponentName;
24import android.content.Context;
25import android.content.Intent;
26import android.content.ServiceConnection;
27import android.graphics.Color;
28import android.os.Handler;
29import android.os.HandlerThread;
30import android.os.IBinder;
31import android.os.Looper;
32import android.util.Log;
33import android.view.Gravity;
34import android.view.View;
35import android.view.ViewGroup;
36import android.view.View.MeasureSpec;
37
38import com.android.internal.widget.IRemoteViewsFactory;
39
40/**
41 * An adapter to a RemoteViewsService which fetches and caches RemoteViews
42 * to be later inflated as child views.
43 */
44/** @hide */
45public class RemoteViewsAdapter extends BaseAdapter {
46    private static final String TAG = "RemoteViewsAdapter";
47
48    private Context mContext;
49    private Intent mIntent;
50    private RemoteViewsAdapterServiceConnection mServiceConnection;
51    private RemoteViewsCache mViewCache;
52
53    private HandlerThread mWorkerThread;
54    // items may be interrupted within the normally processed queues
55    private Handler mWorkerQueue;
56    private Handler mMainQueue;
57    // items are never dequeued from the priority queue and must run
58    private Handler mWorkerPriorityQueue;
59    private Handler mMainPriorityQueue;
60
61    /**
62     * An interface for the RemoteAdapter to notify other classes when adapters
63     * are actually connected to/disconnected from their actual services.
64     */
65    public interface RemoteAdapterConnectionCallback {
66        public void onRemoteAdapterConnected();
67
68        public void onRemoteAdapterDisconnected();
69    }
70
71    /**
72     * The service connection that gets populated when the RemoteViewsService is
73     * bound.
74     */
75    private class RemoteViewsAdapterServiceConnection implements ServiceConnection {
76        private boolean mConnected;
77        private IRemoteViewsFactory mRemoteViewsFactory;
78        private RemoteAdapterConnectionCallback mCallback;
79
80        public RemoteViewsAdapterServiceConnection(RemoteAdapterConnectionCallback callback) {
81            mCallback = callback;
82        }
83
84        public void onServiceConnected(ComponentName name, IBinder service) {
85            mRemoteViewsFactory = IRemoteViewsFactory.Stub.asInterface(service);
86            mConnected = true;
87
88            // notifyDataSetChanged should be called first, to ensure that the
89            // views are not updated twice
90            notifyDataSetChanged();
91
92            // post a new runnable to load the appropriate data, then callback
93            mWorkerPriorityQueue.post(new Runnable() {
94                @Override
95                public void run() {
96                    // we need to get the viewTypeCount specifically, so just get all the
97                    // metadata
98                    mViewCache.requestMetaData();
99
100                    // post a runnable to call the callback on the main thread
101                    mMainPriorityQueue.post(new Runnable() {
102                        @Override
103                        public void run() {
104                            if (mCallback != null)
105                                mCallback.onRemoteAdapterConnected();
106                        }
107                    });
108                }
109            });
110
111            // start the background loader
112            mViewCache.startBackgroundLoader();
113        }
114
115        public void onServiceDisconnected(ComponentName name) {
116            mRemoteViewsFactory = null;
117            mConnected = false;
118
119            // clear the main/worker queues
120            mMainQueue.removeMessages(0);
121
122            // stop the background loader
123            mViewCache.stopBackgroundLoader();
124
125            if (mCallback != null)
126                mCallback.onRemoteAdapterDisconnected();
127        }
128
129        public IRemoteViewsFactory getRemoteViewsFactory() {
130            return mRemoteViewsFactory;
131        }
132
133        public boolean isConnected() {
134            return mConnected;
135        }
136    }
137
138    /**
139     * An internal cache of remote views.
140     */
141    private class RemoteViewsCache {
142        private static final String TAG = "RemoteViewsCache";
143
144        private RemoteViewsInfo mViewCacheInfo;
145        private RemoteViewsIndexInfo[] mViewCache;
146        private int[] mTmpViewCacheLoadIndices;
147        private LinkedList<Integer> mViewCacheLoadIndices;
148        private boolean mBackgroundLoaderEnabled;
149
150        // if a user loading view is not provided, then we create a temporary one
151        // for the user using the height of the first view
152        private RemoteViews mUserLoadingView;
153        private RemoteViews mFirstView;
154        private int mFirstViewHeight;
155
156        // determines when the current cache window needs to be updated with new
157        // items (ie. when there is not enough slack)
158        private int mViewCacheStartPosition;
159        private int mViewCacheEndPosition;
160        private int mHalfCacheSize;
161        private int mCacheSlack;
162        private final float mCacheSlackPercentage = 0.75f;
163
164        /**
165         * The data structure stored at each index of the cache. Any member
166         * that is not invalidated persists throughout the lifetime of the cache.
167         */
168        private class RemoteViewsIndexInfo {
169            FrameLayout flipper;
170            RemoteViews view;
171            long itemId;
172            int typeId;
173
174            RemoteViewsIndexInfo() {
175                invalidate();
176            }
177
178            void set(RemoteViews v, long id) {
179                view = v;
180                itemId = id;
181                if (v != null)
182                    typeId = v.getLayoutId();
183                else
184                    typeId = 0;
185            }
186
187            void invalidate() {
188                view = null;
189                itemId = 0;
190                typeId = 0;
191            }
192
193            final boolean isValid() {
194                return (view != null);
195            }
196        }
197
198        /**
199         * Remote adapter metadata. Useful for when we have to lock on something
200         * before updating the metadata.
201         */
202        private class RemoteViewsInfo {
203            int count;
204            int viewTypeCount;
205            boolean hasStableIds;
206            boolean isDataDirty;
207            Map<Integer, Integer> mTypeIdIndexMap;
208
209            RemoteViewsInfo() {
210                count = 0;
211                // by default there is at least one dummy view type
212                viewTypeCount = 1;
213                hasStableIds = true;
214                isDataDirty = false;
215                mTypeIdIndexMap = new HashMap<Integer, Integer>();
216            }
217        }
218
219        public RemoteViewsCache(int halfCacheSize) {
220            mHalfCacheSize = halfCacheSize;
221            mCacheSlack = Math.round(mCacheSlackPercentage * mHalfCacheSize);
222            mViewCacheStartPosition = 0;
223            mViewCacheEndPosition = -1;
224            mBackgroundLoaderEnabled = false;
225
226            // initialize the cache
227            int cacheSize = 2 * mHalfCacheSize + 1;
228            mViewCacheInfo = new RemoteViewsInfo();
229            mViewCache = new RemoteViewsIndexInfo[cacheSize];
230            for (int i = 0; i < mViewCache.length; ++i) {
231                mViewCache[i] = new RemoteViewsIndexInfo();
232            }
233            mTmpViewCacheLoadIndices = new int[cacheSize];
234            mViewCacheLoadIndices = new LinkedList<Integer>();
235        }
236
237        private final boolean contains(int position) {
238            return (mViewCacheStartPosition <= position) && (position <= mViewCacheEndPosition);
239        }
240
241        private final boolean containsAndIsValid(int position) {
242            if (contains(position)) {
243                RemoteViewsIndexInfo indexInfo = mViewCache[getCacheIndex(position)];
244                if (indexInfo.isValid()) {
245                    return true;
246                }
247            }
248            return false;
249        }
250
251        private final int getCacheIndex(int position) {
252            // take the modulo of the position
253            final int cacheSize = mViewCache.length;
254            return (cacheSize + (position % cacheSize)) % cacheSize;
255        }
256
257        public void requestMetaData() {
258            if (mServiceConnection.isConnected()) {
259                try {
260                    IRemoteViewsFactory factory = mServiceConnection.getRemoteViewsFactory();
261
262                    // get the properties/first view (so that we can use it to
263                    // measure our dummy views)
264                    boolean hasStableIds = factory.hasStableIds();
265                    int viewTypeCount = factory.getViewTypeCount();
266                    int count = factory.getCount();
267                    RemoteViews loadingView = factory.getLoadingView();
268                    RemoteViews firstView = null;
269                    if ((count > 0) && (loadingView == null)) {
270                        firstView = factory.getViewAt(0);
271                    }
272                    synchronized (mViewCacheInfo) {
273                        RemoteViewsInfo info = mViewCacheInfo;
274                        info.hasStableIds = hasStableIds;
275                        info.viewTypeCount = viewTypeCount + 1;
276                        info.count = count;
277                        mUserLoadingView = loadingView;
278                        if (firstView != null) {
279                            mFirstView = firstView;
280                            mFirstViewHeight = -1;
281                        }
282                    }
283                } catch (Exception e) {
284                    // print the error
285                    Log.e(TAG, "Error in requestMetaData(): " + e.getMessage());
286
287                    // reset any members after the failed call
288                    synchronized (mViewCacheInfo) {
289                        RemoteViewsInfo info = mViewCacheInfo;
290                        info.hasStableIds = false;
291                        info.viewTypeCount = 1;
292                        info.count = 0;
293                        mUserLoadingView = null;
294                        mFirstView = null;
295                        mFirstViewHeight = -1;
296                    }
297                }
298            }
299        }
300
301        protected void onNotifyDataSetChanged() {
302            // we mark the data as dirty so that the next call to fetch views will result in
303            // an onDataSetDirty() call from the adapter
304            synchronized (mViewCacheInfo) {
305                mViewCacheInfo.isDataDirty = true;
306            }
307        }
308
309        private void updateNotifyDataSetChanged() {
310            // actually calls through to the factory to notify it to update
311            if (mServiceConnection.isConnected()) {
312                IRemoteViewsFactory factory = mServiceConnection.getRemoteViewsFactory();
313                try {
314                    // call back to the factory
315                    factory.onDataSetChanged();
316                } catch (Exception e) {
317                    // print the error
318                    Log.e(TAG, "Error in updateNotifyDataSetChanged(): " + e.getMessage());
319
320                    // return early to prevent container from being notified (nothing has changed)
321                    return;
322                }
323            }
324
325            // re-request the new metadata (only after the notification to the factory)
326            requestMetaData();
327
328            // post a new runnable on the main thread to propagate the notification back
329            // to the base adapter
330            mMainQueue.post(new Runnable() {
331                @Override
332                public void run() {
333                   completeNotifyDataSetChanged();
334                }
335            });
336        }
337
338        protected void updateRemoteViewsInfo(int position) {
339            if (mServiceConnection.isConnected()) {
340                IRemoteViewsFactory factory = mServiceConnection.getRemoteViewsFactory();
341
342                // load the item information
343                RemoteViews remoteView = null;
344                long itemId = 0;
345                try {
346                    remoteView = factory.getViewAt(position);
347                    itemId = factory.getItemId(position);
348                } catch (Exception e) {
349                    // print the error
350                    Log.e(TAG, "Error in updateRemoteViewsInfo(" + position + "): " +
351                            e.getMessage());
352                    e.printStackTrace();
353
354                    // return early to prevent additional work in re-centering the view cache, and
355                    // swapping from the loading view
356                    return;
357                }
358
359                synchronized (mViewCache) {
360                    // skip if the window has moved
361                    if (position < mViewCacheStartPosition || position > mViewCacheEndPosition)
362                        return;
363
364                    final int positionIndex = position;
365                    final int cacheIndex = getCacheIndex(position);
366                    mViewCache[cacheIndex].set(remoteView, itemId);
367
368                    // notify the main thread when done loading
369                    // flush pending updates
370                    mMainQueue.post(new Runnable() {
371                        @Override
372                        public void run() {
373                            // swap the loader view for this view
374                            synchronized (mViewCache) {
375                                if (containsAndIsValid(positionIndex)) {
376                                    RemoteViewsIndexInfo indexInfo = mViewCache[cacheIndex];
377                                    FrameLayout flipper = indexInfo.flipper;
378
379                                    // update the flipper
380                                    flipper.getChildAt(0).setVisibility(View.GONE);
381                                    boolean addNewView = true;
382                                    if (flipper.getChildCount() > 1) {
383                                        View v = flipper.getChildAt(1);
384                                        int typeId = ((Integer) v.getTag()).intValue();
385                                        if (typeId == indexInfo.typeId) {
386                                            // we can reapply since it is the same type
387                                            indexInfo.view.reapply(mContext, v);
388                                            v.setVisibility(View.VISIBLE);
389                                            if (v.getAnimation() != null)
390                                                v.buildDrawingCache();
391                                            addNewView = false;
392                                        } else {
393                                            flipper.removeViewAt(1);
394                                        }
395                                    }
396                                    if (addNewView) {
397                                        View v = indexInfo.view.apply(mContext, flipper);
398                                        v.setTag(new Integer(indexInfo.typeId));
399                                        flipper.addView(v);
400                                    }
401                                }
402                            }
403                        }
404                    });
405                }
406            }
407        }
408
409        private RemoteViewsIndexInfo requestCachedIndexInfo(final int position) {
410            int indicesToLoadCount = 0;
411
412            synchronized (mViewCache) {
413                if (containsAndIsValid(position)) {
414                    // return the info if it exists in the window and is loaded
415                    return mViewCache[getCacheIndex(position)];
416                }
417
418                // if necessary update the window and load the new information
419                int centerPosition = (mViewCacheEndPosition + mViewCacheStartPosition) / 2;
420                if ((mViewCacheEndPosition <= mViewCacheStartPosition) || (Math.abs(position - centerPosition) > mCacheSlack)) {
421                    int newStartPosition = position - mHalfCacheSize;
422                    int newEndPosition = position + mHalfCacheSize;
423                    int frameSize = mHalfCacheSize / 4;
424                    int frameCount = (int) Math.ceil(mViewCache.length / (float) frameSize);
425
426                    // prune/add before the current start position
427                    int effectiveStart = Math.max(newStartPosition, 0);
428                    int effectiveEnd = Math.min(newEndPosition, getCount() - 1);
429
430                    // invalidate items in the queue
431                    int overlapStart = Math.max(mViewCacheStartPosition, effectiveStart);
432                    int overlapEnd = Math.min(Math.max(mViewCacheStartPosition, mViewCacheEndPosition), effectiveEnd);
433                    for (int i = 0; i < (frameSize * frameCount); ++i) {
434                        int index = newStartPosition + ((i % frameSize) * frameCount + (i / frameSize));
435
436                        if (index <= newEndPosition) {
437                            if ((overlapStart <= index) && (index <= overlapEnd)) {
438                                // load the stuff in the middle that has not already
439                                // been loaded
440                                if (!mViewCache[getCacheIndex(index)].isValid()) {
441                                    mTmpViewCacheLoadIndices[indicesToLoadCount++] = index;
442                                }
443                            } else if ((effectiveStart <= index) && (index <= effectiveEnd)) {
444                                // invalidate and load all new effective items
445                                mViewCache[getCacheIndex(index)].invalidate();
446                                mTmpViewCacheLoadIndices[indicesToLoadCount++] = index;
447                            } else {
448                                // invalidate all other cache indices (outside the effective start/end)
449                                // but don't load
450                                mViewCache[getCacheIndex(index)].invalidate();
451                            }
452                        }
453                    }
454
455                    mViewCacheStartPosition = newStartPosition;
456                    mViewCacheEndPosition = newEndPosition;
457                }
458            }
459
460            // post items to be loaded
461            int length = 0;
462            synchronized (mViewCacheInfo) {
463                length = mViewCacheInfo.count;
464            }
465            if (indicesToLoadCount > 0) {
466                synchronized (mViewCacheLoadIndices) {
467                    mViewCacheLoadIndices.clear();
468                    for (int i = 0; i < indicesToLoadCount; ++i) {
469                        final int index = mTmpViewCacheLoadIndices[i];
470                        if (0 <= index && index < length) {
471                            mViewCacheLoadIndices.addLast(index);
472                        }
473                    }
474                }
475            }
476
477            // return null so that a dummy view can be retrieved
478            return null;
479        }
480
481        public View getView(int position, View convertView, ViewGroup parent) {
482            if (mServiceConnection.isConnected()) {
483                // create the flipper views if necessary (we have to do this now
484                // for all the flippers while we have the reference to the parent)
485                initializeLoadingViews(parent);
486
487                // request the item from the cache (queueing it to load if not
488                // in the cache already)
489                RemoteViewsIndexInfo indexInfo = requestCachedIndexInfo(position);
490
491                // update the flipper appropriately
492                synchronized (mViewCache) {
493                    int cacheIndex = getCacheIndex(position);
494                    FrameLayout flipper = mViewCache[cacheIndex].flipper;
495                    flipper.setVisibility(View.VISIBLE);
496                    flipper.setAlpha(1.0f);
497
498                    if (indexInfo == null) {
499                        // hide the item view and show the loading view
500                        flipper.getChildAt(0).setVisibility(View.VISIBLE);
501                        for (int i = 1; i < flipper.getChildCount(); ++i) {
502                            flipper.getChildAt(i).setVisibility(View.GONE);
503                        }
504                    } else {
505                        // hide the loading view and show the item view
506                        for (int i = 0; i < flipper.getChildCount() - 1; ++i) {
507                            flipper.getChildAt(i).setVisibility(View.GONE);
508                        }
509                        flipper.getChildAt(flipper.getChildCount() - 1).setVisibility(View.VISIBLE);
510                    }
511                    return flipper;
512                }
513            }
514            return new View(mContext);
515        }
516
517        private void initializeLoadingViews(ViewGroup parent) {
518            // ensure that the cache has the appropriate initial flipper
519            synchronized (mViewCache) {
520                if (mViewCache[0].flipper == null) {
521                    for (int i = 0; i < mViewCache.length; ++i) {
522                        FrameLayout flipper = new FrameLayout(mContext);
523                        if (mUserLoadingView != null) {
524                            // use the user-specified loading view
525                            flipper.addView(mUserLoadingView.apply(mContext, parent));
526                        } else {
527                            // calculate the original size of the first row for the loader view
528                            synchronized (mViewCacheInfo) {
529                                if (mFirstViewHeight < 0) {
530                                    View firstView = mFirstView.apply(mContext, parent);
531                                    firstView.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
532                                            MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
533                                    mFirstViewHeight = firstView.getMeasuredHeight();
534                                }
535                            }
536
537                            // construct a new loader and add it to the flipper as the fallback
538                            // default view
539                            TextView textView = new TextView(mContext);
540                            textView.setText("Loading...");
541                            textView.setHeight(mFirstViewHeight);
542                            textView.setGravity(Gravity.CENTER_HORIZONTAL | Gravity.CENTER_VERTICAL);
543                            textView.setTextSize(18.0f);
544                            textView.setTextColor(Color.argb(96, 255, 255, 255));
545                            textView.setShadowLayer(2.0f, 0.0f, 1.0f, Color.BLACK);
546
547                            flipper.addView(textView);
548                        }
549                        mViewCache[i].flipper = flipper;
550                    }
551                }
552            }
553        }
554
555        public void startBackgroundLoader() {
556            // initialize the worker runnable
557            mBackgroundLoaderEnabled = true;
558            mWorkerQueue.post(new Runnable() {
559                @Override
560                public void run() {
561                    while (mBackgroundLoaderEnabled) {
562                        // notify the RemoteViews factory if necessary
563                        boolean isDataDirty = false;
564                        synchronized (mViewCacheInfo) {
565                            isDataDirty = mViewCacheInfo.isDataDirty;
566                            mViewCacheInfo.isDataDirty = false;
567                        }
568                        if (isDataDirty) {
569                            updateNotifyDataSetChanged();
570                        }
571
572                        int index = -1;
573                        synchronized (mViewCacheLoadIndices) {
574                            if (!mViewCacheLoadIndices.isEmpty()) {
575                                index = mViewCacheLoadIndices.removeFirst();
576                            }
577                        }
578                        if (index < 0) {
579                            // there were no items to load, so sleep for a bit
580                            try {
581                                Thread.sleep(10);
582                            } catch (InterruptedException e) {
583                                e.printStackTrace();
584                            }
585                        } else {
586                            // otherwise, try and load the item
587                            updateRemoteViewsInfo(index);
588                        }
589                    }
590                }
591            });
592        }
593
594        public void stopBackgroundLoader() {
595            // clear the items to be loaded
596            mBackgroundLoaderEnabled = false;
597            synchronized (mViewCacheLoadIndices) {
598                mViewCacheLoadIndices.clear();
599            }
600        }
601
602        public long getItemId(int position) {
603            synchronized (mViewCache) {
604                if (containsAndIsValid(position)) {
605                    return mViewCache[getCacheIndex(position)].itemId;
606                }
607            }
608            return 0;
609        }
610
611        public int getItemViewType(int position) {
612            // synchronize to ensure that the type id/index map is updated synchronously
613            synchronized (mViewCache) {
614                if (containsAndIsValid(position)) {
615                    int viewId = mViewCache[getCacheIndex(position)].typeId;
616                    Map<Integer, Integer> typeMap = mViewCacheInfo.mTypeIdIndexMap;
617                    // we +1 because the default dummy view get view type 0
618                    if (typeMap.containsKey(viewId)) {
619                        return typeMap.get(viewId);
620                    } else {
621                        int newIndex = typeMap.size() + 1;
622                        typeMap.put(viewId, newIndex);
623                        return newIndex;
624                    }
625                }
626            }
627            // return the type of the default item
628            return 0;
629        }
630
631        public int getCount() {
632            synchronized (mViewCacheInfo) {
633                return mViewCacheInfo.count;
634            }
635        }
636
637        public int getViewTypeCount() {
638            synchronized (mViewCacheInfo) {
639                return mViewCacheInfo.viewTypeCount;
640            }
641        }
642
643        public boolean hasStableIds() {
644            synchronized (mViewCacheInfo) {
645                return mViewCacheInfo.hasStableIds;
646            }
647        }
648
649        public void flushCache() {
650            // clear the items to be loaded
651            synchronized (mViewCacheLoadIndices) {
652                mViewCacheLoadIndices.clear();
653            }
654
655            synchronized (mViewCache) {
656                // flush the internal cache and invalidate the adapter for future loads
657                mMainQueue.removeMessages(0);
658
659                for (int i = 0; i < mViewCache.length; ++i) {
660                    mViewCache[i].invalidate();
661                }
662
663                mViewCacheStartPosition = 0;
664                mViewCacheEndPosition = -1;
665            }
666        }
667    }
668
669    public RemoteViewsAdapter(Context context, Intent intent, RemoteAdapterConnectionCallback callback) {
670        mContext = context;
671        mIntent = intent;
672        if (mIntent == null) {
673            throw new IllegalArgumentException("Non-null Intent must be specified.");
674        }
675
676        // initialize the worker thread
677        mWorkerThread = new HandlerThread("RemoteViewsCache-loader");
678        mWorkerThread.start();
679        mWorkerQueue = new Handler(mWorkerThread.getLooper());
680        mWorkerPriorityQueue = new Handler(mWorkerThread.getLooper());
681        mMainQueue = new Handler(Looper.myLooper());
682        mMainPriorityQueue = new Handler(Looper.myLooper());
683
684        // initialize the cache and the service connection on startup
685        mViewCache = new RemoteViewsCache(25);
686        mServiceConnection = new RemoteViewsAdapterServiceConnection(callback);
687        requestBindService();
688    }
689
690    protected void finalize() throws Throwable {
691        // remember to unbind from the service when finalizing
692        unbindService();
693    }
694
695    public Intent getRemoteViewsServiceIntent() {
696        return mIntent;
697    }
698
699    public int getCount() {
700        requestBindService();
701        return mViewCache.getCount();
702    }
703
704    public Object getItem(int position) {
705        // disallow arbitrary object to be associated with an item for the time being
706        return null;
707    }
708
709    public long getItemId(int position) {
710        requestBindService();
711        return mViewCache.getItemId(position);
712    }
713
714    public int getItemViewType(int position) {
715        requestBindService();
716        return mViewCache.getItemViewType(position);
717    }
718
719    public View getView(int position, View convertView, ViewGroup parent) {
720        requestBindService();
721        return mViewCache.getView(position, convertView, parent);
722    }
723
724    public int getViewTypeCount() {
725        requestBindService();
726        return mViewCache.getViewTypeCount();
727    }
728
729    public boolean hasStableIds() {
730        requestBindService();
731        return mViewCache.hasStableIds();
732    }
733
734    public boolean isEmpty() {
735        return getCount() <= 0;
736    }
737
738    public void notifyDataSetChanged() {
739        // flush the cache so that we can reload new items from the service
740        mViewCache.flushCache();
741
742        // notify the factory that it's data may no longer be valid
743        mViewCache.onNotifyDataSetChanged();
744    }
745
746    public void completeNotifyDataSetChanged() {
747        super.notifyDataSetChanged();
748    }
749
750    private boolean requestBindService() {
751        // try binding the service (which will start it if it's not already running)
752        if (!mServiceConnection.isConnected()) {
753            mContext.bindService(mIntent, mServiceConnection, Context.BIND_AUTO_CREATE);
754        }
755
756        return mServiceConnection.isConnected();
757    }
758
759    private void unbindService() {
760        if (mServiceConnection.isConnected()) {
761            mContext.unbindService(mServiceConnection);
762        }
763    }
764}
765