Home | History | Annotate | Download | only in widget
      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          */
    125         public boolean onRemoteAdapterConnected();
    126 
    127         public void onRemoteAdapterDisconnected();
    128 
    129         /**
    130          * This defers a notifyDataSetChanged on the pending RemoteViewsAdapter if it has not
    131          * connected yet.
    132          */
    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 
    150         public RemoteViewsAdapterServiceConnection(RemoteViewsAdapter adapter) {
    151             mAdapter = new WeakReference<RemoteViewsAdapter>(adapter);
    152         }
    153 
    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 
    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 
    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 
    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 
    278         public synchronized IRemoteViewsFactory getRemoteViewsFactory() {
    279             return mRemoteViewsFactory;
    280         }
    281 
    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 
    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          */
    304         public void onRemoteViewsLoaded(RemoteViews view, OnClickHandler handler) {
    305             setOnClickHandler(handler);
    306             applyRemoteViews(view);
    307         }
    308 
    309         @Override
    310         protected View getDefaultView() {
    311             return mCache.getMetaData().createDefaultLoadingView(this);
    312         }
    313 
    314         @Override
    315         protected Context getRemoteContext() {
    316             return null;
    317         }
    318 
    319         @Override
    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          */
    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          */
    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          */
    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          */
    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 
    414         public RemoteViewsMetaData() {
    415             reset();
    416         }
    417 
    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 
    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 
    439         public void setLoadingViewTemplates(RemoteViews loadingView, RemoteViews firstView) {
    440             mUserLoadingView = loadingView;
    441             if (firstView != null) {
    442                 mFirstView = firstView;
    443                 mFirstViewHeight = -1;
    444             }
    445         }
    446 
    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 
    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          */
    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 
    499         public RemoteViewsIndexMetaData(RemoteViews v, long itemId) {
    500             set(v, itemId);
    501         }
    502 
    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 
    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 
    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 
    605         public RemoteViewsMetaData getMetaData() {
    606             return mMetaData;
    607         }
    608         public RemoteViewsMetaData getTemporaryMetaData() {
    609             return mTemporaryMetaData;
    610         }
    611         public RemoteViews getRemoteViewsAt(int position) {
    612             return mIndexRemoteViews.get(position);
    613         }
    614         public RemoteViewsIndexMetaData getMetaDataAt(int position) {
    615             return mIndexMetaData.get(position);
    616         }
    617 
    618         public void commitTemporaryMetaData() {
    619             synchronized (mTemporaryMetaData) {
    620                 synchronized (mMetaData) {
    621                     mMetaData.set(mTemporaryMetaData);
    622                 }
    623             }
    624         }
    625 
    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 
    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 
    666         public void queueRequestedPositionToLoad(int position) {
    667             mLastRequestedIndex = position;
    668             synchronized (mIndicesToLoad) {
    669                 mIndicesToLoad.put(position, true);
    670             }
    671         }
    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 */
    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 
    729         public boolean containsRemoteViewAt(int position) {
    730             return mIndexRemoteViews.indexOfKey(position) >= 0;
    731         }
    732         public boolean containsMetaDataAt(int position) {
    733             return mIndexMetaData.indexOfKey(position) >= 0;
    734         }
    735 
    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 
    756         RemoteViewsCacheKey(Intent.FilterComparison filter, int widgetId) {
    757             this.filter = filter;
    758             this.widgetId = widgetId;
    759         }
    760 
    761         @Override
    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
    771         public int hashCode() {
    772             return (filter == null ? 0 : filter.hashCode()) ^ (widgetId << 2);
    773         }
    774     }
    775 
    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
    833     protected void finalize() throws Throwable {
    834         try {
    835             if (mWorkerThread != null) {
    836                 mWorkerThread.quit();
    837             }
    838         } finally {
    839             super.finalize();
    840         }
    841     }
    842 
    843     public boolean isDataReady() {
    844         return mDataReady;
    845     }
    846 
    847     public void setRemoteViewsOnClickHandler(OnClickHandler handler) {
    848         mRemoteViewsOnClickHandler = handler;
    849     }
    850 
    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 
    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 
    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 
    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 
    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 
   1032     public Intent getRemoteViewsServiceIntent() {
   1033         return mIntent;
   1034     }
   1035 
   1036     public int getCount() {
   1037         final RemoteViewsMetaData metaData = mCache.getMetaData();
   1038         synchronized (metaData) {
   1039             return metaData.count;
   1040         }
   1041     }
   1042 
   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 
   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 
   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      */
   1078     public void setVisibleRangeHint(int lowerBound, int upperBound) {
   1079         mVisibleWindowLowerBound = lowerBound;
   1080         mVisibleWindowUpperBound = upperBound;
   1081     }
   1082 
   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 
   1125     public int getViewTypeCount() {
   1126         final RemoteViewsMetaData metaData = mCache.getMetaData();
   1127         synchronized (metaData) {
   1128             return metaData.viewTypeCount;
   1129         }
   1130     }
   1131 
   1132     public boolean hasStableIds() {
   1133         final RemoteViewsMetaData metaData = mCache.getMetaData();
   1134         synchronized (metaData) {
   1135             return metaData.hasStableIds;
   1136         }
   1137     }
   1138 
   1139     public boolean isEmpty() {
   1140         return getCount() <= 0;
   1141     }
   1142 
   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      */
   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 
   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 
   1254     void superNotifyDataSetChanged() {
   1255         super.notifyDataSetChanged();
   1256     }
   1257 
   1258     @Override
   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 
   1274     private void enqueueDeferredUnbindServiceMessage() {
   1275         // Remove any existing deferred-unbind messages
   1276         mMainQueue.removeMessages(sUnbindServiceMessageType);
   1277         mMainQueue.sendEmptyMessageDelayed(sUnbindServiceMessageType, sUnbindServiceDelay);
   1278     }
   1279 
   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