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