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