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