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