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