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