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