PhotoDataAdapter.java revision b7ec5534c7b539be2397c27cfa5e8b992974c12d
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 59 private static final int MIN_LOAD_COUNT = 8; 60 private static final int DATA_CACHE_SIZE = 32; 61 private static final int SCREEN_NAIL_MAX = PhotoView.SCREEN_NAIL_MAX; 62 private static final int IMAGE_CACHE_SIZE = 2 * SCREEN_NAIL_MAX + 1; 63 64 private static final int BIT_SCREEN_NAIL = 1; 65 private static final int BIT_FULL_IMAGE = 2; 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 notify the 132 // view. This is used after a database reload or mCurrentIndex changes. 133 private final long mChanges[] = new long[IMAGE_CACHE_SIZE]; 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 MediaObject.INVALID_DATA_VERSION; 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 fireDataChange() { 204 for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; ++i) { 205 mChanges[i + SCREEN_NAIL_MAX] = getVersion(mCurrentIndex + i); 206 } 207 mPhotoView.notifyDataChange(mChanges); 208 } 209 210 public void setDataListener(DataListener listener) { 211 mDataListener = listener; 212 } 213 214 private void updateScreenNail(long version, Future<ScreenNail> future) { 215 ImageEntry entry = mImageCache.get(version); 216 ScreenNail screenNail = future.get(); 217 218 if (entry == null || entry.screenNailTask != future) { 219 if (screenNail != null) screenNail.pauseDraw(); 220 return; 221 } 222 223 entry.screenNailTask = null; 224 entry.screenNail = screenNail; 225 226 if (screenNail == null) { 227 entry.failToLoad = true; 228 } 229 230 if (mDataListener != null) { 231 mDataListener.onPhotoAvailable(version, false); 232 } 233 234 for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; ++i) { 235 if (version == getVersion(mCurrentIndex + i)) { 236 if (i == 0) updateTileProvider(entry); 237 mPhotoView.notifyImageChange(i); 238 break; 239 } 240 } 241 updateImageRequests(); 242 } 243 244 private void updateFullImage(long version, Future<BitmapRegionDecoder> future) { 245 ImageEntry entry = mImageCache.get(version); 246 if (entry == null || entry.fullImageTask != future) { 247 BitmapRegionDecoder fullImage = future.get(); 248 if (fullImage != null) fullImage.recycle(); 249 return; 250 } 251 252 entry.fullImageTask = null; 253 entry.fullImage = future.get(); 254 if (entry.fullImage != null) { 255 if (mDataListener != null) { 256 mDataListener.onPhotoAvailable(version, true); 257 } 258 if (version == getVersion(mCurrentIndex)) { 259 updateTileProvider(entry); 260 mPhotoView.notifyImageChange(0); 261 } 262 } 263 updateImageRequests(); 264 } 265 266 public void resume() { 267 mIsActive = true; 268 mSource.addContentListener(mSourceListener); 269 updateImageCache(); 270 updateImageRequests(); 271 272 mReloadTask = new ReloadTask(); 273 mReloadTask.start(); 274 275 fireDataChange(); 276 } 277 278 public void pause() { 279 mIsActive = false; 280 281 mReloadTask.terminate(); 282 mReloadTask = null; 283 284 mSource.removeContentListener(mSourceListener); 285 286 for (ImageEntry entry : mImageCache.values()) { 287 if (entry.fullImageTask != null) entry.fullImageTask.cancel(); 288 if (entry.screenNailTask != null) entry.screenNailTask.cancel(); 289 } 290 mImageCache.clear(); 291 mTileProvider.clear(); 292 } 293 294 private ScreenNail getImage(int index) { 295 if (index < 0 || index >= mSize || !mIsActive) return null; 296 Utils.assertTrue(index >= mActiveStart && index < mActiveEnd); 297 298 ImageEntry entry = mImageCache.get(getVersion(index)); 299 return entry == null ? null : entry.screenNail; 300 } 301 302 public ScreenNail getScreenNail(int offset) { 303 return getImage(mCurrentIndex + offset); 304 } 305 306 private void updateCurrentIndex(int index) { 307 mCurrentIndex = index; 308 updateSlidingWindow(); 309 310 MediaItem item = mData[index % DATA_CACHE_SIZE]; 311 mItemPath = item == null ? null : item.getPath(); 312 313 updateImageCache(); 314 updateImageRequests(); 315 updateTileProvider(); 316 317 if (mDataListener != null) { 318 mDataListener.onPhotoChanged(index, mItemPath); 319 } 320 321 fireDataChange(); 322 } 323 324 public void next() { 325 updateCurrentIndex(mCurrentIndex + 1); 326 } 327 328 public void previous() { 329 updateCurrentIndex(mCurrentIndex - 1); 330 } 331 332 public ScreenNail getScreenNail() { 333 return mTileProvider.getScreenNail(); 334 } 335 336 public int getImageHeight() { 337 return mTileProvider.getImageHeight(); 338 } 339 340 public int getImageWidth() { 341 return mTileProvider.getImageWidth(); 342 } 343 344 public int getImageRotation() { 345 ImageEntry entry = mImageCache.get(getVersion(mCurrentIndex)); 346 return entry == null ? 0 : entry.rotation; 347 } 348 349 public int getLevelCount() { 350 return mTileProvider.getLevelCount(); 351 } 352 353 public Bitmap getTile(int level, int x, int y, int tileSize, 354 int borderSize) { 355 return mTileProvider.getTile(level, x, y, tileSize, borderSize); 356 } 357 358 public boolean isFailedToLoad() { 359 return mTileProvider.isFailedToLoad(); 360 } 361 362 public boolean isEmpty() { 363 return mSize == 0; 364 } 365 366 public int getCurrentIndex() { 367 return mCurrentIndex; 368 } 369 370 public MediaItem getCurrentMediaItem() { 371 return mData[mCurrentIndex % DATA_CACHE_SIZE]; 372 } 373 374 public void setCurrentPhoto(Path path, int indexHint) { 375 if (mItemPath == path) return; 376 mItemPath = path; 377 mCurrentIndex = indexHint; 378 updateSlidingWindow(); 379 updateImageCache(); 380 fireDataChange(); 381 382 // We need to reload content if the path doesn't match. 383 MediaItem item = getCurrentMediaItem(); 384 if (item != null && item.getPath() != path) { 385 if (mReloadTask != null) mReloadTask.notifyDirty(); 386 } 387 } 388 389 private void updateTileProvider() { 390 ImageEntry entry = mImageCache.get(getVersion(mCurrentIndex)); 391 if (entry == null) { // in loading 392 mTileProvider.clear(); 393 } else { 394 updateTileProvider(entry); 395 } 396 } 397 398 private void updateTileProvider(ImageEntry entry) { 399 ScreenNail screenNail = entry.screenNail; 400 BitmapRegionDecoder fullImage = entry.fullImage; 401 if (screenNail != null) { 402 if (fullImage != null) { 403 mTileProvider.setScreenNail(screenNail, 404 fullImage.getWidth(), fullImage.getHeight()); 405 mTileProvider.setRegionDecoder(fullImage); 406 } else { 407 int width = screenNail.getWidth(); 408 int height = screenNail.getHeight(); 409 mTileProvider.setScreenNail(screenNail, width, height); 410 } 411 } else { 412 mTileProvider.clear(); 413 if (entry.failToLoad) mTileProvider.setFailedToLoad(); 414 } 415 } 416 417 private void updateSlidingWindow() { 418 // 1. Update the image window 419 int start = Utils.clamp(mCurrentIndex - IMAGE_CACHE_SIZE / 2, 420 0, Math.max(0, mSize - IMAGE_CACHE_SIZE)); 421 int end = Math.min(mSize, start + IMAGE_CACHE_SIZE); 422 423 if (mActiveStart == start && mActiveEnd == end) return; 424 425 mActiveStart = start; 426 mActiveEnd = end; 427 428 // 2. Update the data window 429 start = Utils.clamp(mCurrentIndex - DATA_CACHE_SIZE / 2, 430 0, Math.max(0, mSize - DATA_CACHE_SIZE)); 431 end = Math.min(mSize, start + DATA_CACHE_SIZE); 432 if (mContentStart > mActiveStart || mContentEnd < mActiveEnd 433 || Math.abs(start - mContentStart) > MIN_LOAD_COUNT) { 434 for (int i = mContentStart; i < mContentEnd; ++i) { 435 if (i < start || i >= end) { 436 mData[i % DATA_CACHE_SIZE] = null; 437 } 438 } 439 mContentStart = start; 440 mContentEnd = end; 441 if (mReloadTask != null) mReloadTask.notifyDirty(); 442 } 443 } 444 445 private void updateImageRequests() { 446 if (!mIsActive) return; 447 448 int currentIndex = mCurrentIndex; 449 MediaItem item = mData[currentIndex % DATA_CACHE_SIZE]; 450 if (item == null || item.getPath() != mItemPath) { 451 // current item mismatch - don't request image 452 return; 453 } 454 455 // 1. Find the most wanted request and start it (if not already started). 456 Future<?> task = null; 457 for (int i = 0; i < sImageFetchSeq.length; i++) { 458 int offset = sImageFetchSeq[i].indexOffset; 459 int bit = sImageFetchSeq[i].imageBit; 460 task = startTaskIfNeeded(currentIndex + offset, bit); 461 if (task != null) break; 462 } 463 464 // 2. Cancel everything else. 465 for (ImageEntry entry : mImageCache.values()) { 466 if (entry.screenNailTask != null && entry.screenNailTask != task) { 467 entry.screenNailTask.cancel(); 468 entry.screenNailTask = null; 469 entry.requestedBits &= ~BIT_SCREEN_NAIL; 470 } 471 if (entry.fullImageTask != null && entry.fullImageTask != task) { 472 entry.fullImageTask.cancel(); 473 entry.fullImageTask = null; 474 entry.requestedBits &= ~BIT_FULL_IMAGE; 475 } 476 } 477 } 478 479 private static class ScreenNailJob implements Job<ScreenNail> { 480 private MediaItem mItem; 481 482 public ScreenNailJob(MediaItem item) { 483 mItem = item; 484 } 485 486 @Override 487 public ScreenNail run(JobContext jc) { 488 // We try to get a ScreenNail first, if it fails, we fallback to get 489 // a Bitmap and then wrap it in a BitmapScreenNail instead. 490 ScreenNail s = mItem.getScreenNail(); 491 if (s != null) return s; 492 493 Bitmap bitmap = mItem.requestImage(MediaItem.TYPE_THUMBNAIL).run(jc); 494 if (jc.isCancelled()) return null; 495 if (bitmap != null) { 496 bitmap = BitmapUtils.rotateBitmap(bitmap, 497 mItem.getRotation() - mItem.getFullImageRotation(), true); 498 } 499 return new BitmapScreenNail(bitmap, mItem.getFullImageRotation()); 500 } 501 } 502 503 // Returns the task if we started the task or the task is already started. 504 private Future<?> startTaskIfNeeded(int index, int which) { 505 if (index < mActiveStart || index >= mActiveEnd) return null; 506 507 ImageEntry entry = mImageCache.get(getVersion(index)); 508 if (entry == null) return null; 509 510 if (which == BIT_SCREEN_NAIL && entry.screenNailTask != null) { 511 return entry.screenNailTask; 512 } else if (which == BIT_FULL_IMAGE && entry.fullImageTask != null) { 513 return entry.fullImageTask; 514 } 515 516 MediaItem item = mData[index % DATA_CACHE_SIZE]; 517 Utils.assertTrue(item != null); 518 519 if (which == BIT_SCREEN_NAIL 520 && (entry.requestedBits & BIT_SCREEN_NAIL) == 0) { 521 entry.requestedBits |= BIT_SCREEN_NAIL; 522 entry.screenNailTask = mThreadPool.submit( 523 new ScreenNailJob(item), 524 new ScreenNailListener(item.getDataVersion())); 525 // request screen nail 526 return entry.screenNailTask; 527 } 528 if (which == BIT_FULL_IMAGE 529 && (entry.requestedBits & BIT_FULL_IMAGE) == 0 530 && (item.getSupportedOperations() 531 & MediaItem.SUPPORT_FULL_IMAGE) != 0) { 532 entry.requestedBits |= BIT_FULL_IMAGE; 533 entry.fullImageTask = mThreadPool.submit( 534 item.requestLargeImage(), 535 new FullImageListener(item.getDataVersion())); 536 // request full image 537 return entry.fullImageTask; 538 } 539 return null; 540 } 541 542 private void updateImageCache() { 543 HashSet<Long> toBeRemoved = new HashSet<Long>(mImageCache.keySet()); 544 for (int i = mActiveStart; i < mActiveEnd; ++i) { 545 MediaItem item = mData[i % DATA_CACHE_SIZE]; 546 long version = item == null 547 ? MediaObject.INVALID_DATA_VERSION 548 : item.getDataVersion(); 549 if (version == MediaObject.INVALID_DATA_VERSION) continue; 550 ImageEntry entry = mImageCache.get(version); 551 toBeRemoved.remove(version); 552 if (entry != null) { 553 if (Math.abs(i - mCurrentIndex) > 1) { 554 if (entry.fullImageTask != null) { 555 entry.fullImageTask.cancel(); 556 entry.fullImageTask = null; 557 } 558 entry.fullImage = null; 559 entry.requestedBits &= ~BIT_FULL_IMAGE; 560 } 561 } else { 562 entry = new ImageEntry(); 563 entry.rotation = item.getFullImageRotation(); 564 mImageCache.put(version, entry); 565 } 566 } 567 568 // Clear the data and requests for ImageEntries outside the new window. 569 for (Long version : toBeRemoved) { 570 ImageEntry entry = mImageCache.remove(version); 571 if (entry.fullImageTask != null) entry.fullImageTask.cancel(); 572 if (entry.screenNailTask != null) entry.screenNailTask.cancel(); 573 } 574 } 575 576 private class FullImageListener 577 implements Runnable, FutureListener<BitmapRegionDecoder> { 578 private final long mVersion; 579 private Future<BitmapRegionDecoder> mFuture; 580 581 public FullImageListener(long version) { 582 mVersion = version; 583 } 584 585 @Override 586 public void onFutureDone(Future<BitmapRegionDecoder> future) { 587 mFuture = future; 588 mMainHandler.sendMessage( 589 mMainHandler.obtainMessage(MSG_RUN_OBJECT, this)); 590 } 591 592 @Override 593 public void run() { 594 updateFullImage(mVersion, mFuture); 595 } 596 } 597 598 private class ScreenNailListener 599 implements Runnable, FutureListener<ScreenNail> { 600 private final long mVersion; 601 private Future<ScreenNail> mFuture; 602 603 public ScreenNailListener(long version) { 604 mVersion = version; 605 } 606 607 @Override 608 public void onFutureDone(Future<ScreenNail> future) { 609 mFuture = future; 610 mMainHandler.sendMessage( 611 mMainHandler.obtainMessage(MSG_RUN_OBJECT, this)); 612 } 613 614 @Override 615 public void run() { 616 updateScreenNail(mVersion, mFuture); 617 } 618 } 619 620 private static class ImageEntry { 621 public int requestedBits = 0; 622 public int rotation; 623 public BitmapRegionDecoder fullImage; 624 public ScreenNail screenNail; 625 public Future<ScreenNail> screenNailTask; 626 public Future<BitmapRegionDecoder> fullImageTask; 627 public boolean failToLoad = false; 628 } 629 630 private class SourceListener implements ContentListener { 631 public void onContentDirty() { 632 if (mReloadTask != null) mReloadTask.notifyDirty(); 633 } 634 } 635 636 private <T> T executeAndWait(Callable<T> callable) { 637 FutureTask<T> task = new FutureTask<T>(callable); 638 mMainHandler.sendMessage( 639 mMainHandler.obtainMessage(MSG_RUN_OBJECT, task)); 640 try { 641 return task.get(); 642 } catch (InterruptedException e) { 643 return null; 644 } catch (ExecutionException e) { 645 throw new RuntimeException(e); 646 } 647 } 648 649 private static class UpdateInfo { 650 public long version; 651 public boolean reloadContent; 652 public Path target; 653 public int indexHint; 654 public int contentStart; 655 public int contentEnd; 656 657 public int size; 658 public ArrayList<MediaItem> items; 659 } 660 661 private class GetUpdateInfo implements Callable<UpdateInfo> { 662 663 private boolean needContentReload() { 664 for (int i = mContentStart, n = mContentEnd; i < n; ++i) { 665 if (mData[i % DATA_CACHE_SIZE] == null) return true; 666 } 667 MediaItem current = mData[mCurrentIndex % DATA_CACHE_SIZE]; 668 return current == null || current.getPath() != mItemPath; 669 } 670 671 @Override 672 public UpdateInfo call() throws Exception { 673 // TODO: Try to load some data in first update 674 UpdateInfo info = new UpdateInfo(); 675 info.version = mSourceVersion; 676 info.reloadContent = needContentReload(); 677 info.target = mItemPath; 678 info.indexHint = mCurrentIndex; 679 info.contentStart = mContentStart; 680 info.contentEnd = mContentEnd; 681 info.size = mSize; 682 return info; 683 } 684 } 685 686 private class UpdateContent implements Callable<Void> { 687 UpdateInfo mUpdateInfo; 688 689 public UpdateContent(UpdateInfo updateInfo) { 690 mUpdateInfo = updateInfo; 691 } 692 693 @Override 694 public Void call() throws Exception { 695 UpdateInfo info = mUpdateInfo; 696 mSourceVersion = info.version; 697 698 if (info.size != mSize) { 699 mSize = info.size; 700 if (mContentEnd > mSize) mContentEnd = mSize; 701 if (mActiveEnd > mSize) mActiveEnd = mSize; 702 } 703 704 if (info.indexHint == MediaSet.INDEX_NOT_FOUND) { 705 // The image has been deleted, clear mItemPath, the 706 // mCurrentIndex will be updated in the updateCurrentItem(). 707 mItemPath = null; 708 updateCurrentItem(); 709 } else { 710 mCurrentIndex = info.indexHint; 711 } 712 713 updateSlidingWindow(); 714 715 if (info.items != null) { 716 int start = Math.max(info.contentStart, mContentStart); 717 int end = Math.min(info.contentStart + info.items.size(), mContentEnd); 718 int dataIndex = start % DATA_CACHE_SIZE; 719 for (int i = start; i < end; ++i) { 720 mData[dataIndex] = info.items.get(i - info.contentStart); 721 if (++dataIndex == DATA_CACHE_SIZE) dataIndex = 0; 722 } 723 } 724 if (mItemPath == null) { 725 MediaItem current = mData[mCurrentIndex % DATA_CACHE_SIZE]; 726 mItemPath = current == null ? null : current.getPath(); 727 } 728 updateImageCache(); 729 updateTileProvider(); 730 updateImageRequests(); 731 fireDataChange(); 732 return null; 733 } 734 735 private void updateCurrentItem() { 736 if (mSize == 0) return; 737 if (mCurrentIndex >= mSize) { 738 mCurrentIndex = mSize - 1; 739 } 740 fireDataChange(); 741 } 742 } 743 744 private class ReloadTask extends Thread { 745 private volatile boolean mActive = true; 746 private volatile boolean mDirty = true; 747 748 private boolean mIsLoading = false; 749 750 private void updateLoading(boolean loading) { 751 if (mIsLoading == loading) return; 752 mIsLoading = loading; 753 mMainHandler.sendEmptyMessage(loading ? MSG_LOAD_START : MSG_LOAD_FINISH); 754 } 755 756 @Override 757 public void run() { 758 while (mActive) { 759 synchronized (this) { 760 if (!mDirty && mActive) { 761 updateLoading(false); 762 Utils.waitWithoutInterrupt(this); 763 continue; 764 } 765 } 766 mDirty = false; 767 UpdateInfo info = executeAndWait(new GetUpdateInfo()); 768 synchronized (DataManager.LOCK) { 769 updateLoading(true); 770 long version = mSource.reload(); 771 if (info.version != version) { 772 info.reloadContent = true; 773 info.size = mSource.getMediaItemCount(); 774 } 775 if (!info.reloadContent) continue; 776 info.items = mSource.getMediaItem(info.contentStart, info.contentEnd); 777 MediaItem item = findCurrentMediaItem(info); 778 if (item == null || item.getPath() != info.target) { 779 info.indexHint = findIndexOfTarget(info); 780 } 781 } 782 executeAndWait(new UpdateContent(info)); 783 } 784 } 785 786 public synchronized void notifyDirty() { 787 mDirty = true; 788 notifyAll(); 789 } 790 791 public synchronized void terminate() { 792 mActive = false; 793 notifyAll(); 794 } 795 796 private MediaItem findCurrentMediaItem(UpdateInfo info) { 797 ArrayList<MediaItem> items = info.items; 798 int index = info.indexHint - info.contentStart; 799 return index < 0 || index >= items.size() ? null : items.get(index); 800 } 801 802 private int findIndexOfTarget(UpdateInfo info) { 803 if (info.target == null) return info.indexHint; 804 ArrayList<MediaItem> items = info.items; 805 806 // First, try to find the item in the data just loaded 807 if (items != null) { 808 for (int i = 0, n = items.size(); i < n; ++i) { 809 if (items.get(i).getPath() == info.target) return i + info.contentStart; 810 } 811 } 812 813 // Not found, find it in mSource. 814 return mSource.getIndexOfItem(info.target, info.indexHint); 815 } 816 } 817} 818