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