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