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