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