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