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