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