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