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