PhotoDataAdapter.java revision 616a70fdb4473d2fbd7b70772a3a82b908aeae1e
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 images. If any 132 // of the version number changes, we notify the view. This is used after a 133 // database reload or mCurrentIndex changes. 134 private final long mChanges[] = new long[IMAGE_CACHE_SIZE]; 135 // mPaths keeps the corresponding Path (of MediaItem) for the images. This 136 // is used to determine the item movement. 137 private final Path mPaths[] = new Path[IMAGE_CACHE_SIZE]; 138 139 private final Handler mMainHandler; 140 private final ThreadPool mThreadPool; 141 142 private final PhotoView mPhotoView; 143 private final MediaSet mSource; 144 private ReloadTask mReloadTask; 145 146 private long mSourceVersion = MediaObject.INVALID_DATA_VERSION; 147 private int mSize = 0; 148 private Path mItemPath; 149 private int mCameraIndex; 150 private boolean mIsActive; 151 private boolean mNeedFullImage; 152 153 public interface DataListener extends LoadingListener { 154 public void onPhotoChanged(int index, Path item); 155 } 156 157 private DataListener mDataListener; 158 159 private final SourceListener mSourceListener = new SourceListener(); 160 161 // The path of the current viewing item will be stored in mItemPath. 162 // If mItemPath is not null, mCurrentIndex is only a hint for where we 163 // can find the item. If mItemPath is null, then we use the mCurrentIndex to 164 // find the image being viewed. cameraIndex is the index of the camera 165 // preview. If cameraIndex < 0, there is no camera preview. 166 public PhotoDataAdapter(GalleryActivity activity, PhotoView view, 167 MediaSet mediaSet, Path itemPath, int indexHint, int cameraIndex) { 168 mSource = Utils.checkNotNull(mediaSet); 169 mPhotoView = Utils.checkNotNull(view); 170 mItemPath = Utils.checkNotNull(itemPath); 171 mCurrentIndex = indexHint; 172 mCameraIndex = cameraIndex; 173 mThreadPool = activity.getThreadPool(); 174 mNeedFullImage = true; 175 176 Arrays.fill(mChanges, MediaObject.INVALID_DATA_VERSION); 177 178 mMainHandler = new SynchronizedHandler(activity.getGLRoot()) { 179 @SuppressWarnings("unchecked") 180 @Override 181 public void handleMessage(Message message) { 182 switch (message.what) { 183 case MSG_RUN_OBJECT: 184 ((Runnable) message.obj).run(); 185 return; 186 case MSG_LOAD_START: { 187 if (mDataListener != null) mDataListener.onLoadingStarted(); 188 return; 189 } 190 case MSG_LOAD_FINISH: { 191 if (mDataListener != null) mDataListener.onLoadingFinished(); 192 return; 193 } 194 case MSG_UPDATE_IMAGE_REQUESTS: { 195 updateImageRequests(); 196 return; 197 } 198 default: throw new AssertionError(); 199 } 200 } 201 }; 202 203 updateSlidingWindow(); 204 } 205 206 private MediaItem getItemInternal(int index) { 207 if (index < 0 || index >= mSize) return null; 208 if (index >= mContentStart && index < mContentEnd) { 209 return mData[index % DATA_CACHE_SIZE]; 210 } 211 return null; 212 } 213 214 private long getVersion(int index) { 215 MediaItem item = getItemInternal(index); 216 if (item == null) return MediaObject.INVALID_DATA_VERSION; 217 return item.getDataVersion(); 218 } 219 220 private Path getPath(int index) { 221 MediaItem item = getItemInternal(index); 222 if (item == null) return null; 223 return item.getPath(); 224 } 225 226 private void fireDataChange() { 227 // First check if data actually changed. 228 boolean changed = false; 229 for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; ++i) { 230 long newVersion = getVersion(mCurrentIndex + i); 231 if (mChanges[i + SCREEN_NAIL_MAX] != newVersion) { 232 mChanges[i + SCREEN_NAIL_MAX] = newVersion; 233 changed = true; 234 } 235 } 236 237 if (!changed) return; 238 239 // Now calculate the fromIndex array. fromIndex represents the item 240 // movement. It records the index where the picture come from. The 241 // special value Integer.MAX_VALUE means it's a new picture. 242 final int N = IMAGE_CACHE_SIZE; 243 int fromIndex[] = new int[N]; 244 245 // Remember the old path array. 246 Path oldPaths[] = new Path[N]; 247 System.arraycopy(mPaths, 0, oldPaths, 0, N); 248 249 // Update the mPaths array. 250 for (int i = 0; i < N; ++i) { 251 mPaths[i] = getPath(mCurrentIndex + i - SCREEN_NAIL_MAX); 252 } 253 254 // Calculate the fromIndex array. 255 for (int i = 0; i < N; i++) { 256 Path p = mPaths[i]; 257 if (p == null) { 258 fromIndex[i] = Integer.MAX_VALUE; 259 continue; 260 } 261 262 // Try to find the same path in the old array 263 int j; 264 for (j = 0; j < N; j++) { 265 if (oldPaths[j] == p) { 266 break; 267 } 268 } 269 fromIndex[i] = (j < N) ? j - SCREEN_NAIL_MAX : Integer.MAX_VALUE; 270 } 271 272 mPhotoView.notifyDataChange(fromIndex, -mCurrentIndex, 273 mSize - 1 - mCurrentIndex); 274 } 275 276 public void setDataListener(DataListener listener) { 277 mDataListener = listener; 278 } 279 280 private void updateScreenNail(long version, Future<ScreenNail> future) { 281 ImageEntry entry = mImageCache.get(version); 282 ScreenNail screenNail = future.get(); 283 284 if (entry == null || entry.screenNailTask != future) { 285 if (screenNail != null) screenNail.recycle(); 286 return; 287 } 288 289 entry.screenNailTask = null; 290 Utils.assertTrue(entry.screenNail == null); 291 entry.screenNail = screenNail; 292 293 if (screenNail == null) { 294 entry.failToLoad = true; 295 } 296 297 for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; ++i) { 298 if (version == getVersion(mCurrentIndex + i)) { 299 if (i == 0) updateTileProvider(entry); 300 mPhotoView.notifyImageChange(i); 301 break; 302 } 303 } 304 updateImageRequests(); 305 } 306 307 private void updateFullImage(long version, Future<BitmapRegionDecoder> future) { 308 ImageEntry entry = mImageCache.get(version); 309 if (entry == null || entry.fullImageTask != future) { 310 BitmapRegionDecoder fullImage = future.get(); 311 if (fullImage != null) fullImage.recycle(); 312 return; 313 } 314 315 entry.fullImageTask = null; 316 entry.fullImage = future.get(); 317 if (entry.fullImage != null) { 318 if (version == getVersion(mCurrentIndex)) { 319 updateTileProvider(entry); 320 mPhotoView.notifyImageChange(0); 321 } 322 } 323 updateImageRequests(); 324 } 325 326 public void resume() { 327 mIsActive = true; 328 mSource.addContentListener(mSourceListener); 329 updateImageCache(); 330 updateImageRequests(); 331 332 mReloadTask = new ReloadTask(); 333 mReloadTask.start(); 334 335 fireDataChange(); 336 } 337 338 public void pause() { 339 mIsActive = false; 340 341 mReloadTask.terminate(); 342 mReloadTask = null; 343 344 mSource.removeContentListener(mSourceListener); 345 346 for (ImageEntry entry : mImageCache.values()) { 347 if (entry.fullImageTask != null) entry.fullImageTask.cancel(); 348 if (entry.screenNailTask != null) entry.screenNailTask.cancel(); 349 if (entry.screenNail != null) entry.screenNail.recycle(); 350 } 351 mImageCache.clear(); 352 mTileProvider.clear(); 353 } 354 355 private ScreenNail getImage(int index) { 356 if (index < 0 || index >= mSize || !mIsActive) return null; 357 Utils.assertTrue(index >= mActiveStart && index < mActiveEnd); 358 359 ImageEntry entry = mImageCache.get(getVersion(index)); 360 return entry == null ? null : entry.screenNail; 361 } 362 363 private MediaItem getItem(int index) { 364 if (index < 0 || index >= mSize || !mIsActive) return null; 365 Utils.assertTrue(index >= mActiveStart && index < mActiveEnd); 366 367 if (index >= mContentStart && index < mContentEnd) { 368 return mData[index % DATA_CACHE_SIZE]; 369 } 370 return null; 371 } 372 373 private void updateCurrentIndex(int index) { 374 if (mCurrentIndex == index) return; 375 mCurrentIndex = index; 376 updateSlidingWindow(); 377 378 MediaItem item = mData[index % DATA_CACHE_SIZE]; 379 mItemPath = item == null ? null : item.getPath(); 380 381 updateImageCache(); 382 updateImageRequests(); 383 updateTileProvider(); 384 385 if (mDataListener != null) { 386 mDataListener.onPhotoChanged(index, mItemPath); 387 } 388 389 fireDataChange(); 390 } 391 392 @Override 393 public void moveTo(int index) { 394 updateCurrentIndex(index); 395 } 396 397 @Override 398 public ScreenNail getScreenNail(int offset) { 399 return getImage(mCurrentIndex + offset); 400 } 401 402 @Override 403 public void getImageSize(int offset, PhotoView.Size size) { 404 MediaItem item = getItem(mCurrentIndex + offset); 405 if (item == null) { 406 size.width = 0; 407 size.height = 0; 408 } else { 409 size.width = item.getWidth(); 410 size.height = item.getHeight(); 411 } 412 } 413 414 @Override 415 public int getImageRotation(int offset) { 416 MediaItem item = getItem(mCurrentIndex + offset); 417 return (item == null) ? 0 : item.getFullImageRotation(); 418 } 419 420 @Override 421 public void setNeedFullImage(boolean enabled) { 422 mNeedFullImage = enabled; 423 mMainHandler.sendEmptyMessage(MSG_UPDATE_IMAGE_REQUESTS); 424 } 425 426 @Override 427 public boolean isCamera(int offset) { 428 return mCurrentIndex + offset == mCameraIndex; 429 } 430 431 @Override 432 public boolean isVideo(int offset) { 433 MediaItem item = getItem(mCurrentIndex + offset); 434 return (item == null) 435 ? false 436 : item.getMediaType() == MediaItem.MEDIA_TYPE_VIDEO; 437 } 438 439 public ScreenNail getScreenNail() { 440 return mTileProvider.getScreenNail(); 441 } 442 443 public int getImageHeight() { 444 return mTileProvider.getImageHeight(); 445 } 446 447 public int getImageWidth() { 448 return mTileProvider.getImageWidth(); 449 } 450 451 public int getLevelCount() { 452 return mTileProvider.getLevelCount(); 453 } 454 455 public Bitmap getTile(int level, int x, int y, int tileSize, 456 int borderSize) { 457 return mTileProvider.getTile(level, x, y, tileSize, borderSize); 458 } 459 460 public boolean isFailedToLoad() { 461 return mTileProvider.isFailedToLoad(); 462 } 463 464 public boolean isEmpty() { 465 return mSize == 0; 466 } 467 468 public int getCurrentIndex() { 469 return mCurrentIndex; 470 } 471 472 public MediaItem getMediaItem(int offset) { 473 int index = mCurrentIndex + offset; 474 if (index >= mContentStart && index < mContentEnd) { 475 return mData[index % DATA_CACHE_SIZE]; 476 } 477 return null; 478 } 479 480 public void setCurrentPhoto(Path path, int indexHint) { 481 if (mItemPath == path) return; 482 mItemPath = path; 483 mCurrentIndex = indexHint; 484 updateSlidingWindow(); 485 updateImageCache(); 486 fireDataChange(); 487 488 // We need to reload content if the path doesn't match. 489 MediaItem item = getMediaItem(0); 490 if (item != null && item.getPath() != path) { 491 if (mReloadTask != null) mReloadTask.notifyDirty(); 492 } 493 } 494 495 private void updateTileProvider() { 496 ImageEntry entry = mImageCache.get(getVersion(mCurrentIndex)); 497 if (entry == null) { // in loading 498 mTileProvider.clear(); 499 } else { 500 updateTileProvider(entry); 501 } 502 } 503 504 private void updateTileProvider(ImageEntry entry) { 505 ScreenNail screenNail = entry.screenNail; 506 BitmapRegionDecoder fullImage = entry.fullImage; 507 if (screenNail != null) { 508 if (fullImage != null) { 509 mTileProvider.setScreenNail(screenNail, 510 fullImage.getWidth(), fullImage.getHeight()); 511 mTileProvider.setRegionDecoder(fullImage); 512 } else { 513 int width = screenNail.getWidth(); 514 int height = screenNail.getHeight(); 515 mTileProvider.setScreenNail(screenNail, width, height); 516 } 517 } else { 518 mTileProvider.clear(); 519 if (entry.failToLoad) mTileProvider.setFailedToLoad(); 520 } 521 } 522 523 private void updateSlidingWindow() { 524 // 1. Update the image window 525 int start = Utils.clamp(mCurrentIndex - IMAGE_CACHE_SIZE / 2, 526 0, Math.max(0, mSize - IMAGE_CACHE_SIZE)); 527 int end = Math.min(mSize, start + IMAGE_CACHE_SIZE); 528 529 if (mActiveStart == start && mActiveEnd == end) return; 530 531 mActiveStart = start; 532 mActiveEnd = end; 533 534 // 2. Update the data window 535 start = Utils.clamp(mCurrentIndex - DATA_CACHE_SIZE / 2, 536 0, Math.max(0, mSize - DATA_CACHE_SIZE)); 537 end = Math.min(mSize, start + DATA_CACHE_SIZE); 538 if (mContentStart > mActiveStart || mContentEnd < mActiveEnd 539 || Math.abs(start - mContentStart) > MIN_LOAD_COUNT) { 540 for (int i = mContentStart; i < mContentEnd; ++i) { 541 if (i < start || i >= end) { 542 mData[i % DATA_CACHE_SIZE] = null; 543 } 544 } 545 mContentStart = start; 546 mContentEnd = end; 547 if (mReloadTask != null) mReloadTask.notifyDirty(); 548 } 549 } 550 551 private void updateImageRequests() { 552 if (!mIsActive) return; 553 554 int currentIndex = mCurrentIndex; 555 MediaItem item = mData[currentIndex % DATA_CACHE_SIZE]; 556 if (item == null || item.getPath() != mItemPath) { 557 // current item mismatch - don't request image 558 return; 559 } 560 561 // 1. Find the most wanted request and start it (if not already started). 562 Future<?> task = null; 563 for (int i = 0; i < sImageFetchSeq.length; i++) { 564 int offset = sImageFetchSeq[i].indexOffset; 565 int bit = sImageFetchSeq[i].imageBit; 566 if (bit == BIT_FULL_IMAGE && !mNeedFullImage) continue; 567 task = startTaskIfNeeded(currentIndex + offset, bit); 568 if (task != null) break; 569 } 570 571 // 2. Cancel everything else. 572 for (ImageEntry entry : mImageCache.values()) { 573 if (entry.screenNailTask != null && entry.screenNailTask != task) { 574 entry.screenNailTask.cancel(); 575 entry.screenNailTask = null; 576 entry.requestedBits &= ~BIT_SCREEN_NAIL; 577 } 578 if (entry.fullImageTask != null && entry.fullImageTask != task) { 579 entry.fullImageTask.cancel(); 580 entry.fullImageTask = null; 581 entry.requestedBits &= ~BIT_FULL_IMAGE; 582 } 583 } 584 } 585 586 private static class ScreenNailJob implements Job<ScreenNail> { 587 private MediaItem mItem; 588 589 public ScreenNailJob(MediaItem item) { 590 mItem = item; 591 } 592 593 @Override 594 public ScreenNail run(JobContext jc) { 595 // We try to get a ScreenNail first, if it fails, we fallback to get 596 // a Bitmap and then wrap it in a BitmapScreenNail instead. 597 ScreenNail s = mItem.getScreenNail(); 598 if (s != null) return s; 599 600 Bitmap bitmap = mItem.requestImage(MediaItem.TYPE_THUMBNAIL).run(jc); 601 if (jc.isCancelled()) return null; 602 if (bitmap != null) { 603 bitmap = BitmapUtils.rotateBitmap(bitmap, 604 mItem.getRotation() - mItem.getFullImageRotation(), true); 605 } 606 return bitmap == null ? null : new BitmapScreenNail(bitmap); 607 } 608 } 609 610 // Returns the task if we started the task or the task is already started. 611 private Future<?> startTaskIfNeeded(int index, int which) { 612 if (index < mActiveStart || index >= mActiveEnd) return null; 613 614 ImageEntry entry = mImageCache.get(getVersion(index)); 615 if (entry == null) return null; 616 617 if (which == BIT_SCREEN_NAIL && entry.screenNailTask != null) { 618 return entry.screenNailTask; 619 } else if (which == BIT_FULL_IMAGE && entry.fullImageTask != null) { 620 return entry.fullImageTask; 621 } 622 623 MediaItem item = mData[index % DATA_CACHE_SIZE]; 624 Utils.assertTrue(item != null); 625 626 if (which == BIT_SCREEN_NAIL 627 && (entry.requestedBits & BIT_SCREEN_NAIL) == 0) { 628 entry.requestedBits |= BIT_SCREEN_NAIL; 629 entry.screenNailTask = mThreadPool.submit( 630 new ScreenNailJob(item), 631 new ScreenNailListener(item.getDataVersion())); 632 // request screen nail 633 return entry.screenNailTask; 634 } 635 if (which == BIT_FULL_IMAGE 636 && (entry.requestedBits & BIT_FULL_IMAGE) == 0 637 && (item.getSupportedOperations() 638 & MediaItem.SUPPORT_FULL_IMAGE) != 0) { 639 entry.requestedBits |= BIT_FULL_IMAGE; 640 entry.fullImageTask = mThreadPool.submit( 641 item.requestLargeImage(), 642 new FullImageListener(item.getDataVersion())); 643 // request full image 644 return entry.fullImageTask; 645 } 646 return null; 647 } 648 649 private void updateImageCache() { 650 HashSet<Long> toBeRemoved = new HashSet<Long>(mImageCache.keySet()); 651 for (int i = mActiveStart; i < mActiveEnd; ++i) { 652 MediaItem item = mData[i % DATA_CACHE_SIZE]; 653 long version = item == null 654 ? MediaObject.INVALID_DATA_VERSION 655 : item.getDataVersion(); 656 if (version == MediaObject.INVALID_DATA_VERSION) continue; 657 ImageEntry entry = mImageCache.get(version); 658 toBeRemoved.remove(version); 659 if (entry != null) { 660 if (Math.abs(i - mCurrentIndex) > 1) { 661 if (entry.fullImageTask != null) { 662 entry.fullImageTask.cancel(); 663 entry.fullImageTask = null; 664 } 665 entry.fullImage = null; 666 entry.requestedBits &= ~BIT_FULL_IMAGE; 667 } 668 } else { 669 entry = new ImageEntry(); 670 mImageCache.put(version, entry); 671 } 672 } 673 674 // Clear the data and requests for ImageEntries outside the new window. 675 for (Long version : toBeRemoved) { 676 ImageEntry entry = mImageCache.remove(version); 677 if (entry.fullImageTask != null) entry.fullImageTask.cancel(); 678 if (entry.screenNailTask != null) entry.screenNailTask.cancel(); 679 if (entry.screenNail != null) entry.screenNail.recycle(); 680 } 681 } 682 683 private class FullImageListener 684 implements Runnable, FutureListener<BitmapRegionDecoder> { 685 private final long mVersion; 686 private Future<BitmapRegionDecoder> mFuture; 687 688 public FullImageListener(long version) { 689 mVersion = version; 690 } 691 692 @Override 693 public void onFutureDone(Future<BitmapRegionDecoder> future) { 694 mFuture = future; 695 mMainHandler.sendMessage( 696 mMainHandler.obtainMessage(MSG_RUN_OBJECT, this)); 697 } 698 699 @Override 700 public void run() { 701 updateFullImage(mVersion, mFuture); 702 } 703 } 704 705 private class ScreenNailListener 706 implements Runnable, FutureListener<ScreenNail> { 707 private final long mVersion; 708 private Future<ScreenNail> mFuture; 709 710 public ScreenNailListener(long version) { 711 mVersion = version; 712 } 713 714 @Override 715 public void onFutureDone(Future<ScreenNail> future) { 716 mFuture = future; 717 mMainHandler.sendMessage( 718 mMainHandler.obtainMessage(MSG_RUN_OBJECT, this)); 719 } 720 721 @Override 722 public void run() { 723 updateScreenNail(mVersion, mFuture); 724 } 725 } 726 727 private static class ImageEntry { 728 public int requestedBits = 0; 729 public BitmapRegionDecoder fullImage; 730 public ScreenNail screenNail; 731 public Future<ScreenNail> screenNailTask; 732 public Future<BitmapRegionDecoder> fullImageTask; 733 public boolean failToLoad = false; 734 } 735 736 private class SourceListener implements ContentListener { 737 public void onContentDirty() { 738 if (mReloadTask != null) mReloadTask.notifyDirty(); 739 } 740 } 741 742 private <T> T executeAndWait(Callable<T> callable) { 743 FutureTask<T> task = new FutureTask<T>(callable); 744 mMainHandler.sendMessage( 745 mMainHandler.obtainMessage(MSG_RUN_OBJECT, task)); 746 try { 747 return task.get(); 748 } catch (InterruptedException e) { 749 return null; 750 } catch (ExecutionException e) { 751 throw new RuntimeException(e); 752 } 753 } 754 755 private static class UpdateInfo { 756 public long version; 757 public boolean reloadContent; 758 public Path target; 759 public int indexHint; 760 public int contentStart; 761 public int contentEnd; 762 763 public int size; 764 public ArrayList<MediaItem> items; 765 } 766 767 private class GetUpdateInfo implements Callable<UpdateInfo> { 768 769 private boolean needContentReload() { 770 for (int i = mContentStart, n = mContentEnd; i < n; ++i) { 771 if (mData[i % DATA_CACHE_SIZE] == null) return true; 772 } 773 MediaItem current = mData[mCurrentIndex % DATA_CACHE_SIZE]; 774 return current == null || current.getPath() != mItemPath; 775 } 776 777 @Override 778 public UpdateInfo call() throws Exception { 779 // TODO: Try to load some data in first update 780 UpdateInfo info = new UpdateInfo(); 781 info.version = mSourceVersion; 782 info.reloadContent = needContentReload(); 783 info.target = mItemPath; 784 info.indexHint = mCurrentIndex; 785 info.contentStart = mContentStart; 786 info.contentEnd = mContentEnd; 787 info.size = mSize; 788 return info; 789 } 790 } 791 792 private class UpdateContent implements Callable<Void> { 793 UpdateInfo mUpdateInfo; 794 795 public UpdateContent(UpdateInfo updateInfo) { 796 mUpdateInfo = updateInfo; 797 } 798 799 @Override 800 public Void call() throws Exception { 801 UpdateInfo info = mUpdateInfo; 802 mSourceVersion = info.version; 803 804 if (info.size != mSize) { 805 mSize = info.size; 806 if (mContentEnd > mSize) mContentEnd = mSize; 807 if (mActiveEnd > mSize) mActiveEnd = mSize; 808 } 809 810 if (info.indexHint == MediaSet.INDEX_NOT_FOUND) { 811 // The image has been deleted, clear mItemPath, the 812 // mCurrentIndex will be updated in the updateCurrentItem(). 813 mItemPath = null; 814 updateCurrentItem(); 815 } else { 816 mCurrentIndex = info.indexHint; 817 } 818 819 updateSlidingWindow(); 820 821 if (info.items != null) { 822 int start = Math.max(info.contentStart, mContentStart); 823 int end = Math.min(info.contentStart + info.items.size(), mContentEnd); 824 int dataIndex = start % DATA_CACHE_SIZE; 825 for (int i = start; i < end; ++i) { 826 mData[dataIndex] = info.items.get(i - info.contentStart); 827 if (++dataIndex == DATA_CACHE_SIZE) dataIndex = 0; 828 } 829 } 830 if (mItemPath == null) { 831 MediaItem current = mData[mCurrentIndex % DATA_CACHE_SIZE]; 832 mItemPath = current == null ? null : current.getPath(); 833 } 834 updateImageCache(); 835 updateTileProvider(); 836 updateImageRequests(); 837 fireDataChange(); 838 return null; 839 } 840 841 private void updateCurrentItem() { 842 if (mSize == 0) return; 843 if (mCurrentIndex >= mSize) { 844 mCurrentIndex = mSize - 1; 845 } 846 fireDataChange(); 847 } 848 } 849 850 private class ReloadTask extends Thread { 851 private volatile boolean mActive = true; 852 private volatile boolean mDirty = true; 853 854 private boolean mIsLoading = false; 855 856 private void updateLoading(boolean loading) { 857 if (mIsLoading == loading) return; 858 mIsLoading = loading; 859 mMainHandler.sendEmptyMessage(loading ? MSG_LOAD_START : MSG_LOAD_FINISH); 860 } 861 862 @Override 863 public void run() { 864 while (mActive) { 865 synchronized (this) { 866 if (!mDirty && mActive) { 867 updateLoading(false); 868 Utils.waitWithoutInterrupt(this); 869 continue; 870 } 871 } 872 mDirty = false; 873 UpdateInfo info = executeAndWait(new GetUpdateInfo()); 874 synchronized (DataManager.LOCK) { 875 updateLoading(true); 876 long version = mSource.reload(); 877 if (info.version != version) { 878 info.reloadContent = true; 879 info.size = mSource.getMediaItemCount(); 880 } 881 if (!info.reloadContent) continue; 882 info.items = mSource.getMediaItem(info.contentStart, info.contentEnd); 883 MediaItem item = findCurrentMediaItem(info); 884 if (item == null || item.getPath() != info.target) { 885 info.indexHint = findIndexOfTarget(info); 886 } 887 } 888 executeAndWait(new UpdateContent(info)); 889 } 890 } 891 892 public synchronized void notifyDirty() { 893 mDirty = true; 894 notifyAll(); 895 } 896 897 public synchronized void terminate() { 898 mActive = false; 899 notifyAll(); 900 } 901 902 private MediaItem findCurrentMediaItem(UpdateInfo info) { 903 ArrayList<MediaItem> items = info.items; 904 int index = info.indexHint - info.contentStart; 905 return index < 0 || index >= items.size() ? null : items.get(index); 906 } 907 908 private int findIndexOfTarget(UpdateInfo info) { 909 if (info.target == null) return info.indexHint; 910 ArrayList<MediaItem> items = info.items; 911 912 // First, try to find the item in the data just loaded 913 if (items != null) { 914 for (int i = 0, n = items.size(); i < n; ++i) { 915 if (items.get(i).getPath() == info.target) return i + info.contentStart; 916 } 917 } 918 919 // Not found, find it in mSource. 920 return mSource.getIndexOfItem(info.target, info.indexHint); 921 } 922 } 923} 924