PhotoDataAdapter.java revision d9355113da391f8bbddef1d2a2126ce6edc72291
1/* 2 * Copyright (C) 2010 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 com.android.gallery3d.app; 18 19import android.graphics.Bitmap; 20import android.graphics.BitmapRegionDecoder; 21import android.os.Handler; 22import android.os.Message; 23 24import com.android.gallery3d.common.BitmapUtils; 25import com.android.gallery3d.common.Utils; 26import com.android.gallery3d.data.ContentListener; 27import com.android.gallery3d.data.DataManager; 28import com.android.gallery3d.data.MediaItem; 29import com.android.gallery3d.data.MediaObject; 30import com.android.gallery3d.data.MediaSet; 31import com.android.gallery3d.data.Path; 32import com.android.gallery3d.ui.BitmapScreenNail; 33import com.android.gallery3d.ui.PhotoView; 34import com.android.gallery3d.ui.ScreenNail; 35import com.android.gallery3d.ui.SynchronizedHandler; 36import com.android.gallery3d.ui.TileImageViewAdapter; 37import com.android.gallery3d.util.Future; 38import com.android.gallery3d.util.FutureListener; 39import com.android.gallery3d.util.ThreadPool; 40import com.android.gallery3d.util.ThreadPool.Job; 41import com.android.gallery3d.util.ThreadPool.JobContext; 42 43import java.util.ArrayList; 44import java.util.Arrays; 45import java.util.HashMap; 46import java.util.HashSet; 47import java.util.concurrent.Callable; 48import java.util.concurrent.ExecutionException; 49import java.util.concurrent.FutureTask; 50 51public class PhotoDataAdapter implements PhotoPage.Model { 52 @SuppressWarnings("unused") 53 private static final String TAG = "PhotoDataAdapter"; 54 55 private static final int MSG_LOAD_START = 1; 56 private static final int MSG_LOAD_FINISH = 2; 57 private static final int MSG_RUN_OBJECT = 3; 58 private static final int MSG_UPDATE_IMAGE_REQUESTS = 4; 59 60 private static final int MIN_LOAD_COUNT = 8; 61 private static final int DATA_CACHE_SIZE = 32; 62 private static final int SCREEN_NAIL_MAX = PhotoView.SCREEN_NAIL_MAX; 63 private static final int IMAGE_CACHE_SIZE = 2 * SCREEN_NAIL_MAX + 1; 64 65 private static final int BIT_SCREEN_NAIL = 1; 66 private static final int BIT_FULL_IMAGE = 2; 67 68 // sImageFetchSeq is the fetching sequence for images. 69 // We want to fetch the current screennail first (offset = 0), the next 70 // screennail (offset = +1), then the previous screennail (offset = -1) etc. 71 // After all the screennail are fetched, we fetch the full images (only some 72 // of them because of we don't want to use too much memory). 73 private static ImageFetch[] sImageFetchSeq; 74 75 private static class ImageFetch { 76 int indexOffset; 77 int imageBit; 78 public ImageFetch(int offset, int bit) { 79 indexOffset = offset; 80 imageBit = bit; 81 } 82 } 83 84 static { 85 int k = 0; 86 sImageFetchSeq = new ImageFetch[1 + (IMAGE_CACHE_SIZE - 1) * 2 + 3]; 87 sImageFetchSeq[k++] = new ImageFetch(0, BIT_SCREEN_NAIL); 88 89 for (int i = 1; i < IMAGE_CACHE_SIZE; ++i) { 90 sImageFetchSeq[k++] = new ImageFetch(i, BIT_SCREEN_NAIL); 91 sImageFetchSeq[k++] = new ImageFetch(-i, BIT_SCREEN_NAIL); 92 } 93 94 sImageFetchSeq[k++] = new ImageFetch(0, BIT_FULL_IMAGE); 95 sImageFetchSeq[k++] = new ImageFetch(1, BIT_FULL_IMAGE); 96 sImageFetchSeq[k++] = new ImageFetch(-1, BIT_FULL_IMAGE); 97 } 98 99 private final TileImageViewAdapter mTileProvider = new TileImageViewAdapter(); 100 101 // PhotoDataAdapter caches MediaItems (data) and ImageEntries (image). 102 // 103 // The MediaItems are stored in the mData array, which has DATA_CACHE_SIZE 104 // entries. The valid index range are [mContentStart, mContentEnd). We keep 105 // mContentEnd - mContentStart <= DATA_CACHE_SIZE, so we can use 106 // (i % DATA_CACHE_SIZE) as index to the array. 107 // 108 // The valid MediaItem window size (mContentEnd - mContentStart) may be 109 // smaller than DATA_CACHE_SIZE because we only update the window and reload 110 // the MediaItems when there are significant changes to the window position 111 // (>= MIN_LOAD_COUNT). 112 private final MediaItem mData[] = new MediaItem[DATA_CACHE_SIZE]; 113 private int mContentStart = 0; 114 private int mContentEnd = 0; 115 116 /* 117 * The ImageCache is a version-to-ImageEntry map. It only holds 118 * the ImageEntries in the range of [mActiveStart, mActiveEnd). 119 * We also keep mActiveEnd - mActiveStart <= IMAGE_CACHE_SIZE. 120 * Besides, the [mActiveStart, mActiveEnd) range must be contained 121 * within the[mContentStart, mContentEnd) range. 122 */ 123 private HashMap<Long, ImageEntry> mImageCache = new HashMap<Long, ImageEntry>(); 124 private int mActiveStart = 0; 125 private int mActiveEnd = 0; 126 127 // mCurrentIndex is the "center" image the user is viewing. The change of 128 // mCurrentIndex triggers the data loading and image loading. 129 private int mCurrentIndex; 130 131 // mChanges keeps the version number (of MediaItem) about the previous, 132 // current, and next image. If the version number changes, we notify the 133 // view. This is used after a database reload or mCurrentIndex changes. 134 private final long mChanges[] = new long[IMAGE_CACHE_SIZE]; 135 136 private final Handler mMainHandler; 137 private final ThreadPool mThreadPool; 138 139 private final PhotoView mPhotoView; 140 private final MediaSet mSource; 141 private ReloadTask mReloadTask; 142 143 private long mSourceVersion = MediaObject.INVALID_DATA_VERSION; 144 private int mSize = 0; 145 private Path mItemPath; 146 private int mCameraIndex; 147 private boolean mIsActive; 148 private boolean mNeedFullImage; 149 150 public interface DataListener extends LoadingListener { 151 public void onPhotoChanged(int index, Path item); 152 } 153 154 private DataListener mDataListener; 155 156 private final SourceListener mSourceListener = new SourceListener(); 157 158 // The path of the current viewing item will be stored in mItemPath. 159 // If mItemPath is not null, mCurrentIndex is only a hint for where we 160 // can find the item. If mItemPath is null, then we use the mCurrentIndex to 161 // find the image being viewed. 162 public PhotoDataAdapter(GalleryActivity activity, PhotoView view, 163 MediaSet mediaSet, Path itemPath, int indexHint, int cameraIndex) { 164 mSource = Utils.checkNotNull(mediaSet); 165 mPhotoView = Utils.checkNotNull(view); 166 mItemPath = Utils.checkNotNull(itemPath); 167 mCurrentIndex = indexHint; 168 mCameraIndex = cameraIndex; 169 mThreadPool = activity.getThreadPool(); 170 mNeedFullImage = true; 171 172 Arrays.fill(mChanges, MediaObject.INVALID_DATA_VERSION); 173 174 mMainHandler = new SynchronizedHandler(activity.getGLRoot()) { 175 @SuppressWarnings("unchecked") 176 @Override 177 public void handleMessage(Message message) { 178 switch (message.what) { 179 case MSG_RUN_OBJECT: 180 ((Runnable) message.obj).run(); 181 return; 182 case MSG_LOAD_START: { 183 if (mDataListener != null) mDataListener.onLoadingStarted(); 184 return; 185 } 186 case MSG_LOAD_FINISH: { 187 if (mDataListener != null) mDataListener.onLoadingFinished(); 188 return; 189 } 190 case MSG_UPDATE_IMAGE_REQUESTS: { 191 updateImageRequests(); 192 return; 193 } 194 default: throw new AssertionError(); 195 } 196 } 197 }; 198 199 updateSlidingWindow(); 200 } 201 202 private long getVersion(int index) { 203 if (index < 0 || index >= mSize) return MediaObject.INVALID_DATA_VERSION; 204 if (index >= mContentStart && index < mContentEnd) { 205 MediaItem item = mData[index % DATA_CACHE_SIZE]; 206 if (item != null) return item.getDataVersion(); 207 } 208 return MediaObject.INVALID_DATA_VERSION; 209 } 210 211 private void fireDataChange() { 212 for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; ++i) { 213 mChanges[i + SCREEN_NAIL_MAX] = getVersion(mCurrentIndex + i); 214 } 215 mPhotoView.notifyDataChange(mChanges, -mCurrentIndex, 216 mSize - 1 - mCurrentIndex); 217 } 218 219 public void setDataListener(DataListener listener) { 220 mDataListener = listener; 221 } 222 223 private void updateScreenNail(long version, Future<ScreenNail> future) { 224 ImageEntry entry = mImageCache.get(version); 225 ScreenNail screenNail = future.get(); 226 227 if (entry == null || entry.screenNailTask != future) { 228 if (screenNail != null) screenNail.recycle(); 229 return; 230 } 231 232 entry.screenNailTask = null; 233 Utils.assertTrue(entry.screenNail == null); 234 entry.screenNail = screenNail; 235 236 if (screenNail == null) { 237 entry.failToLoad = true; 238 } 239 240 for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; ++i) { 241 if (version == getVersion(mCurrentIndex + i)) { 242 if (i == 0) updateTileProvider(entry); 243 mPhotoView.notifyImageChange(i); 244 break; 245 } 246 } 247 updateImageRequests(); 248 } 249 250 private void updateFullImage(long version, Future<BitmapRegionDecoder> future) { 251 ImageEntry entry = mImageCache.get(version); 252 if (entry == null || entry.fullImageTask != future) { 253 BitmapRegionDecoder fullImage = future.get(); 254 if (fullImage != null) fullImage.recycle(); 255 return; 256 } 257 258 entry.fullImageTask = null; 259 entry.fullImage = future.get(); 260 if (entry.fullImage != null) { 261 if (version == getVersion(mCurrentIndex)) { 262 updateTileProvider(entry); 263 mPhotoView.notifyImageChange(0); 264 } 265 } 266 updateImageRequests(); 267 } 268 269 public void resume() { 270 mIsActive = true; 271 mSource.addContentListener(mSourceListener); 272 updateImageCache(); 273 updateImageRequests(); 274 275 mReloadTask = new ReloadTask(); 276 mReloadTask.start(); 277 278 fireDataChange(); 279 } 280 281 public void pause() { 282 mIsActive = false; 283 284 mReloadTask.terminate(); 285 mReloadTask = null; 286 287 mSource.removeContentListener(mSourceListener); 288 289 for (ImageEntry entry : mImageCache.values()) { 290 if (entry.fullImageTask != null) entry.fullImageTask.cancel(); 291 if (entry.screenNailTask != null) entry.screenNailTask.cancel(); 292 if (entry.screenNail != null) entry.screenNail.recycle(); 293 } 294 mImageCache.clear(); 295 mTileProvider.clear(); 296 } 297 298 private ScreenNail getImage(int index) { 299 if (index < 0 || index >= mSize || !mIsActive) return null; 300 Utils.assertTrue(index >= mActiveStart && index < mActiveEnd); 301 302 ImageEntry entry = mImageCache.get(getVersion(index)); 303 return entry == null ? null : entry.screenNail; 304 } 305 306 private MediaItem getItem(int index) { 307 if (index < 0 || index >= mSize || !mIsActive) return null; 308 Utils.assertTrue(index >= mActiveStart && index < mActiveEnd); 309 310 if (index >= mContentStart && index < mContentEnd) { 311 return mData[index % DATA_CACHE_SIZE]; 312 } 313 return null; 314 } 315 316 private void updateCurrentIndex(int index) { 317 if (mCurrentIndex == index) return; 318 mCurrentIndex = index; 319 updateSlidingWindow(); 320 321 MediaItem item = mData[index % DATA_CACHE_SIZE]; 322 mItemPath = item == null ? null : item.getPath(); 323 324 updateImageCache(); 325 updateImageRequests(); 326 updateTileProvider(); 327 328 if (mDataListener != null) { 329 mDataListener.onPhotoChanged(index, mItemPath); 330 } 331 332 fireDataChange(); 333 } 334 335 @Override 336 public void moveTo(int index) { 337 updateCurrentIndex(index); 338 } 339 340 @Override 341 public ScreenNail getScreenNail(int offset) { 342 return getImage(mCurrentIndex + offset); 343 } 344 345 @Override 346 public void getImageSize(int offset, PhotoView.Size size) { 347 MediaItem item = getItem(mCurrentIndex + offset); 348 if (item == null) { 349 size.width = 0; 350 size.height = 0; 351 } else { 352 size.width = item.getWidth(); 353 size.height = item.getHeight(); 354 } 355 } 356 357 @Override 358 public int getImageRotation(int offset) { 359 MediaItem item = getItem(mCurrentIndex + offset); 360 return (item == null) ? 0 : item.getFullImageRotation(); 361 } 362 363 @Override 364 public void setNeedFullImage(boolean enabled) { 365 mNeedFullImage = enabled; 366 mMainHandler.sendEmptyMessage(MSG_UPDATE_IMAGE_REQUESTS); 367 } 368 369 @Override 370 public boolean isCamera(int offset) { 371 return mCurrentIndex + offset == mCameraIndex; 372 } 373 374 @Override 375 public boolean isVideo(int offset) { 376 MediaItem item = getItem(mCurrentIndex + offset); 377 return (item == null) 378 ? false 379 : item.getMediaType() == MediaItem.MEDIA_TYPE_VIDEO; 380 } 381 382 public ScreenNail getScreenNail() { 383 return mTileProvider.getScreenNail(); 384 } 385 386 public int getImageHeight() { 387 return mTileProvider.getImageHeight(); 388 } 389 390 public int getImageWidth() { 391 return mTileProvider.getImageWidth(); 392 } 393 394 public int getLevelCount() { 395 return mTileProvider.getLevelCount(); 396 } 397 398 public Bitmap getTile(int level, int x, int y, int tileSize, 399 int borderSize) { 400 return mTileProvider.getTile(level, x, y, tileSize, borderSize); 401 } 402 403 public boolean isFailedToLoad() { 404 return mTileProvider.isFailedToLoad(); 405 } 406 407 public boolean isEmpty() { 408 return mSize == 0; 409 } 410 411 public int getCurrentIndex() { 412 return mCurrentIndex; 413 } 414 415 public MediaItem getCurrentMediaItem() { 416 return mData[mCurrentIndex % DATA_CACHE_SIZE]; 417 } 418 419 public void setCurrentPhoto(Path path, int indexHint) { 420 if (mItemPath == path) return; 421 mItemPath = path; 422 mCurrentIndex = indexHint; 423 updateSlidingWindow(); 424 updateImageCache(); 425 fireDataChange(); 426 427 // We need to reload content if the path doesn't match. 428 MediaItem item = getCurrentMediaItem(); 429 if (item != null && item.getPath() != path) { 430 if (mReloadTask != null) mReloadTask.notifyDirty(); 431 } 432 } 433 434 private void updateTileProvider() { 435 ImageEntry entry = mImageCache.get(getVersion(mCurrentIndex)); 436 if (entry == null) { // in loading 437 mTileProvider.clear(); 438 } else { 439 updateTileProvider(entry); 440 } 441 } 442 443 private void updateTileProvider(ImageEntry entry) { 444 ScreenNail screenNail = entry.screenNail; 445 BitmapRegionDecoder fullImage = entry.fullImage; 446 if (screenNail != null) { 447 if (fullImage != null) { 448 mTileProvider.setScreenNail(screenNail, 449 fullImage.getWidth(), fullImage.getHeight()); 450 mTileProvider.setRegionDecoder(fullImage); 451 } else { 452 int width = screenNail.getWidth(); 453 int height = screenNail.getHeight(); 454 mTileProvider.setScreenNail(screenNail, width, height); 455 } 456 } else { 457 mTileProvider.clear(); 458 if (entry.failToLoad) mTileProvider.setFailedToLoad(); 459 } 460 } 461 462 private void updateSlidingWindow() { 463 // 1. Update the image window 464 int start = Utils.clamp(mCurrentIndex - IMAGE_CACHE_SIZE / 2, 465 0, Math.max(0, mSize - IMAGE_CACHE_SIZE)); 466 int end = Math.min(mSize, start + IMAGE_CACHE_SIZE); 467 468 if (mActiveStart == start && mActiveEnd == end) return; 469 470 mActiveStart = start; 471 mActiveEnd = end; 472 473 // 2. Update the data window 474 start = Utils.clamp(mCurrentIndex - DATA_CACHE_SIZE / 2, 475 0, Math.max(0, mSize - DATA_CACHE_SIZE)); 476 end = Math.min(mSize, start + DATA_CACHE_SIZE); 477 if (mContentStart > mActiveStart || mContentEnd < mActiveEnd 478 || Math.abs(start - mContentStart) > MIN_LOAD_COUNT) { 479 for (int i = mContentStart; i < mContentEnd; ++i) { 480 if (i < start || i >= end) { 481 mData[i % DATA_CACHE_SIZE] = null; 482 } 483 } 484 mContentStart = start; 485 mContentEnd = end; 486 if (mReloadTask != null) mReloadTask.notifyDirty(); 487 } 488 } 489 490 private void updateImageRequests() { 491 if (!mIsActive) return; 492 493 int currentIndex = mCurrentIndex; 494 MediaItem item = mData[currentIndex % DATA_CACHE_SIZE]; 495 if (item == null || item.getPath() != mItemPath) { 496 // current item mismatch - don't request image 497 return; 498 } 499 500 // 1. Find the most wanted request and start it (if not already started). 501 Future<?> task = null; 502 for (int i = 0; i < sImageFetchSeq.length; i++) { 503 int offset = sImageFetchSeq[i].indexOffset; 504 int bit = sImageFetchSeq[i].imageBit; 505 if (bit == BIT_FULL_IMAGE && !mNeedFullImage) continue; 506 task = startTaskIfNeeded(currentIndex + offset, bit); 507 if (task != null) break; 508 } 509 510 // 2. Cancel everything else. 511 for (ImageEntry entry : mImageCache.values()) { 512 if (entry.screenNailTask != null && entry.screenNailTask != task) { 513 entry.screenNailTask.cancel(); 514 entry.screenNailTask = null; 515 entry.requestedBits &= ~BIT_SCREEN_NAIL; 516 } 517 if (entry.fullImageTask != null && entry.fullImageTask != task) { 518 entry.fullImageTask.cancel(); 519 entry.fullImageTask = null; 520 entry.requestedBits &= ~BIT_FULL_IMAGE; 521 } 522 } 523 } 524 525 private static class ScreenNailJob implements Job<ScreenNail> { 526 private MediaItem mItem; 527 528 public ScreenNailJob(MediaItem item) { 529 mItem = item; 530 } 531 532 @Override 533 public ScreenNail run(JobContext jc) { 534 // We try to get a ScreenNail first, if it fails, we fallback to get 535 // a Bitmap and then wrap it in a BitmapScreenNail instead. 536 ScreenNail s = mItem.getScreenNail(); 537 if (s != null) return s; 538 539 Bitmap bitmap = mItem.requestImage(MediaItem.TYPE_THUMBNAIL).run(jc); 540 if (jc.isCancelled()) return null; 541 if (bitmap != null) { 542 bitmap = BitmapUtils.rotateBitmap(bitmap, 543 mItem.getRotation() - mItem.getFullImageRotation(), true); 544 } 545 return new BitmapScreenNail(bitmap); 546 } 547 } 548 549 // Returns the task if we started the task or the task is already started. 550 private Future<?> startTaskIfNeeded(int index, int which) { 551 if (index < mActiveStart || index >= mActiveEnd) return null; 552 553 ImageEntry entry = mImageCache.get(getVersion(index)); 554 if (entry == null) return null; 555 556 if (which == BIT_SCREEN_NAIL && entry.screenNailTask != null) { 557 return entry.screenNailTask; 558 } else if (which == BIT_FULL_IMAGE && entry.fullImageTask != null) { 559 return entry.fullImageTask; 560 } 561 562 MediaItem item = mData[index % DATA_CACHE_SIZE]; 563 Utils.assertTrue(item != null); 564 565 if (which == BIT_SCREEN_NAIL 566 && (entry.requestedBits & BIT_SCREEN_NAIL) == 0) { 567 entry.requestedBits |= BIT_SCREEN_NAIL; 568 entry.screenNailTask = mThreadPool.submit( 569 new ScreenNailJob(item), 570 new ScreenNailListener(item.getDataVersion())); 571 // request screen nail 572 return entry.screenNailTask; 573 } 574 if (which == BIT_FULL_IMAGE 575 && (entry.requestedBits & BIT_FULL_IMAGE) == 0 576 && (item.getSupportedOperations() 577 & MediaItem.SUPPORT_FULL_IMAGE) != 0) { 578 entry.requestedBits |= BIT_FULL_IMAGE; 579 entry.fullImageTask = mThreadPool.submit( 580 item.requestLargeImage(), 581 new FullImageListener(item.getDataVersion())); 582 // request full image 583 return entry.fullImageTask; 584 } 585 return null; 586 } 587 588 private void updateImageCache() { 589 HashSet<Long> toBeRemoved = new HashSet<Long>(mImageCache.keySet()); 590 for (int i = mActiveStart; i < mActiveEnd; ++i) { 591 MediaItem item = mData[i % DATA_CACHE_SIZE]; 592 long version = item == null 593 ? MediaObject.INVALID_DATA_VERSION 594 : item.getDataVersion(); 595 if (version == MediaObject.INVALID_DATA_VERSION) continue; 596 ImageEntry entry = mImageCache.get(version); 597 toBeRemoved.remove(version); 598 if (entry != null) { 599 if (Math.abs(i - mCurrentIndex) > 1) { 600 if (entry.fullImageTask != null) { 601 entry.fullImageTask.cancel(); 602 entry.fullImageTask = null; 603 } 604 entry.fullImage = null; 605 entry.requestedBits &= ~BIT_FULL_IMAGE; 606 } 607 } else { 608 entry = new ImageEntry(); 609 mImageCache.put(version, entry); 610 } 611 } 612 613 // Clear the data and requests for ImageEntries outside the new window. 614 for (Long version : toBeRemoved) { 615 ImageEntry entry = mImageCache.remove(version); 616 if (entry.fullImageTask != null) entry.fullImageTask.cancel(); 617 if (entry.screenNailTask != null) entry.screenNailTask.cancel(); 618 if (entry.screenNail != null) entry.screenNail.recycle(); 619 } 620 } 621 622 private class FullImageListener 623 implements Runnable, FutureListener<BitmapRegionDecoder> { 624 private final long mVersion; 625 private Future<BitmapRegionDecoder> mFuture; 626 627 public FullImageListener(long version) { 628 mVersion = version; 629 } 630 631 @Override 632 public void onFutureDone(Future<BitmapRegionDecoder> future) { 633 mFuture = future; 634 mMainHandler.sendMessage( 635 mMainHandler.obtainMessage(MSG_RUN_OBJECT, this)); 636 } 637 638 @Override 639 public void run() { 640 updateFullImage(mVersion, mFuture); 641 } 642 } 643 644 private class ScreenNailListener 645 implements Runnable, FutureListener<ScreenNail> { 646 private final long mVersion; 647 private Future<ScreenNail> mFuture; 648 649 public ScreenNailListener(long version) { 650 mVersion = version; 651 } 652 653 @Override 654 public void onFutureDone(Future<ScreenNail> future) { 655 mFuture = future; 656 mMainHandler.sendMessage( 657 mMainHandler.obtainMessage(MSG_RUN_OBJECT, this)); 658 } 659 660 @Override 661 public void run() { 662 updateScreenNail(mVersion, mFuture); 663 } 664 } 665 666 private static class ImageEntry { 667 public int requestedBits = 0; 668 public BitmapRegionDecoder fullImage; 669 public ScreenNail screenNail; 670 public Future<ScreenNail> screenNailTask; 671 public Future<BitmapRegionDecoder> fullImageTask; 672 public boolean failToLoad = false; 673 } 674 675 private class SourceListener implements ContentListener { 676 public void onContentDirty() { 677 if (mReloadTask != null) mReloadTask.notifyDirty(); 678 } 679 } 680 681 private <T> T executeAndWait(Callable<T> callable) { 682 FutureTask<T> task = new FutureTask<T>(callable); 683 mMainHandler.sendMessage( 684 mMainHandler.obtainMessage(MSG_RUN_OBJECT, task)); 685 try { 686 return task.get(); 687 } catch (InterruptedException e) { 688 return null; 689 } catch (ExecutionException e) { 690 throw new RuntimeException(e); 691 } 692 } 693 694 private static class UpdateInfo { 695 public long version; 696 public boolean reloadContent; 697 public Path target; 698 public int indexHint; 699 public int contentStart; 700 public int contentEnd; 701 702 public int size; 703 public ArrayList<MediaItem> items; 704 } 705 706 private class GetUpdateInfo implements Callable<UpdateInfo> { 707 708 private boolean needContentReload() { 709 for (int i = mContentStart, n = mContentEnd; i < n; ++i) { 710 if (mData[i % DATA_CACHE_SIZE] == null) return true; 711 } 712 MediaItem current = mData[mCurrentIndex % DATA_CACHE_SIZE]; 713 return current == null || current.getPath() != mItemPath; 714 } 715 716 @Override 717 public UpdateInfo call() throws Exception { 718 // TODO: Try to load some data in first update 719 UpdateInfo info = new UpdateInfo(); 720 info.version = mSourceVersion; 721 info.reloadContent = needContentReload(); 722 info.target = mItemPath; 723 info.indexHint = mCurrentIndex; 724 info.contentStart = mContentStart; 725 info.contentEnd = mContentEnd; 726 info.size = mSize; 727 return info; 728 } 729 } 730 731 private class UpdateContent implements Callable<Void> { 732 UpdateInfo mUpdateInfo; 733 734 public UpdateContent(UpdateInfo updateInfo) { 735 mUpdateInfo = updateInfo; 736 } 737 738 @Override 739 public Void call() throws Exception { 740 UpdateInfo info = mUpdateInfo; 741 mSourceVersion = info.version; 742 743 if (info.size != mSize) { 744 mSize = info.size; 745 if (mContentEnd > mSize) mContentEnd = mSize; 746 if (mActiveEnd > mSize) mActiveEnd = mSize; 747 } 748 749 if (info.indexHint == MediaSet.INDEX_NOT_FOUND) { 750 // The image has been deleted, clear mItemPath, the 751 // mCurrentIndex will be updated in the updateCurrentItem(). 752 mItemPath = null; 753 updateCurrentItem(); 754 } else { 755 mCurrentIndex = info.indexHint; 756 } 757 758 updateSlidingWindow(); 759 760 if (info.items != null) { 761 int start = Math.max(info.contentStart, mContentStart); 762 int end = Math.min(info.contentStart + info.items.size(), mContentEnd); 763 int dataIndex = start % DATA_CACHE_SIZE; 764 for (int i = start; i < end; ++i) { 765 mData[dataIndex] = info.items.get(i - info.contentStart); 766 if (++dataIndex == DATA_CACHE_SIZE) dataIndex = 0; 767 } 768 } 769 if (mItemPath == null) { 770 MediaItem current = mData[mCurrentIndex % DATA_CACHE_SIZE]; 771 mItemPath = current == null ? null : current.getPath(); 772 } 773 updateImageCache(); 774 updateTileProvider(); 775 updateImageRequests(); 776 fireDataChange(); 777 return null; 778 } 779 780 private void updateCurrentItem() { 781 if (mSize == 0) return; 782 if (mCurrentIndex >= mSize) { 783 mCurrentIndex = mSize - 1; 784 } 785 fireDataChange(); 786 } 787 } 788 789 private class ReloadTask extends Thread { 790 private volatile boolean mActive = true; 791 private volatile boolean mDirty = true; 792 793 private boolean mIsLoading = false; 794 795 private void updateLoading(boolean loading) { 796 if (mIsLoading == loading) return; 797 mIsLoading = loading; 798 mMainHandler.sendEmptyMessage(loading ? MSG_LOAD_START : MSG_LOAD_FINISH); 799 } 800 801 @Override 802 public void run() { 803 while (mActive) { 804 synchronized (this) { 805 if (!mDirty && mActive) { 806 updateLoading(false); 807 Utils.waitWithoutInterrupt(this); 808 continue; 809 } 810 } 811 mDirty = false; 812 UpdateInfo info = executeAndWait(new GetUpdateInfo()); 813 synchronized (DataManager.LOCK) { 814 updateLoading(true); 815 long version = mSource.reload(); 816 if (info.version != version) { 817 info.reloadContent = true; 818 info.size = mSource.getMediaItemCount(); 819 } 820 if (!info.reloadContent) continue; 821 info.items = mSource.getMediaItem(info.contentStart, info.contentEnd); 822 MediaItem item = findCurrentMediaItem(info); 823 if (item == null || item.getPath() != info.target) { 824 info.indexHint = findIndexOfTarget(info); 825 } 826 } 827 executeAndWait(new UpdateContent(info)); 828 } 829 } 830 831 public synchronized void notifyDirty() { 832 mDirty = true; 833 notifyAll(); 834 } 835 836 public synchronized void terminate() { 837 mActive = false; 838 notifyAll(); 839 } 840 841 private MediaItem findCurrentMediaItem(UpdateInfo info) { 842 ArrayList<MediaItem> items = info.items; 843 int index = info.indexHint - info.contentStart; 844 return index < 0 || index >= items.size() ? null : items.get(index); 845 } 846 847 private int findIndexOfTarget(UpdateInfo info) { 848 if (info.target == null) return info.indexHint; 849 ArrayList<MediaItem> items = info.items; 850 851 // First, try to find the item in the data just loaded 852 if (items != null) { 853 for (int i = 0, n = items.size(); i < n; ++i) { 854 if (items.get(i).getPath() == info.target) return i + info.contentStart; 855 } 856 } 857 858 // Not found, find it in mSource. 859 return mSource.getIndexOfItem(info.target, info.indexHint); 860 } 861 } 862} 863