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