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