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