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