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