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