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