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