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