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