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