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