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