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