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