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