RemoteViewsAdapter.java revision 9b3a2cf2a0a482ce8212eb2775176dd4c23e8e9a
1/* 2 * Copyright (C) 2007 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package android.widget; 18 19import java.util.HashMap; 20import java.util.LinkedList; 21import java.util.Map; 22 23import android.content.ComponentName; 24import android.content.Context; 25import android.content.Intent; 26import android.content.ServiceConnection; 27import android.graphics.Color; 28import android.os.Handler; 29import android.os.HandlerThread; 30import android.os.IBinder; 31import android.os.Looper; 32import android.util.Log; 33import android.view.Gravity; 34import android.view.View; 35import android.view.ViewGroup; 36import android.view.View.MeasureSpec; 37 38import com.android.internal.widget.IRemoteViewsFactory; 39 40/** 41 * An adapter to a RemoteViewsService which fetches and caches RemoteViews 42 * to be later inflated as child views. 43 */ 44/** @hide */ 45public class RemoteViewsAdapter extends BaseAdapter { 46 private static final String TAG = "RemoteViewsAdapter"; 47 48 private Context mContext; 49 private Intent mIntent; 50 private RemoteViewsAdapterServiceConnection mServiceConnection; 51 private RemoteViewsCache mViewCache; 52 53 private HandlerThread mWorkerThread; 54 // items may be interrupted within the normally processed queues 55 private Handler mWorkerQueue; 56 private Handler mMainQueue; 57 // items are never dequeued from the priority queue and must run 58 private Handler mWorkerPriorityQueue; 59 private Handler mMainPriorityQueue; 60 61 /** 62 * An interface for the RemoteAdapter to notify other classes when adapters 63 * are actually connected to/disconnected from their actual services. 64 */ 65 public interface RemoteAdapterConnectionCallback { 66 public void onRemoteAdapterConnected(); 67 68 public void onRemoteAdapterDisconnected(); 69 } 70 71 /** 72 * The service connection that gets populated when the RemoteViewsService is 73 * bound. 74 */ 75 private class RemoteViewsAdapterServiceConnection implements ServiceConnection { 76 private boolean mConnected; 77 private IRemoteViewsFactory mRemoteViewsFactory; 78 private RemoteAdapterConnectionCallback mCallback; 79 80 public RemoteViewsAdapterServiceConnection(RemoteAdapterConnectionCallback callback) { 81 mCallback = callback; 82 } 83 84 public void onServiceConnected(ComponentName name, IBinder service) { 85 mRemoteViewsFactory = IRemoteViewsFactory.Stub.asInterface(service); 86 mConnected = true; 87 88 // notifyDataSetChanged should be called first, to ensure that the 89 // views are not updated twice 90 notifyDataSetChanged(); 91 92 // post a new runnable to load the appropriate data, then callback 93 mWorkerPriorityQueue.post(new Runnable() { 94 @Override 95 public void run() { 96 // we need to get the viewTypeCount specifically, so just get all the 97 // metadata 98 mViewCache.requestMetaData(); 99 100 // post a runnable to call the callback on the main thread 101 mMainPriorityQueue.post(new Runnable() { 102 @Override 103 public void run() { 104 if (mCallback != null) 105 mCallback.onRemoteAdapterConnected(); 106 } 107 }); 108 } 109 }); 110 111 // start the background loader 112 mViewCache.startBackgroundLoader(); 113 } 114 115 public void onServiceDisconnected(ComponentName name) { 116 mRemoteViewsFactory = null; 117 mConnected = false; 118 119 // clear the main/worker queues 120 mMainQueue.removeMessages(0); 121 122 // stop the background loader 123 mViewCache.stopBackgroundLoader(); 124 125 if (mCallback != null) 126 mCallback.onRemoteAdapterDisconnected(); 127 } 128 129 public IRemoteViewsFactory getRemoteViewsFactory() { 130 return mRemoteViewsFactory; 131 } 132 133 public boolean isConnected() { 134 return mConnected; 135 } 136 } 137 138 /** 139 * An internal cache of remote views. 140 */ 141 private class RemoteViewsCache { 142 private static final String TAG = "RemoteViewsCache"; 143 144 private RemoteViewsInfo mViewCacheInfo; 145 private RemoteViewsIndexInfo[] mViewCache; 146 private int[] mTmpViewCacheLoadIndices; 147 private LinkedList<Integer> mViewCacheLoadIndices; 148 private boolean mBackgroundLoaderEnabled; 149 150 // if a user loading view is not provided, then we create a temporary one 151 // for the user using the height of the first view 152 private RemoteViews mUserLoadingView; 153 private RemoteViews mFirstView; 154 private int mFirstViewHeight; 155 156 // determines when the current cache window needs to be updated with new 157 // items (ie. when there is not enough slack) 158 private int mViewCacheStartPosition; 159 private int mViewCacheEndPosition; 160 private int mHalfCacheSize; 161 private int mCacheSlack; 162 private final float mCacheSlackPercentage = 0.75f; 163 164 /** 165 * The data structure stored at each index of the cache. Any member 166 * that is not invalidated persists throughout the lifetime of the cache. 167 */ 168 private class RemoteViewsIndexInfo { 169 FrameLayout flipper; 170 RemoteViews view; 171 long itemId; 172 int typeId; 173 174 RemoteViewsIndexInfo() { 175 invalidate(); 176 } 177 178 void set(RemoteViews v, long id) { 179 view = v; 180 itemId = id; 181 if (v != null) 182 typeId = v.getLayoutId(); 183 else 184 typeId = 0; 185 } 186 187 void invalidate() { 188 view = null; 189 itemId = 0; 190 typeId = 0; 191 } 192 193 final boolean isValid() { 194 return (view != null); 195 } 196 } 197 198 /** 199 * Remote adapter metadata. Useful for when we have to lock on something 200 * before updating the metadata. 201 */ 202 private class RemoteViewsInfo { 203 int count; 204 int viewTypeCount; 205 boolean hasStableIds; 206 boolean isDataDirty; 207 Map<Integer, Integer> mTypeIdIndexMap; 208 209 RemoteViewsInfo() { 210 count = 0; 211 // by default there is at least one dummy view type 212 viewTypeCount = 1; 213 hasStableIds = true; 214 isDataDirty = false; 215 mTypeIdIndexMap = new HashMap<Integer, Integer>(); 216 } 217 } 218 219 public RemoteViewsCache(int halfCacheSize) { 220 mHalfCacheSize = halfCacheSize; 221 mCacheSlack = Math.round(mCacheSlackPercentage * mHalfCacheSize); 222 mViewCacheStartPosition = 0; 223 mViewCacheEndPosition = -1; 224 mBackgroundLoaderEnabled = false; 225 226 // initialize the cache 227 int cacheSize = 2 * mHalfCacheSize + 1; 228 mViewCacheInfo = new RemoteViewsInfo(); 229 mViewCache = new RemoteViewsIndexInfo[cacheSize]; 230 for (int i = 0; i < mViewCache.length; ++i) { 231 mViewCache[i] = new RemoteViewsIndexInfo(); 232 } 233 mTmpViewCacheLoadIndices = new int[cacheSize]; 234 mViewCacheLoadIndices = new LinkedList<Integer>(); 235 } 236 237 private final boolean contains(int position) { 238 return (mViewCacheStartPosition <= position) && (position <= mViewCacheEndPosition); 239 } 240 241 private final boolean containsAndIsValid(int position) { 242 if (contains(position)) { 243 RemoteViewsIndexInfo indexInfo = mViewCache[getCacheIndex(position)]; 244 if (indexInfo.isValid()) { 245 return true; 246 } 247 } 248 return false; 249 } 250 251 private final int getCacheIndex(int position) { 252 // take the modulo of the position 253 final int cacheSize = mViewCache.length; 254 return (cacheSize + (position % cacheSize)) % cacheSize; 255 } 256 257 public void requestMetaData() { 258 if (mServiceConnection.isConnected()) { 259 try { 260 IRemoteViewsFactory factory = mServiceConnection.getRemoteViewsFactory(); 261 262 // get the properties/first view (so that we can use it to 263 // measure our dummy views) 264 boolean hasStableIds = factory.hasStableIds(); 265 int viewTypeCount = factory.getViewTypeCount(); 266 int count = factory.getCount(); 267 RemoteViews loadingView = factory.getLoadingView(); 268 RemoteViews firstView = null; 269 if ((count > 0) && (loadingView == null)) { 270 firstView = factory.getViewAt(0); 271 } 272 synchronized (mViewCacheInfo) { 273 RemoteViewsInfo info = mViewCacheInfo; 274 info.hasStableIds = hasStableIds; 275 info.viewTypeCount = viewTypeCount + 1; 276 info.count = count; 277 mUserLoadingView = loadingView; 278 if (firstView != null) { 279 mFirstView = firstView; 280 mFirstViewHeight = -1; 281 } 282 } 283 } catch (Exception e) { 284 // print the error 285 Log.e(TAG, "Error in requestMetaData(): " + e.getMessage()); 286 287 // reset any members after the failed call 288 synchronized (mViewCacheInfo) { 289 RemoteViewsInfo info = mViewCacheInfo; 290 info.hasStableIds = false; 291 info.viewTypeCount = 1; 292 info.count = 0; 293 mUserLoadingView = null; 294 mFirstView = null; 295 mFirstViewHeight = -1; 296 } 297 } 298 } 299 } 300 301 protected void onNotifyDataSetChanged() { 302 // we mark the data as dirty so that the next call to fetch views will result in 303 // an onDataSetDirty() call from the adapter 304 synchronized (mViewCacheInfo) { 305 mViewCacheInfo.isDataDirty = true; 306 } 307 } 308 309 private void updateNotifyDataSetChanged() { 310 // actually calls through to the factory to notify it to update 311 if (mServiceConnection.isConnected()) { 312 IRemoteViewsFactory factory = mServiceConnection.getRemoteViewsFactory(); 313 try { 314 // call back to the factory 315 factory.onDataSetChanged(); 316 } catch (Exception e) { 317 // print the error 318 Log.e(TAG, "Error in updateNotifyDataSetChanged(): " + e.getMessage()); 319 320 // return early to prevent container from being notified (nothing has changed) 321 return; 322 } 323 } 324 325 // re-request the new metadata (only after the notification to the factory) 326 requestMetaData(); 327 328 // post a new runnable on the main thread to propagate the notification back 329 // to the base adapter 330 mMainQueue.post(new Runnable() { 331 @Override 332 public void run() { 333 completeNotifyDataSetChanged(); 334 } 335 }); 336 } 337 338 protected void updateRemoteViewsInfo(int position) { 339 if (mServiceConnection.isConnected()) { 340 IRemoteViewsFactory factory = mServiceConnection.getRemoteViewsFactory(); 341 342 // load the item information 343 RemoteViews remoteView = null; 344 long itemId = 0; 345 try { 346 remoteView = factory.getViewAt(position); 347 itemId = factory.getItemId(position); 348 } catch (Exception e) { 349 // print the error 350 Log.e(TAG, "Error in updateRemoteViewsInfo(" + position + "): " + 351 e.getMessage()); 352 e.printStackTrace(); 353 354 // return early to prevent additional work in re-centering the view cache, and 355 // swapping from the loading view 356 return; 357 } 358 359 synchronized (mViewCache) { 360 // skip if the window has moved 361 if (position < mViewCacheStartPosition || position > mViewCacheEndPosition) 362 return; 363 364 final int positionIndex = position; 365 final int cacheIndex = getCacheIndex(position); 366 mViewCache[cacheIndex].set(remoteView, itemId); 367 368 // notify the main thread when done loading 369 // flush pending updates 370 mMainQueue.post(new Runnable() { 371 @Override 372 public void run() { 373 // swap the loader view for this view 374 synchronized (mViewCache) { 375 if (containsAndIsValid(positionIndex)) { 376 RemoteViewsIndexInfo indexInfo = mViewCache[cacheIndex]; 377 FrameLayout flipper = indexInfo.flipper; 378 379 // update the flipper 380 flipper.getChildAt(0).setVisibility(View.GONE); 381 boolean addNewView = true; 382 if (flipper.getChildCount() > 1) { 383 View v = flipper.getChildAt(1); 384 int typeId = ((Integer) v.getTag()).intValue(); 385 if (typeId == indexInfo.typeId) { 386 // we can reapply since it is the same type 387 indexInfo.view.reapply(mContext, v); 388 v.setVisibility(View.VISIBLE); 389 if (v.getAnimation() != null) 390 v.buildDrawingCache(); 391 addNewView = false; 392 } else { 393 flipper.removeViewAt(1); 394 } 395 } 396 if (addNewView) { 397 View v = indexInfo.view.apply(mContext, flipper); 398 v.setTag(new Integer(indexInfo.typeId)); 399 flipper.addView(v); 400 } 401 } 402 } 403 } 404 }); 405 } 406 } 407 } 408 409 private RemoteViewsIndexInfo requestCachedIndexInfo(final int position) { 410 int indicesToLoadCount = 0; 411 412 synchronized (mViewCache) { 413 if (containsAndIsValid(position)) { 414 // return the info if it exists in the window and is loaded 415 return mViewCache[getCacheIndex(position)]; 416 } 417 418 // if necessary update the window and load the new information 419 int centerPosition = (mViewCacheEndPosition + mViewCacheStartPosition) / 2; 420 if ((mViewCacheEndPosition <= mViewCacheStartPosition) || (Math.abs(position - centerPosition) > mCacheSlack)) { 421 int newStartPosition = position - mHalfCacheSize; 422 int newEndPosition = position + mHalfCacheSize; 423 int frameSize = mHalfCacheSize / 4; 424 int frameCount = (int) Math.ceil(mViewCache.length / (float) frameSize); 425 426 // prune/add before the current start position 427 int effectiveStart = Math.max(newStartPosition, 0); 428 int effectiveEnd = Math.min(newEndPosition, getCount() - 1); 429 430 // invalidate items in the queue 431 int overlapStart = Math.max(mViewCacheStartPosition, effectiveStart); 432 int overlapEnd = Math.min(Math.max(mViewCacheStartPosition, mViewCacheEndPosition), effectiveEnd); 433 for (int i = 0; i < (frameSize * frameCount); ++i) { 434 int index = newStartPosition + ((i % frameSize) * frameCount + (i / frameSize)); 435 436 if (index <= newEndPosition) { 437 if ((overlapStart <= index) && (index <= overlapEnd)) { 438 // load the stuff in the middle that has not already 439 // been loaded 440 if (!mViewCache[getCacheIndex(index)].isValid()) { 441 mTmpViewCacheLoadIndices[indicesToLoadCount++] = index; 442 } 443 } else if ((effectiveStart <= index) && (index <= effectiveEnd)) { 444 // invalidate and load all new effective items 445 mViewCache[getCacheIndex(index)].invalidate(); 446 mTmpViewCacheLoadIndices[indicesToLoadCount++] = index; 447 } else { 448 // invalidate all other cache indices (outside the effective start/end) 449 // but don't load 450 mViewCache[getCacheIndex(index)].invalidate(); 451 } 452 } 453 } 454 455 mViewCacheStartPosition = newStartPosition; 456 mViewCacheEndPosition = newEndPosition; 457 } 458 } 459 460 // post items to be loaded 461 int length = 0; 462 synchronized (mViewCacheInfo) { 463 length = mViewCacheInfo.count; 464 } 465 if (indicesToLoadCount > 0) { 466 synchronized (mViewCacheLoadIndices) { 467 mViewCacheLoadIndices.clear(); 468 for (int i = 0; i < indicesToLoadCount; ++i) { 469 final int index = mTmpViewCacheLoadIndices[i]; 470 if (0 <= index && index < length) { 471 mViewCacheLoadIndices.addLast(index); 472 } 473 } 474 } 475 } 476 477 // return null so that a dummy view can be retrieved 478 return null; 479 } 480 481 public View getView(int position, View convertView, ViewGroup parent) { 482 if (mServiceConnection.isConnected()) { 483 // create the flipper views if necessary (we have to do this now 484 // for all the flippers while we have the reference to the parent) 485 initializeLoadingViews(parent); 486 487 // request the item from the cache (queueing it to load if not 488 // in the cache already) 489 RemoteViewsIndexInfo indexInfo = requestCachedIndexInfo(position); 490 491 // update the flipper appropriately 492 synchronized (mViewCache) { 493 int cacheIndex = getCacheIndex(position); 494 FrameLayout flipper = mViewCache[cacheIndex].flipper; 495 flipper.setVisibility(View.VISIBLE); 496 flipper.setAlpha(1.0f); 497 498 if (indexInfo == null) { 499 // hide the item view and show the loading view 500 flipper.getChildAt(0).setVisibility(View.VISIBLE); 501 for (int i = 1; i < flipper.getChildCount(); ++i) { 502 flipper.getChildAt(i).setVisibility(View.GONE); 503 } 504 } else { 505 // hide the loading view and show the item view 506 for (int i = 0; i < flipper.getChildCount() - 1; ++i) { 507 flipper.getChildAt(i).setVisibility(View.GONE); 508 } 509 flipper.getChildAt(flipper.getChildCount() - 1).setVisibility(View.VISIBLE); 510 } 511 return flipper; 512 } 513 } 514 return new View(mContext); 515 } 516 517 private void initializeLoadingViews(ViewGroup parent) { 518 // ensure that the cache has the appropriate initial flipper 519 synchronized (mViewCache) { 520 if (mViewCache[0].flipper == null) { 521 for (int i = 0; i < mViewCache.length; ++i) { 522 FrameLayout flipper = new FrameLayout(mContext); 523 if (mUserLoadingView != null) { 524 // use the user-specified loading view 525 flipper.addView(mUserLoadingView.apply(mContext, parent)); 526 } else { 527 // calculate the original size of the first row for the loader view 528 synchronized (mViewCacheInfo) { 529 if (mFirstViewHeight < 0) { 530 View firstView = mFirstView.apply(mContext, parent); 531 firstView.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), 532 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); 533 mFirstViewHeight = firstView.getMeasuredHeight(); 534 } 535 } 536 537 // construct a new loader and add it to the flipper as the fallback 538 // default view 539 TextView textView = new TextView(mContext); 540 textView.setText("Loading..."); 541 textView.setHeight(mFirstViewHeight); 542 textView.setGravity(Gravity.CENTER_HORIZONTAL | Gravity.CENTER_VERTICAL); 543 textView.setTextSize(18.0f); 544 textView.setTextColor(Color.argb(96, 255, 255, 255)); 545 textView.setShadowLayer(2.0f, 0.0f, 1.0f, Color.BLACK); 546 547 flipper.addView(textView); 548 } 549 mViewCache[i].flipper = flipper; 550 } 551 } 552 } 553 } 554 555 public void startBackgroundLoader() { 556 // initialize the worker runnable 557 mBackgroundLoaderEnabled = true; 558 mWorkerQueue.post(new Runnable() { 559 @Override 560 public void run() { 561 while (mBackgroundLoaderEnabled) { 562 // notify the RemoteViews factory if necessary 563 boolean isDataDirty = false; 564 synchronized (mViewCacheInfo) { 565 isDataDirty = mViewCacheInfo.isDataDirty; 566 mViewCacheInfo.isDataDirty = false; 567 } 568 if (isDataDirty) { 569 updateNotifyDataSetChanged(); 570 } 571 572 int index = -1; 573 synchronized (mViewCacheLoadIndices) { 574 if (!mViewCacheLoadIndices.isEmpty()) { 575 index = mViewCacheLoadIndices.removeFirst(); 576 } 577 } 578 if (index < 0) { 579 // there were no items to load, so sleep for a bit 580 try { 581 Thread.sleep(10); 582 } catch (InterruptedException e) { 583 e.printStackTrace(); 584 } 585 } else { 586 // otherwise, try and load the item 587 updateRemoteViewsInfo(index); 588 } 589 } 590 } 591 }); 592 } 593 594 public void stopBackgroundLoader() { 595 // clear the items to be loaded 596 mBackgroundLoaderEnabled = false; 597 synchronized (mViewCacheLoadIndices) { 598 mViewCacheLoadIndices.clear(); 599 } 600 } 601 602 public long getItemId(int position) { 603 synchronized (mViewCache) { 604 if (containsAndIsValid(position)) { 605 return mViewCache[getCacheIndex(position)].itemId; 606 } 607 } 608 return 0; 609 } 610 611 public int getItemViewType(int position) { 612 // synchronize to ensure that the type id/index map is updated synchronously 613 synchronized (mViewCache) { 614 if (containsAndIsValid(position)) { 615 int viewId = mViewCache[getCacheIndex(position)].typeId; 616 Map<Integer, Integer> typeMap = mViewCacheInfo.mTypeIdIndexMap; 617 // we +1 because the default dummy view get view type 0 618 if (typeMap.containsKey(viewId)) { 619 return typeMap.get(viewId); 620 } else { 621 int newIndex = typeMap.size() + 1; 622 typeMap.put(viewId, newIndex); 623 return newIndex; 624 } 625 } 626 } 627 // return the type of the default item 628 return 0; 629 } 630 631 public int getCount() { 632 synchronized (mViewCacheInfo) { 633 return mViewCacheInfo.count; 634 } 635 } 636 637 public int getViewTypeCount() { 638 synchronized (mViewCacheInfo) { 639 return mViewCacheInfo.viewTypeCount; 640 } 641 } 642 643 public boolean hasStableIds() { 644 synchronized (mViewCacheInfo) { 645 return mViewCacheInfo.hasStableIds; 646 } 647 } 648 649 public void flushCache() { 650 // clear the items to be loaded 651 synchronized (mViewCacheLoadIndices) { 652 mViewCacheLoadIndices.clear(); 653 } 654 655 synchronized (mViewCache) { 656 // flush the internal cache and invalidate the adapter for future loads 657 mMainQueue.removeMessages(0); 658 659 for (int i = 0; i < mViewCache.length; ++i) { 660 mViewCache[i].invalidate(); 661 } 662 663 mViewCacheStartPosition = 0; 664 mViewCacheEndPosition = -1; 665 } 666 } 667 } 668 669 public RemoteViewsAdapter(Context context, Intent intent, RemoteAdapterConnectionCallback callback) { 670 mContext = context; 671 mIntent = intent; 672 if (mIntent == null) { 673 throw new IllegalArgumentException("Non-null Intent must be specified."); 674 } 675 676 // initialize the worker thread 677 mWorkerThread = new HandlerThread("RemoteViewsCache-loader"); 678 mWorkerThread.start(); 679 mWorkerQueue = new Handler(mWorkerThread.getLooper()); 680 mWorkerPriorityQueue = new Handler(mWorkerThread.getLooper()); 681 mMainQueue = new Handler(Looper.myLooper()); 682 mMainPriorityQueue = new Handler(Looper.myLooper()); 683 684 // initialize the cache and the service connection on startup 685 mViewCache = new RemoteViewsCache(25); 686 mServiceConnection = new RemoteViewsAdapterServiceConnection(callback); 687 requestBindService(); 688 } 689 690 protected void finalize() throws Throwable { 691 // remember to unbind from the service when finalizing 692 unbindService(); 693 } 694 695 public Intent getRemoteViewsServiceIntent() { 696 return mIntent; 697 } 698 699 public int getCount() { 700 requestBindService(); 701 return mViewCache.getCount(); 702 } 703 704 public Object getItem(int position) { 705 // disallow arbitrary object to be associated with an item for the time being 706 return null; 707 } 708 709 public long getItemId(int position) { 710 requestBindService(); 711 return mViewCache.getItemId(position); 712 } 713 714 public int getItemViewType(int position) { 715 requestBindService(); 716 return mViewCache.getItemViewType(position); 717 } 718 719 public View getView(int position, View convertView, ViewGroup parent) { 720 requestBindService(); 721 return mViewCache.getView(position, convertView, parent); 722 } 723 724 public int getViewTypeCount() { 725 requestBindService(); 726 return mViewCache.getViewTypeCount(); 727 } 728 729 public boolean hasStableIds() { 730 requestBindService(); 731 return mViewCache.hasStableIds(); 732 } 733 734 public boolean isEmpty() { 735 return getCount() <= 0; 736 } 737 738 public void notifyDataSetChanged() { 739 // flush the cache so that we can reload new items from the service 740 mViewCache.flushCache(); 741 742 // notify the factory that it's data may no longer be valid 743 mViewCache.onNotifyDataSetChanged(); 744 } 745 746 public void completeNotifyDataSetChanged() { 747 super.notifyDataSetChanged(); 748 } 749 750 private boolean requestBindService() { 751 // try binding the service (which will start it if it's not already running) 752 if (!mServiceConnection.isConnected()) { 753 mContext.bindService(mIntent, mServiceConnection, Context.BIND_AUTO_CREATE); 754 } 755 756 return mServiceConnection.isConnected(); 757 } 758 759 private void unbindService() { 760 if (mServiceConnection.isConnected()) { 761 mContext.unbindService(mServiceConnection); 762 } 763 } 764} 765