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