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