RemoteViewsAdapter.java revision ec84c3a189e4aa70aa6ea8ba712e5a4f260a153b
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 17package android.widget; 18 19import java.lang.ref.WeakReference; 20import java.util.HashMap; 21import java.util.HashSet; 22import java.util.LinkedList; 23import java.util.Map; 24 25import android.content.ComponentName; 26import android.content.Context; 27import android.content.Intent; 28import android.content.ServiceConnection; 29import android.os.Handler; 30import android.os.HandlerThread; 31import android.os.IBinder; 32import android.os.Looper; 33import android.util.Log; 34import android.view.LayoutInflater; 35import android.view.View; 36import android.view.View.MeasureSpec; 37import android.view.ViewGroup; 38 39import com.android.internal.widget.IRemoteViewsFactory; 40 41/** 42 * An adapter to a RemoteViewsService which fetches and caches RemoteViews 43 * to be later inflated as child views. 44 */ 45/** @hide */ 46public class RemoteViewsAdapter extends BaseAdapter { 47 private static final String TAG = "RemoteViewsAdapter"; 48 49 private Context mContext; 50 private Intent mIntent; 51 private LayoutInflater mLayoutInflater; 52 private RemoteViewsAdapterServiceConnection mServiceConnection; 53 private WeakReference<RemoteAdapterConnectionCallback> mCallback; 54 private FixedSizeRemoteViewsCache mCache; 55 56 // The set of requested views that are to be notified when the associated RemoteViews are 57 // loaded. 58 private RemoteViewsFrameLayoutRefSet mRequestedViews; 59 60 private HandlerThread mWorkerThread; 61 // items may be interrupted within the normally processed queues 62 private Handler mWorkerQueue; 63 private Handler mMainQueue; 64 65 /** 66 * An interface for the RemoteAdapter to notify other classes when adapters 67 * are actually connected to/disconnected from their actual services. 68 */ 69 public interface RemoteAdapterConnectionCallback { 70 public void onRemoteAdapterConnected(); 71 72 public void onRemoteAdapterDisconnected(); 73 } 74 75 /** 76 * The service connection that gets populated when the RemoteViewsService is 77 * bound. This must be a static inner class to ensure that no references to the outer 78 * RemoteViewsAdapter instance is retained (this would prevent the RemoteViewsAdapter from being 79 * garbage collected, and would cause us to leak activities due to the caching mechanism for 80 * FrameLayouts in the adapter). 81 */ 82 private static class RemoteViewsAdapterServiceConnection implements ServiceConnection { 83 private boolean mConnected; 84 private WeakReference<RemoteViewsAdapter> mAdapter; 85 private IRemoteViewsFactory mRemoteViewsFactory; 86 87 public RemoteViewsAdapterServiceConnection(RemoteViewsAdapter adapter) { 88 mAdapter = new WeakReference<RemoteViewsAdapter>(adapter); 89 } 90 91 public void onServiceConnected(ComponentName name, 92 IBinder service) { 93 mRemoteViewsFactory = IRemoteViewsFactory.Stub.asInterface(service); 94 mConnected = true; 95 96 // Queue up work that we need to do for the callback to run 97 final RemoteViewsAdapter adapter = mAdapter.get(); 98 if (adapter == null) return; 99 adapter.mWorkerQueue.post(new Runnable() { 100 @Override 101 public void run() { 102 // Call back to the service to notify that the data set changed 103 if (adapter.mServiceConnection.isConnected()) { 104 IRemoteViewsFactory factory = 105 adapter.mServiceConnection.getRemoteViewsFactory(); 106 try { 107 // call back to the factory 108 factory.onDataSetChanged(); 109 } catch (Exception e) { 110 Log.e(TAG, "Error notifying factory of data set changed in " + 111 "onServiceConnected(): " + e.getMessage()); 112 e.printStackTrace(); 113 114 // Return early to prevent anything further from being notified 115 // (effectively nothing has changed) 116 return; 117 } 118 119 // Request meta data so that we have up to date data when calling back to 120 // the remote adapter callback 121 adapter.updateMetaData(); 122 123 // Post a runnable to call back to the view to notify it that we have 124 // connected 125 adapter.mMainQueue.post(new Runnable() { 126 @Override 127 public void run() { 128 final RemoteAdapterConnectionCallback callback = 129 adapter.mCallback.get(); 130 if (callback != null) { 131 callback.onRemoteAdapterConnected(); 132 } 133 } 134 }); 135 } 136 } 137 }); 138 } 139 140 public void onServiceDisconnected(ComponentName name) { 141 mConnected = false; 142 mRemoteViewsFactory = null; 143 144 final RemoteViewsAdapter adapter = mAdapter.get(); 145 if (adapter == null) return; 146 147 // Clear the main/worker queues 148 adapter.mMainQueue.removeMessages(0); 149 adapter.mWorkerQueue.removeMessages(0); 150 151 final RemoteAdapterConnectionCallback callback = adapter.mCallback.get(); 152 if (callback != null) { 153 callback.onRemoteAdapterDisconnected(); 154 } 155 adapter.mCache.reset(); 156 } 157 158 public IRemoteViewsFactory getRemoteViewsFactory() { 159 return mRemoteViewsFactory; 160 } 161 162 public boolean isConnected() { 163 return mConnected; 164 } 165 } 166 167 /** 168 * A FrameLayout which contains a loading view, and manages the re/applying of RemoteViews when 169 * they are loaded. 170 */ 171 private class RemoteViewsFrameLayout extends FrameLayout { 172 public RemoteViewsFrameLayout(Context context) { 173 super(context); 174 } 175 176 /** 177 * Updates this RemoteViewsFrameLayout depending on the view that was loaded. 178 * @param view the RemoteViews that was loaded. If null, the RemoteViews was not loaded 179 * successfully. 180 */ 181 public void onRemoteViewsLoaded(RemoteViews view) { 182 try { 183 // Remove all the children of this layout first 184 removeAllViews(); 185 addView(view.apply(getContext(), this)); 186 } catch (Exception e) { 187 Log.e(TAG, "Failed to apply RemoteViews."); 188 } 189 } 190 } 191 192 /** 193 * Stores the references of all the RemoteViewsFrameLayouts that have been returned by the 194 * adapter that have not yet had their RemoteViews loaded. 195 */ 196 private class RemoteViewsFrameLayoutRefSet { 197 private HashMap<Integer, LinkedList<RemoteViewsFrameLayout>> mReferences; 198 199 public RemoteViewsFrameLayoutRefSet() { 200 mReferences = new HashMap<Integer, LinkedList<RemoteViewsFrameLayout>>(); 201 } 202 203 /** 204 * Adds a new reference to a RemoteViewsFrameLayout returned by the adapter. 205 */ 206 public void add(int position, RemoteViewsFrameLayout layout) { 207 final Integer pos = position; 208 LinkedList<RemoteViewsFrameLayout> refs; 209 210 // Create the list if necessary 211 if (mReferences.containsKey(pos)) { 212 refs = mReferences.get(pos); 213 } else { 214 refs = new LinkedList<RemoteViewsFrameLayout>(); 215 mReferences.put(pos, refs); 216 } 217 218 // Add the references to the list 219 refs.add(layout); 220 } 221 222 /** 223 * Notifies each of the RemoteViewsFrameLayouts associated with a particular position that 224 * the associated RemoteViews has loaded. 225 */ 226 public void notifyOnRemoteViewsLoaded(int position, RemoteViews view, int typeId) { 227 if (view == null) return; 228 229 final Integer pos = position; 230 if (mReferences.containsKey(pos)) { 231 // Notify all the references for that position of the newly loaded RemoteViews 232 final LinkedList<RemoteViewsFrameLayout> refs = mReferences.get(pos); 233 for (final RemoteViewsFrameLayout ref : refs) { 234 ref.onRemoteViewsLoaded(view); 235 } 236 refs.clear(); 237 238 // Remove this set from the original mapping 239 mReferences.remove(pos); 240 } 241 } 242 243 /** 244 * Removes all references to all RemoteViewsFrameLayouts returned by the adapter. 245 */ 246 public void clear() { 247 // We currently just clear the references, and leave all the previous layouts returned 248 // in their default state of the loading view. 249 mReferences.clear(); 250 } 251 } 252 253 /** 254 * The meta-data associated with the cache in it's current state. 255 */ 256 private class RemoteViewsMetaData { 257 int count; 258 int viewTypeCount; 259 boolean hasStableIds; 260 boolean isDataDirty; 261 262 // Used to determine how to construct loading views. If a loading view is not specified 263 // by the user, then we try and load the first view, and use its height as the height for 264 // the default loading view. 265 RemoteViews mUserLoadingView; 266 RemoteViews mFirstView; 267 int mFirstViewHeight; 268 269 // A mapping from type id to a set of unique type ids 270 private Map<Integer, Integer> mTypeIdIndexMap; 271 272 public RemoteViewsMetaData() { 273 reset(); 274 } 275 276 public void reset() { 277 count = 0; 278 // by default there is at least one dummy view type 279 viewTypeCount = 1; 280 hasStableIds = true; 281 isDataDirty = false; 282 mUserLoadingView = null; 283 mFirstView = null; 284 mFirstViewHeight = 0; 285 mTypeIdIndexMap = new HashMap<Integer, Integer>(); 286 } 287 288 public void setLoadingViewTemplates(RemoteViews loadingView, RemoteViews firstView) { 289 mUserLoadingView = loadingView; 290 if (firstView != null) { 291 mFirstView = firstView; 292 mFirstViewHeight = -1; 293 } 294 } 295 296 public int getMappedViewType(int typeId) { 297 if (mTypeIdIndexMap.containsKey(typeId)) { 298 return mTypeIdIndexMap.get(typeId); 299 } else { 300 // We +1 because the loading view always has view type id of 0 301 int incrementalTypeId = mTypeIdIndexMap.size() + 1; 302 mTypeIdIndexMap.put(typeId, incrementalTypeId); 303 return incrementalTypeId; 304 } 305 } 306 307 private RemoteViewsFrameLayout createLoadingView(int position, View convertView, 308 ViewGroup parent) { 309 // Create and return a new FrameLayout, and setup the references for this position 310 final Context context = parent.getContext(); 311 RemoteViewsFrameLayout layout = new RemoteViewsFrameLayout(context); 312 313 // Create a new loading view 314 synchronized (mCache) { 315 if (mUserLoadingView != null) { 316 // A user-specified loading view 317 View loadingView = mUserLoadingView.apply(parent.getContext(), parent); 318 loadingView.setTagInternal(com.android.internal.R.id.rowTypeId, new Integer(0)); 319 layout.addView(loadingView); 320 } else { 321 // A default loading view 322 // Use the size of the first row as a guide for the size of the loading view 323 if (mFirstViewHeight < 0) { 324 View firstView = mFirstView.apply(parent.getContext(), parent); 325 firstView.measure( 326 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), 327 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); 328 mFirstViewHeight = firstView.getMeasuredHeight(); 329 mFirstView = null; 330 } 331 332 // Compose the loading view text 333 TextView loadingTextView = (TextView) mLayoutInflater.inflate( 334 com.android.internal.R.layout.remote_views_adapter_default_loading_view, 335 layout, false); 336 loadingTextView.setHeight(mFirstViewHeight); 337 loadingTextView.setTag(new Integer(0)); 338 339 layout.addView(loadingTextView); 340 } 341 } 342 343 return layout; 344 } 345 } 346 347 /** 348 * The meta-data associated with a single item in the cache. 349 */ 350 private class RemoteViewsIndexMetaData { 351 int typeId; 352 long itemId; 353 354 public RemoteViewsIndexMetaData(RemoteViews v, long itemId) { 355 set(v, itemId); 356 } 357 358 public void set(RemoteViews v, long id) { 359 itemId = id; 360 if (v != null) 361 typeId = v.getLayoutId(); 362 else 363 typeId = 0; 364 } 365 } 366 367 /** 368 * 369 */ 370 private class FixedSizeRemoteViewsCache { 371 private static final String TAG = "FixedSizeRemoteViewsCache"; 372 373 // The meta data related to all the RemoteViews, ie. count, is stable, etc. 374 private RemoteViewsMetaData mMetaData; 375 376 // The cache/mapping of position to RemoteViewsMetaData. This set is guaranteed to be 377 // greater than or equal to the set of RemoteViews. 378 // Note: The reason that we keep this separate from the RemoteViews cache below is that this 379 // we still need to be able to access the mapping of position to meta data, without keeping 380 // the heavy RemoteViews around. The RemoteViews cache is trimmed to fixed constraints wrt. 381 // memory and size, but this metadata cache will retain information until the data at the 382 // position is guaranteed as not being necessary any more (usually on notifyDataSetChanged). 383 private HashMap<Integer, RemoteViewsIndexMetaData> mIndexMetaData; 384 385 // The cache of actual RemoteViews, which may be pruned if the cache gets too large, or uses 386 // too much memory. 387 private HashMap<Integer, RemoteViews> mIndexRemoteViews; 388 389 // The set of indices that have been explicitly requested by the collection view 390 private HashSet<Integer> mRequestedIndices; 391 392 // The set of indices to load, including those explicitly requested, as well as those 393 // determined by the preloading algorithm to be prefetched 394 private HashSet<Integer> mLoadIndices; 395 396 // The lower and upper bounds of the preloaded range 397 private int mPreloadLowerBound; 398 private int mPreloadUpperBound; 399 400 // The bounds of this fixed cache, we will try and fill as many items into the cache up to 401 // the maxCount number of items, or the maxSize memory usage. 402 // The maxCountSlack is used to determine if a new position in the cache to be loaded is 403 // sufficiently ouside the old set, prompting a shifting of the "window" of items to be 404 // preloaded. 405 private int mMaxCount; 406 private int mMaxCountSlack; 407 private static final float sMaxCountSlackPercent = 0.75f; 408 private static final int sMaxMemoryUsage = 1024 * 1024; 409 410 public FixedSizeRemoteViewsCache(int maxCacheSize) { 411 mMaxCount = maxCacheSize; 412 mMaxCountSlack = Math.round(sMaxCountSlackPercent * (mMaxCount / 2)); 413 mPreloadLowerBound = 0; 414 mPreloadUpperBound = -1; 415 mMetaData = new RemoteViewsMetaData(); 416 mIndexMetaData = new HashMap<Integer, RemoteViewsIndexMetaData>(); 417 mIndexRemoteViews = new HashMap<Integer, RemoteViews>(); 418 mRequestedIndices = new HashSet<Integer>(); 419 mLoadIndices = new HashSet<Integer>(); 420 } 421 422 public void insert(int position, RemoteViews v, long itemId) { 423 // Trim the cache if we go beyond the count 424 if (mIndexRemoteViews.size() >= mMaxCount) { 425 mIndexRemoteViews.remove(getFarthestPositionFrom(position)); 426 } 427 428 // Trim the cache if we go beyond the available memory size constraints 429 while (getRemoteViewsBitmapMemoryUsage() >= sMaxMemoryUsage) { 430 // Note: This is currently the most naive mechanism for deciding what to prune when 431 // we hit the memory limit. In the future, we may want to calculate which index to 432 // remove based on both its position as well as it's current memory usage, as well 433 // as whether it was directly requested vs. whether it was preloaded by our caching 434 // mechanism. 435 mIndexRemoteViews.remove(getFarthestPositionFrom(position)); 436 } 437 438 // Update the metadata cache 439 if (mIndexMetaData.containsKey(position)) { 440 final RemoteViewsIndexMetaData metaData = mIndexMetaData.get(position); 441 metaData.set(v, itemId); 442 } else { 443 mIndexMetaData.put(position, new RemoteViewsIndexMetaData(v, itemId)); 444 } 445 mIndexRemoteViews.put(position, v); 446 } 447 448 public RemoteViewsMetaData getMetaData() { 449 return mMetaData; 450 } 451 public RemoteViews getRemoteViewsAt(int position) { 452 if (mIndexRemoteViews.containsKey(position)) { 453 return mIndexRemoteViews.get(position); 454 } 455 return null; 456 } 457 public RemoteViewsIndexMetaData getMetaDataAt(int position) { 458 if (mIndexMetaData.containsKey(position)) { 459 return mIndexMetaData.get(position); 460 } 461 return null; 462 } 463 464 private int getRemoteViewsBitmapMemoryUsage() { 465 // Calculate the memory usage of all the RemoteViews bitmaps being cached 466 int mem = 0; 467 for (Integer i : mIndexRemoteViews.keySet()) { 468 final RemoteViews v = mIndexRemoteViews.get(i); 469 if (v != null) { 470 mem += v.estimateBitmapMemoryUsage(); 471 } 472 } 473 return mem; 474 } 475 private int getFarthestPositionFrom(int pos) { 476 // Find the index farthest away and remove that 477 int maxDist = 0; 478 int maxDistIndex = -1; 479 for (int i : mIndexRemoteViews.keySet()) { 480 int dist = Math.abs(i-pos); 481 if (dist > maxDist) { 482 maxDistIndex = i; 483 maxDist = dist; 484 } 485 } 486 return maxDistIndex; 487 } 488 489 public void queueRequestedPositionToLoad(int position) { 490 synchronized (mLoadIndices) { 491 mRequestedIndices.add(position); 492 mLoadIndices.add(position); 493 } 494 } 495 public void queuePositionsToBePreloadedFromRequestedPosition(int position) { 496 // Check if we need to preload any items 497 if (mPreloadLowerBound <= position && position <= mPreloadUpperBound) { 498 int center = (mPreloadUpperBound + mPreloadLowerBound) / 2; 499 if (Math.abs(position - center) < mMaxCountSlack) { 500 return; 501 } 502 } 503 504 int count = 0; 505 synchronized (mMetaData) { 506 count = mMetaData.count; 507 } 508 synchronized (mLoadIndices) { 509 mLoadIndices.clear(); 510 511 // Add all the requested indices 512 mLoadIndices.addAll(mRequestedIndices); 513 514 // Add all the preload indices 515 int halfMaxCount = mMaxCount / 2; 516 mPreloadLowerBound = position - halfMaxCount; 517 mPreloadUpperBound = position + halfMaxCount; 518 int effectiveLowerBound = Math.max(0, mPreloadLowerBound); 519 int effectiveUpperBound = Math.min(mPreloadUpperBound, count - 1); 520 for (int i = effectiveLowerBound; i <= effectiveUpperBound; ++i) { 521 mLoadIndices.add(i); 522 } 523 524 // But remove all the indices that have already been loaded and are cached 525 mLoadIndices.removeAll(mIndexRemoteViews.keySet()); 526 } 527 } 528 public int getNextIndexToLoad() { 529 // We try and prioritize items that have been requested directly, instead 530 // of items that are loaded as a result of the caching mechanism 531 synchronized (mLoadIndices) { 532 // Prioritize requested indices to be loaded first 533 if (!mRequestedIndices.isEmpty()) { 534 Integer i = mRequestedIndices.iterator().next(); 535 mRequestedIndices.remove(i); 536 mLoadIndices.remove(i); 537 return i.intValue(); 538 } 539 540 // Otherwise, preload other indices as necessary 541 if (!mLoadIndices.isEmpty()) { 542 Integer i = mLoadIndices.iterator().next(); 543 mLoadIndices.remove(i); 544 return i.intValue(); 545 } 546 547 return -1; 548 } 549 } 550 551 public boolean containsRemoteViewAt(int position) { 552 return mIndexRemoteViews.containsKey(position); 553 } 554 public boolean containsMetaDataAt(int position) { 555 return mIndexMetaData.containsKey(position); 556 } 557 558 public void reset() { 559 // Note: We do not try and reset the meta data, since that information is still used by 560 // collection views to validate it's own contents (and will be re-requested if the data 561 // is invalidated through the notifyDataSetChanged() flow). 562 563 mPreloadLowerBound = 0; 564 mPreloadUpperBound = -1; 565 mIndexRemoteViews.clear(); 566 mIndexMetaData.clear(); 567 synchronized (mLoadIndices) { 568 mRequestedIndices.clear(); 569 mLoadIndices.clear(); 570 } 571 } 572 } 573 574 public RemoteViewsAdapter(Context context, Intent intent, RemoteAdapterConnectionCallback callback) { 575 mContext = context; 576 mIntent = intent; 577 mLayoutInflater = LayoutInflater.from(context); 578 if (mIntent == null) { 579 throw new IllegalArgumentException("Non-null Intent must be specified."); 580 } 581 mRequestedViews = new RemoteViewsFrameLayoutRefSet(); 582 583 // initialize the worker thread 584 mWorkerThread = new HandlerThread("RemoteViewsCache-loader"); 585 mWorkerThread.start(); 586 mWorkerQueue = new Handler(mWorkerThread.getLooper()); 587 mMainQueue = new Handler(Looper.myLooper()); 588 589 // initialize the cache and the service connection on startup 590 mCache = new FixedSizeRemoteViewsCache(50); 591 mCallback = new WeakReference<RemoteAdapterConnectionCallback>(callback); 592 mServiceConnection = new RemoteViewsAdapterServiceConnection(this); 593 requestBindService(); 594 } 595 596 private void loadNextIndexInBackground() { 597 mWorkerQueue.post(new Runnable() { 598 @Override 599 public void run() { 600 // Get the next index to load 601 int position = -1; 602 synchronized (mCache) { 603 position = mCache.getNextIndexToLoad(); 604 } 605 if (position > -1) { 606 // Load the item, and notify any existing RemoteViewsFrameLayouts 607 updateRemoteViews(position); 608 609 // Queue up for the next one to load 610 loadNextIndexInBackground(); 611 } 612 } 613 }); 614 } 615 616 private void updateMetaData() { 617 if (mServiceConnection.isConnected()) { 618 try { 619 IRemoteViewsFactory factory = mServiceConnection.getRemoteViewsFactory(); 620 621 // get the properties/first view (so that we can use it to 622 // measure our dummy views) 623 boolean hasStableIds = factory.hasStableIds(); 624 int viewTypeCount = factory.getViewTypeCount(); 625 int count = factory.getCount(); 626 RemoteViews loadingView = factory.getLoadingView(); 627 RemoteViews firstView = null; 628 if ((count > 0) && (loadingView == null)) { 629 firstView = factory.getViewAt(0); 630 } 631 final RemoteViewsMetaData metaData = mCache.getMetaData(); 632 synchronized (metaData) { 633 metaData.hasStableIds = hasStableIds; 634 metaData.viewTypeCount = viewTypeCount + 1; 635 metaData.count = count; 636 metaData.setLoadingViewTemplates(loadingView, firstView); 637 } 638 } catch (Exception e) { 639 // print the error 640 Log.e(TAG, "Error in requestMetaData(): " + e.getMessage()); 641 642 // reset any members after the failed call 643 final RemoteViewsMetaData metaData = mCache.getMetaData(); 644 synchronized (metaData) { 645 metaData.reset(); 646 } 647 } 648 } 649 } 650 651 private void updateRemoteViews(final int position) { 652 if (mServiceConnection.isConnected()) { 653 IRemoteViewsFactory factory = mServiceConnection.getRemoteViewsFactory(); 654 655 // Load the item information from the remote service 656 RemoteViews remoteViews = null; 657 long itemId = 0; 658 try { 659 remoteViews = factory.getViewAt(position); 660 itemId = factory.getItemId(position); 661 } catch (Throwable t) { 662 Log.e(TAG, "Error in updateRemoteViews(" + position + "): " + t.getMessage()); 663 t.printStackTrace(); 664 665 // Return early to prevent additional work in re-centering the view cache, and 666 // swapping from the loading view 667 return; 668 } 669 670 if (remoteViews == null) { 671 // If a null view was returned, we break early to prevent it from getting 672 // into our cache and causing problems later. The effect is that the child at this 673 // position will remain as a loading view until it is updated. 674 Log.e(TAG, "Error in updateRemoteViews(" + position + "): " + " null RemoteViews " + 675 "returned from RemoteViewsFactory."); 676 return; 677 } 678 synchronized (mCache) { 679 // Cache the RemoteViews we loaded 680 mCache.insert(position, remoteViews, itemId); 681 682 // Notify all the views that we have previously returned for this index that 683 // there is new data for it. 684 final RemoteViews rv = remoteViews; 685 final int typeId = mCache.getMetaDataAt(position).typeId; 686 mMainQueue.post(new Runnable() { 687 @Override 688 public void run() { 689 mRequestedViews.notifyOnRemoteViewsLoaded(position, rv, typeId); 690 } 691 }); 692 } 693 } 694 } 695 696 public Intent getRemoteViewsServiceIntent() { 697 return mIntent; 698 } 699 700 public int getCount() { 701 requestBindService(); 702 final RemoteViewsMetaData metaData = mCache.getMetaData(); 703 synchronized (metaData) { 704 return metaData.count; 705 } 706 } 707 708 public Object getItem(int position) { 709 // Disallow arbitrary object to be associated with an item for the time being 710 return null; 711 } 712 713 public long getItemId(int position) { 714 requestBindService(); 715 synchronized (mCache) { 716 if (mCache.containsMetaDataAt(position)) { 717 return mCache.getMetaDataAt(position).itemId; 718 } 719 return 0; 720 } 721 } 722 723 public int getItemViewType(int position) { 724 requestBindService(); 725 int typeId = 0; 726 synchronized (mCache) { 727 if (mCache.containsMetaDataAt(position)) { 728 typeId = mCache.getMetaDataAt(position).typeId; 729 } else { 730 return 0; 731 } 732 } 733 734 final RemoteViewsMetaData metaData = mCache.getMetaData(); 735 synchronized (metaData) { 736 return metaData.getMappedViewType(typeId); 737 } 738 } 739 740 /** 741 * Returns the item type id for the specified convert view. Returns -1 if the convert view 742 * is invalid. 743 */ 744 private int getConvertViewTypeId(View convertView) { 745 int typeId = -1; 746 if (convertView != null) { 747 Object tag = convertView.getTag(com.android.internal.R.id.rowTypeId); 748 if (tag != null) { 749 typeId = (Integer) tag; 750 } 751 } 752 return typeId; 753 } 754 755 public View getView(int position, View convertView, ViewGroup parent) { 756 requestBindService(); 757 if (mServiceConnection.isConnected()) { 758 // "Request" an index so that we can queue it for loading, initiate subsequent 759 // preloading, etc. 760 synchronized (mCache) { 761 // Queue up other indices to be preloaded based on this position 762 mCache.queuePositionsToBePreloadedFromRequestedPosition(position); 763 764 RemoteViewsFrameLayout layout = (RemoteViewsFrameLayout) convertView; 765 View convertViewChild = null; 766 int convertViewTypeId = 0; 767 if (convertView != null) { 768 convertViewChild = layout.getChildAt(0); 769 convertViewTypeId = getConvertViewTypeId(convertViewChild); 770 } 771 772 // Second, we try and retrieve the RemoteViews from the cache, returning a loading 773 // view and queueing it to be loaded if it has not already been loaded. 774 if (mCache.containsRemoteViewAt(position)) { 775 Context context = parent.getContext(); 776 RemoteViews rv = mCache.getRemoteViewsAt(position); 777 int typeId = mCache.getMetaDataAt(position).typeId; 778 779 // Reuse the convert view where possible 780 if (convertView != null) { 781 if (convertViewTypeId == typeId) { 782 rv.reapply(context, convertViewChild); 783 return convertView; 784 } 785 } 786 787 // Otherwise, create a new view to be returned 788 View newView = rv.apply(context, parent); 789 newView.setTagInternal(com.android.internal.R.id.rowTypeId, new Integer(typeId)); 790 if (convertView != null) { 791 layout.removeAllViews(); 792 } else { 793 layout = new RemoteViewsFrameLayout(context); 794 } 795 layout.addView(newView); 796 return layout; 797 } else { 798 // If the cache does not have the RemoteViews at this position, then create a 799 // loading view and queue the actual position to be loaded in the background 800 RemoteViewsFrameLayout loadingView = null; 801 final RemoteViewsMetaData metaData = mCache.getMetaData(); 802 synchronized (metaData) { 803 loadingView = metaData.createLoadingView(position, convertView, parent); 804 } 805 806 mRequestedViews.add(position, loadingView); 807 mCache.queueRequestedPositionToLoad(position); 808 loadNextIndexInBackground(); 809 810 return loadingView; 811 } 812 } 813 } 814 return new View(parent.getContext()); 815 } 816 817 public int getViewTypeCount() { 818 requestBindService(); 819 final RemoteViewsMetaData metaData = mCache.getMetaData(); 820 synchronized (metaData) { 821 return metaData.viewTypeCount; 822 } 823 } 824 825 public boolean hasStableIds() { 826 requestBindService(); 827 final RemoteViewsMetaData metaData = mCache.getMetaData(); 828 synchronized (metaData) { 829 return metaData.hasStableIds; 830 } 831 } 832 833 public boolean isEmpty() { 834 return getCount() <= 0; 835 } 836 837 public void notifyDataSetChanged() { 838 mWorkerQueue.post(new Runnable() { 839 @Override 840 public void run() { 841 // Complete the actual notifyDataSetChanged() call initiated earlier 842 if (mServiceConnection.isConnected()) { 843 IRemoteViewsFactory factory = mServiceConnection.getRemoteViewsFactory(); 844 try { 845 factory.onDataSetChanged(); 846 } catch (Exception e) { 847 Log.e(TAG, "Error in updateNotifyDataSetChanged(): " + e.getMessage()); 848 849 // Return early to prevent from further being notified (since nothing has 850 // changed) 851 return; 852 } 853 } 854 855 // Flush the cache so that we can reload new items from the service 856 synchronized (mCache) { 857 mCache.reset(); 858 } 859 860 // Re-request the new metadata (only after the notification to the factory) 861 updateMetaData(); 862 863 // Propagate the notification back to the base adapter 864 mMainQueue.post(new Runnable() { 865 @Override 866 public void run() { 867 superNotifyDataSetChanged(); 868 } 869 }); 870 } 871 }); 872 873 // Note: we do not call super.notifyDataSetChanged() until the RemoteViewsFactory has had 874 // a chance to update itself and return new meta data associated with the new data. 875 } 876 877 void superNotifyDataSetChanged() { 878 super.notifyDataSetChanged(); 879 } 880 881 private boolean requestBindService() { 882 // try binding the service (which will start it if it's not already running) 883 if (!mServiceConnection.isConnected()) { 884 mContext.bindService(mIntent, mServiceConnection, Context.BIND_AUTO_CREATE); 885 } 886 887 return mServiceConnection.isConnected(); 888 } 889} 890