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