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