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 int borderSize) { 370 return mTileProvider.getTile(level, x, y, tileSize, borderSize); 371 } 372 373 public boolean isFailedToLoad() { 374 return mTileProvider.isFailedToLoad(); 375 } 376 377 public boolean isEmpty() { 378 return mSize == 0; 379 } 380 381 public int getCurrentIndex() { 382 return mCurrentIndex; 383 } 384 385 public MediaItem getCurrentMediaItem() { 386 return mData[mCurrentIndex % DATA_CACHE_SIZE]; 387 } 388 389 public void setCurrentPhoto(Path path, int indexHint) { 390 if (mItemPath == path) return; 391 mItemPath = path; 392 mCurrentIndex = indexHint; 393 updateSlidingWindow(); 394 updateImageCache(); 395 fireModelInvalidated(); 396 397 // We need to reload content if the path doesn't match. 398 MediaItem item = getCurrentMediaItem(); 399 if (item != null && item.getPath() != path) { 400 if (mReloadTask != null) mReloadTask.notifyDirty(); 401 } 402 } 403 404 private void updateTileProvider() { 405 ImageEntry entry = mImageCache.get(getVersion(mCurrentIndex)); 406 if (entry == null) { // in loading 407 mTileProvider.clear(); 408 } else { 409 updateTileProvider(entry); 410 } 411 } 412 413 private void updateTileProvider(ImageEntry entry) { 414 Bitmap screenNail = entry.screenNail; 415 BitmapRegionDecoder fullImage = entry.fullImage; 416 if (screenNail != null) { 417 if (fullImage != null) { 418 mTileProvider.setBackupImage(screenNail, 419 fullImage.getWidth(), fullImage.getHeight()); 420 mTileProvider.setRegionDecoder(fullImage); 421 } else { 422 int width = screenNail.getWidth(); 423 int height = screenNail.getHeight(); 424 mTileProvider.setBackupImage(screenNail, width, height); 425 } 426 } else { 427 mTileProvider.clear(); 428 if (entry.failToLoad) mTileProvider.setFailedToLoad(); 429 } 430 } 431 432 private void updateSlidingWindow() { 433 // 1. Update the image window 434 int start = Utils.clamp(mCurrentIndex - IMAGE_CACHE_SIZE / 2, 435 0, Math.max(0, mSize - IMAGE_CACHE_SIZE)); 436 int end = Math.min(mSize, start + IMAGE_CACHE_SIZE); 437 438 if (mActiveStart == start && mActiveEnd == end) return; 439 440 mActiveStart = start; 441 mActiveEnd = end; 442 443 // 2. Update the data window 444 start = Utils.clamp(mCurrentIndex - DATA_CACHE_SIZE / 2, 445 0, Math.max(0, mSize - DATA_CACHE_SIZE)); 446 end = Math.min(mSize, start + DATA_CACHE_SIZE); 447 if (mContentStart > mActiveStart || mContentEnd < mActiveEnd 448 || Math.abs(start - mContentStart) > MIN_LOAD_COUNT) { 449 for (int i = mContentStart; i < mContentEnd; ++i) { 450 if (i < start || i >= end) { 451 mData[i % DATA_CACHE_SIZE] = null; 452 } 453 } 454 mContentStart = start; 455 mContentEnd = end; 456 if (mReloadTask != null) mReloadTask.notifyDirty(); 457 } 458 } 459 460 private void updateImageRequests() { 461 if (!mIsActive) return; 462 463 int currentIndex = mCurrentIndex; 464 MediaItem item = mData[currentIndex % DATA_CACHE_SIZE]; 465 if (item == null || item.getPath() != mItemPath) { 466 // current item mismatch - don't request image 467 return; 468 } 469 470 // 1. Find the most wanted request and start it (if not already started). 471 Future<?> task = null; 472 for (int i = 0; i < sImageFetchSeq.length; i++) { 473 int offset = sImageFetchSeq[i].indexOffset; 474 int bit = sImageFetchSeq[i].imageBit; 475 task = startTaskIfNeeded(currentIndex + offset, bit); 476 if (task != null) break; 477 } 478 479 // 2. Cancel everything else. 480 for (ImageEntry entry : mImageCache.values()) { 481 if (entry.screenNailTask != null && entry.screenNailTask != task) { 482 entry.screenNailTask.cancel(); 483 entry.screenNailTask = null; 484 entry.requestedBits &= ~BIT_SCREEN_NAIL; 485 } 486 if (entry.fullImageTask != null && entry.fullImageTask != task) { 487 entry.fullImageTask.cancel(); 488 entry.fullImageTask = null; 489 entry.requestedBits &= ~BIT_FULL_IMAGE; 490 } 491 } 492 } 493 494 private static class ScreenNailJob implements Job<Bitmap> { 495 private MediaItem mItem; 496 497 public ScreenNailJob(MediaItem item) { 498 mItem = item; 499 } 500 501 @Override 502 public Bitmap run(JobContext jc) { 503 Bitmap bitmap = mItem.requestImage(MediaItem.TYPE_THUMBNAIL).run(jc); 504 if (jc.isCancelled()) return null; 505 if (bitmap != null) { 506 bitmap = BitmapUtils.rotateBitmap(bitmap, 507 mItem.getRotation() - mItem.getFullImageRotation(), true); 508 } 509 return bitmap; 510 } 511 } 512 513 // Returns the task if we started the task or the task is already started. 514 private Future<?> startTaskIfNeeded(int index, int which) { 515 if (index < mActiveStart || index >= mActiveEnd) return null; 516 517 ImageEntry entry = mImageCache.get(getVersion(index)); 518 if (entry == null) return null; 519 520 if (which == BIT_SCREEN_NAIL && entry.screenNailTask != null) { 521 return entry.screenNailTask; 522 } else if (which == BIT_FULL_IMAGE && entry.fullImageTask != null) { 523 return entry.fullImageTask; 524 } 525 526 MediaItem item = mData[index % DATA_CACHE_SIZE]; 527 Utils.assertTrue(item != null); 528 529 if (which == BIT_SCREEN_NAIL 530 && (entry.requestedBits & BIT_SCREEN_NAIL) == 0) { 531 entry.requestedBits |= BIT_SCREEN_NAIL; 532 entry.screenNailTask = mThreadPool.submit( 533 new ScreenNailJob(item), 534 new ScreenNailListener(item.getDataVersion())); 535 // request screen nail 536 return entry.screenNailTask; 537 } 538 if (which == BIT_FULL_IMAGE 539 && (entry.requestedBits & BIT_FULL_IMAGE) == 0 540 && (item.getSupportedOperations() 541 & MediaItem.SUPPORT_FULL_IMAGE) != 0) { 542 entry.requestedBits |= BIT_FULL_IMAGE; 543 entry.fullImageTask = mThreadPool.submit( 544 item.requestLargeImage(), 545 new FullImageListener(item.getDataVersion())); 546 // request full image 547 return entry.fullImageTask; 548 } 549 return null; 550 } 551 552 private void updateImageCache() { 553 HashSet<Long> toBeRemoved = new HashSet<Long>(mImageCache.keySet()); 554 for (int i = mActiveStart; i < mActiveEnd; ++i) { 555 MediaItem item = mData[i % DATA_CACHE_SIZE]; 556 long version = item == null 557 ? MediaObject.INVALID_DATA_VERSION 558 : item.getDataVersion(); 559 if (version == MediaObject.INVALID_DATA_VERSION) continue; 560 ImageEntry entry = mImageCache.get(version); 561 toBeRemoved.remove(version); 562 if (entry != null) { 563 if (Math.abs(i - mCurrentIndex) > 1) { 564 if (entry.fullImageTask != null) { 565 entry.fullImageTask.cancel(); 566 entry.fullImageTask = null; 567 } 568 entry.fullImage = null; 569 entry.requestedBits &= ~BIT_FULL_IMAGE; 570 } 571 } else { 572 entry = new ImageEntry(); 573 entry.rotation = item.getFullImageRotation(); 574 mImageCache.put(version, entry); 575 } 576 } 577 578 // Clear the data and requests for ImageEntries outside the new window. 579 for (Long version : toBeRemoved) { 580 ImageEntry entry = mImageCache.remove(version); 581 if (entry.fullImageTask != null) entry.fullImageTask.cancel(); 582 if (entry.screenNailTask != null) entry.screenNailTask.cancel(); 583 } 584 } 585 586 private class FullImageListener 587 implements Runnable, FutureListener<BitmapRegionDecoder> { 588 private final long mVersion; 589 private Future<BitmapRegionDecoder> mFuture; 590 591 public FullImageListener(long version) { 592 mVersion = version; 593 } 594 595 @Override 596 public void onFutureDone(Future<BitmapRegionDecoder> future) { 597 mFuture = future; 598 mMainHandler.sendMessage( 599 mMainHandler.obtainMessage(MSG_RUN_OBJECT, this)); 600 } 601 602 @Override 603 public void run() { 604 updateFullImage(mVersion, mFuture); 605 } 606 } 607 608 private class ScreenNailListener 609 implements Runnable, FutureListener<Bitmap> { 610 private final long mVersion; 611 private Future<Bitmap> mFuture; 612 613 public ScreenNailListener(long version) { 614 mVersion = version; 615 } 616 617 @Override 618 public void onFutureDone(Future<Bitmap> future) { 619 mFuture = future; 620 mMainHandler.sendMessage( 621 mMainHandler.obtainMessage(MSG_RUN_OBJECT, this)); 622 } 623 624 @Override 625 public void run() { 626 updateScreenNail(mVersion, mFuture); 627 } 628 } 629 630 private static class ImageEntry { 631 public int requestedBits = 0; 632 public int rotation; 633 public BitmapRegionDecoder fullImage; 634 public Bitmap screenNail; 635 public Future<Bitmap> screenNailTask; 636 public Future<BitmapRegionDecoder> fullImageTask; 637 public boolean failToLoad = false; 638 } 639 640 private class SourceListener implements ContentListener { 641 public void onContentDirty() { 642 if (mReloadTask != null) mReloadTask.notifyDirty(); 643 } 644 } 645 646 private <T> T executeAndWait(Callable<T> callable) { 647 FutureTask<T> task = new FutureTask<T>(callable); 648 mMainHandler.sendMessage( 649 mMainHandler.obtainMessage(MSG_RUN_OBJECT, task)); 650 try { 651 return task.get(); 652 } catch (InterruptedException e) { 653 return null; 654 } catch (ExecutionException e) { 655 throw new RuntimeException(e); 656 } 657 } 658 659 private static class UpdateInfo { 660 public long version; 661 public boolean reloadContent; 662 public Path target; 663 public int indexHint; 664 public int contentStart; 665 public int contentEnd; 666 667 public int size; 668 public ArrayList<MediaItem> items; 669 } 670 671 private class GetUpdateInfo implements Callable<UpdateInfo> { 672 673 private boolean needContentReload() { 674 for (int i = mContentStart, n = mContentEnd; i < n; ++i) { 675 if (mData[i % DATA_CACHE_SIZE] == null) return true; 676 } 677 MediaItem current = mData[mCurrentIndex % DATA_CACHE_SIZE]; 678 return current == null || current.getPath() != mItemPath; 679 } 680 681 @Override 682 public UpdateInfo call() throws Exception { 683 // TODO: Try to load some data in first update 684 UpdateInfo info = new UpdateInfo(); 685 info.version = mSourceVersion; 686 info.reloadContent = needContentReload(); 687 info.target = mItemPath; 688 info.indexHint = mCurrentIndex; 689 info.contentStart = mContentStart; 690 info.contentEnd = mContentEnd; 691 info.size = mSize; 692 return info; 693 } 694 } 695 696 private class UpdateContent implements Callable<Void> { 697 UpdateInfo mUpdateInfo; 698 699 public UpdateContent(UpdateInfo updateInfo) { 700 mUpdateInfo = updateInfo; 701 } 702 703 @Override 704 public Void call() throws Exception { 705 UpdateInfo info = mUpdateInfo; 706 mSourceVersion = info.version; 707 708 if (info.size != mSize) { 709 mSize = info.size; 710 if (mContentEnd > mSize) mContentEnd = mSize; 711 if (mActiveEnd > mSize) mActiveEnd = mSize; 712 } 713 714 if (info.indexHint == MediaSet.INDEX_NOT_FOUND) { 715 // The image has been deleted, clear mItemPath, the 716 // mCurrentIndex will be updated in the updateCurrentItem(). 717 mItemPath = null; 718 updateCurrentItem(); 719 } else { 720 mCurrentIndex = info.indexHint; 721 } 722 723 updateSlidingWindow(); 724 725 if (info.items != null) { 726 int start = Math.max(info.contentStart, mContentStart); 727 int end = Math.min(info.contentStart + info.items.size(), mContentEnd); 728 int dataIndex = start % DATA_CACHE_SIZE; 729 for (int i = start; i < end; ++i) { 730 mData[dataIndex] = info.items.get(i - info.contentStart); 731 if (++dataIndex == DATA_CACHE_SIZE) dataIndex = 0; 732 } 733 } 734 if (mItemPath == null) { 735 MediaItem current = mData[mCurrentIndex % DATA_CACHE_SIZE]; 736 mItemPath = current == null ? null : current.getPath(); 737 } 738 updateImageCache(); 739 updateTileProvider(); 740 updateImageRequests(); 741 fireModelInvalidated(); 742 return null; 743 } 744 745 private void updateCurrentItem() { 746 if (mSize == 0) return; 747 if (mCurrentIndex >= mSize) { 748 mCurrentIndex = mSize - 1; 749 mPhotoView.notifyOnNewImage(); 750 mPhotoView.startSlideInAnimation(PhotoView.TRANS_SLIDE_IN_LEFT); 751 } else { 752 mPhotoView.notifyOnNewImage(); 753 mPhotoView.startSlideInAnimation(PhotoView.TRANS_SLIDE_IN_RIGHT); 754 } 755 } 756 } 757 758 private class ReloadTask extends Thread { 759 private volatile boolean mActive = true; 760 private volatile boolean mDirty = true; 761 762 private boolean mIsLoading = false; 763 764 private void updateLoading(boolean loading) { 765 if (mIsLoading == loading) return; 766 mIsLoading = loading; 767 mMainHandler.sendEmptyMessage(loading ? MSG_LOAD_START : MSG_LOAD_FINISH); 768 } 769 770 @Override 771 public void run() { 772 while (mActive) { 773 synchronized (this) { 774 if (!mDirty && mActive) { 775 updateLoading(false); 776 Utils.waitWithoutInterrupt(this); 777 continue; 778 } 779 } 780 mDirty = false; 781 UpdateInfo info = executeAndWait(new GetUpdateInfo()); 782 synchronized (DataManager.LOCK) { 783 updateLoading(true); 784 long version = mSource.reload(); 785 if (info.version != version) { 786 info.reloadContent = true; 787 info.size = mSource.getMediaItemCount(); 788 } 789 if (!info.reloadContent) continue; 790 info.items = mSource.getMediaItem(info.contentStart, info.contentEnd); 791 MediaItem item = findCurrentMediaItem(info); 792 if (item == null || item.getPath() != info.target) { 793 info.indexHint = findIndexOfTarget(info); 794 } 795 } 796 executeAndWait(new UpdateContent(info)); 797 } 798 } 799 800 public synchronized void notifyDirty() { 801 mDirty = true; 802 notifyAll(); 803 } 804 805 public synchronized void terminate() { 806 mActive = false; 807 notifyAll(); 808 } 809 810 private MediaItem findCurrentMediaItem(UpdateInfo info) { 811 ArrayList<MediaItem> items = info.items; 812 int index = info.indexHint - info.contentStart; 813 return index < 0 || index >= items.size() ? null : items.get(index); 814 } 815 816 private int findIndexOfTarget(UpdateInfo info) { 817 if (info.target == null) return info.indexHint; 818 ArrayList<MediaItem> items = info.items; 819 820 // First, try to find the item in the data just loaded 821 if (items != null) { 822 for (int i = 0, n = items.size(); i < n; ++i) { 823 if (items.get(i).getPath() == info.target) return i + info.contentStart; 824 } 825 } 826 827 // Not found, find it in mSource. 828 return mSource.getIndexOfItem(info.target, info.indexHint); 829 } 830 } 831} 832