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