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