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