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