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