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 17 package com.android.gallery3d.app; 18 19 import android.graphics.Bitmap; 20 import android.graphics.BitmapRegionDecoder; 21 import android.os.Handler; 22 import android.os.Message; 23 24 import com.android.gallery3d.common.BitmapUtils; 25 import com.android.gallery3d.common.Utils; 26 import com.android.gallery3d.data.ContentListener; 27 import com.android.gallery3d.data.LocalMediaItem; 28 import com.android.gallery3d.data.MediaItem; 29 import com.android.gallery3d.data.MediaObject; 30 import com.android.gallery3d.data.MediaSet; 31 import com.android.gallery3d.data.Path; 32 import com.android.gallery3d.glrenderer.TiledTexture; 33 import com.android.gallery3d.ui.PhotoView; 34 import com.android.gallery3d.ui.ScreenNail; 35 import com.android.gallery3d.ui.SynchronizedHandler; 36 import com.android.gallery3d.ui.TileImageViewAdapter; 37 import com.android.gallery3d.ui.TiledScreenNail; 38 import com.android.gallery3d.util.Future; 39 import com.android.gallery3d.util.FutureListener; 40 import com.android.gallery3d.util.MediaSetUtils; 41 import com.android.gallery3d.util.ThreadPool; 42 import com.android.gallery3d.util.ThreadPool.Job; 43 import com.android.gallery3d.util.ThreadPool.JobContext; 44 45 import java.util.ArrayList; 46 import java.util.Arrays; 47 import java.util.HashMap; 48 import java.util.HashSet; 49 import java.util.concurrent.Callable; 50 import java.util.concurrent.ExecutionException; 51 import java.util.concurrent.FutureTask; 52 53 public class PhotoDataAdapter implements PhotoPage.Model { 54 @SuppressWarnings("unused") 55 private static final String TAG = "PhotoDataAdapter"; 56 57 private static final int MSG_LOAD_START = 1; 58 private static final int MSG_LOAD_FINISH = 2; 59 private static final int MSG_RUN_OBJECT = 3; 60 private static final int MSG_UPDATE_IMAGE_REQUESTS = 4; 61 62 private static final int MIN_LOAD_COUNT = 16; 63 private static final int DATA_CACHE_SIZE = 256; 64 private static final int SCREEN_NAIL_MAX = PhotoView.SCREEN_NAIL_MAX; 65 private static final int IMAGE_CACHE_SIZE = 2 * SCREEN_NAIL_MAX + 1; 66 67 private static final int BIT_SCREEN_NAIL = 1; 68 private static final int BIT_FULL_IMAGE = 2; 69 70 // sImageFetchSeq is the fetching sequence for images. 71 // We want to fetch the current screennail first (offset = 0), the next 72 // screennail (offset = +1), then the previous screennail (offset = -1) etc. 73 // After all the screennail are fetched, we fetch the full images (only some 74 // of them because of we don't want to use too much memory). 75 private static ImageFetch[] sImageFetchSeq; 76 77 private static class ImageFetch { 78 int indexOffset; 79 int imageBit; ImageFetch(int offset, int bit)80 public ImageFetch(int offset, int bit) { 81 indexOffset = offset; 82 imageBit = bit; 83 } 84 } 85 86 static { 87 int k = 0; 88 sImageFetchSeq = new ImageFetch[1 + (IMAGE_CACHE_SIZE - 1) * 2 + 3]; 89 sImageFetchSeq[k++] = new ImageFetch(0, BIT_SCREEN_NAIL); 90 91 for (int i = 1; i < IMAGE_CACHE_SIZE; ++i) { 92 sImageFetchSeq[k++] = new ImageFetch(i, BIT_SCREEN_NAIL); 93 sImageFetchSeq[k++] = new ImageFetch(-i, BIT_SCREEN_NAIL); 94 } 95 96 sImageFetchSeq[k++] = new ImageFetch(0, BIT_FULL_IMAGE); 97 sImageFetchSeq[k++] = new ImageFetch(1, BIT_FULL_IMAGE); 98 sImageFetchSeq[k++] = new ImageFetch(-1, BIT_FULL_IMAGE); 99 } 100 101 private final TileImageViewAdapter mTileProvider = new TileImageViewAdapter(); 102 103 // PhotoDataAdapter caches MediaItems (data) and ImageEntries (image). 104 // 105 // The MediaItems are stored in the mData array, which has DATA_CACHE_SIZE 106 // entries. The valid index range are [mContentStart, mContentEnd). We keep 107 // mContentEnd - mContentStart <= DATA_CACHE_SIZE, so we can use 108 // (i % DATA_CACHE_SIZE) as index to the array. 109 // 110 // The valid MediaItem window size (mContentEnd - mContentStart) may be 111 // smaller than DATA_CACHE_SIZE because we only update the window and reload 112 // the MediaItems when there are significant changes to the window position 113 // (>= MIN_LOAD_COUNT). 114 private final MediaItem mData[] = new MediaItem[DATA_CACHE_SIZE]; 115 private int mContentStart = 0; 116 private int mContentEnd = 0; 117 118 // The ImageCache is a Path-to-ImageEntry map. It only holds the 119 // ImageEntries in the range of [mActiveStart, mActiveEnd). We also keep 120 // mActiveEnd - mActiveStart <= IMAGE_CACHE_SIZE. Besides, the 121 // [mActiveStart, mActiveEnd) range must be contained within 122 // the [mContentStart, mContentEnd) range. 123 private HashMap<Path, ImageEntry> mImageCache = 124 new HashMap<Path, ImageEntry>(); 125 private int mActiveStart = 0; 126 private int mActiveEnd = 0; 127 128 // mCurrentIndex is the "center" image the user is viewing. The change of 129 // mCurrentIndex triggers the data loading and image loading. 130 private int mCurrentIndex; 131 132 // mChanges keeps the version number (of MediaItem) about the images. If any 133 // of the version number changes, we notify the view. This is used after a 134 // database reload or mCurrentIndex changes. 135 private final long mChanges[] = new long[IMAGE_CACHE_SIZE]; 136 // mPaths keeps the corresponding Path (of MediaItem) for the images. This 137 // is used to determine the item movement. 138 private final Path mPaths[] = new Path[IMAGE_CACHE_SIZE]; 139 140 private final Handler mMainHandler; 141 private final ThreadPool mThreadPool; 142 143 private final PhotoView mPhotoView; 144 private final MediaSet mSource; 145 private ReloadTask mReloadTask; 146 147 private long mSourceVersion = MediaObject.INVALID_DATA_VERSION; 148 private int mSize = 0; 149 private Path mItemPath; 150 private int mCameraIndex; 151 private boolean mIsPanorama; 152 private boolean mIsStaticCamera; 153 private boolean mIsActive; 154 private boolean mNeedFullImage; 155 private int mFocusHintDirection = FOCUS_HINT_NEXT; 156 private Path mFocusHintPath = null; 157 158 public interface DataListener extends LoadingListener { onPhotoChanged(int index, Path item)159 public void onPhotoChanged(int index, Path item); 160 } 161 162 private DataListener mDataListener; 163 164 private final SourceListener mSourceListener = new SourceListener(); 165 private final TiledTexture.Uploader mUploader; 166 167 // The path of the current viewing item will be stored in mItemPath. 168 // If mItemPath is not null, mCurrentIndex is only a hint for where we 169 // can find the item. If mItemPath is null, then we use the mCurrentIndex to 170 // find the image being viewed. cameraIndex is the index of the camera 171 // preview. If cameraIndex < 0, there is no camera preview. PhotoDataAdapter(AbstractGalleryActivity activity, PhotoView view, MediaSet mediaSet, Path itemPath, int indexHint, int cameraIndex, boolean isPanorama, boolean isStaticCamera)172 public PhotoDataAdapter(AbstractGalleryActivity activity, PhotoView view, 173 MediaSet mediaSet, Path itemPath, int indexHint, int cameraIndex, 174 boolean isPanorama, boolean isStaticCamera) { 175 mSource = Utils.checkNotNull(mediaSet); 176 mPhotoView = Utils.checkNotNull(view); 177 mItemPath = Utils.checkNotNull(itemPath); 178 mCurrentIndex = indexHint; 179 mCameraIndex = cameraIndex; 180 mIsPanorama = isPanorama; 181 mIsStaticCamera = isStaticCamera; 182 mThreadPool = activity.getThreadPool(); 183 mNeedFullImage = true; 184 185 Arrays.fill(mChanges, MediaObject.INVALID_DATA_VERSION); 186 187 mUploader = new TiledTexture.Uploader(activity.getGLRoot()); 188 189 mMainHandler = new SynchronizedHandler(activity.getGLRoot()) { 190 @SuppressWarnings("unchecked") 191 @Override 192 public void handleMessage(Message message) { 193 switch (message.what) { 194 case MSG_RUN_OBJECT: 195 ((Runnable) message.obj).run(); 196 return; 197 case MSG_LOAD_START: { 198 if (mDataListener != null) { 199 mDataListener.onLoadingStarted(); 200 } 201 return; 202 } 203 case MSG_LOAD_FINISH: { 204 if (mDataListener != null) { 205 mDataListener.onLoadingFinished(false); 206 } 207 return; 208 } 209 case MSG_UPDATE_IMAGE_REQUESTS: { 210 updateImageRequests(); 211 return; 212 } 213 default: throw new AssertionError(); 214 } 215 } 216 }; 217 218 updateSlidingWindow(); 219 } 220 getItemInternal(int index)221 private MediaItem getItemInternal(int index) { 222 if (index < 0 || index >= mSize) return null; 223 if (index >= mContentStart && index < mContentEnd) { 224 return mData[index % DATA_CACHE_SIZE]; 225 } 226 return null; 227 } 228 getVersion(int index)229 private long getVersion(int index) { 230 MediaItem item = getItemInternal(index); 231 if (item == null) return MediaObject.INVALID_DATA_VERSION; 232 return item.getDataVersion(); 233 } 234 getPath(int index)235 private Path getPath(int index) { 236 MediaItem item = getItemInternal(index); 237 if (item == null) return null; 238 return item.getPath(); 239 } 240 fireDataChange()241 private void fireDataChange() { 242 // First check if data actually changed. 243 boolean changed = false; 244 for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; ++i) { 245 long newVersion = getVersion(mCurrentIndex + i); 246 if (mChanges[i + SCREEN_NAIL_MAX] != newVersion) { 247 mChanges[i + SCREEN_NAIL_MAX] = newVersion; 248 changed = true; 249 } 250 } 251 252 if (!changed) return; 253 254 // Now calculate the fromIndex array. fromIndex represents the item 255 // movement. It records the index where the picture come from. The 256 // special value Integer.MAX_VALUE means it's a new picture. 257 final int N = IMAGE_CACHE_SIZE; 258 int fromIndex[] = new int[N]; 259 260 // Remember the old path array. 261 Path oldPaths[] = new Path[N]; 262 System.arraycopy(mPaths, 0, oldPaths, 0, N); 263 264 // Update the mPaths array. 265 for (int i = 0; i < N; ++i) { 266 mPaths[i] = getPath(mCurrentIndex + i - SCREEN_NAIL_MAX); 267 } 268 269 // Calculate the fromIndex array. 270 for (int i = 0; i < N; i++) { 271 Path p = mPaths[i]; 272 if (p == null) { 273 fromIndex[i] = Integer.MAX_VALUE; 274 continue; 275 } 276 277 // Try to find the same path in the old array 278 int j; 279 for (j = 0; j < N; j++) { 280 if (oldPaths[j] == p) { 281 break; 282 } 283 } 284 fromIndex[i] = (j < N) ? j - SCREEN_NAIL_MAX : Integer.MAX_VALUE; 285 } 286 287 mPhotoView.notifyDataChange(fromIndex, -mCurrentIndex, 288 mSize - 1 - mCurrentIndex); 289 } 290 setDataListener(DataListener listener)291 public void setDataListener(DataListener listener) { 292 mDataListener = listener; 293 } 294 updateScreenNail(Path path, Future<ScreenNail> future)295 private void updateScreenNail(Path path, Future<ScreenNail> future) { 296 ImageEntry entry = mImageCache.get(path); 297 ScreenNail screenNail = future.get(); 298 299 if (entry == null || entry.screenNailTask != future) { 300 if (screenNail != null) screenNail.recycle(); 301 return; 302 } 303 304 entry.screenNailTask = null; 305 306 // Combine the ScreenNails if we already have a BitmapScreenNail 307 if (entry.screenNail instanceof TiledScreenNail) { 308 TiledScreenNail original = (TiledScreenNail) entry.screenNail; 309 screenNail = original.combine(screenNail); 310 } 311 312 if (screenNail == null) { 313 entry.failToLoad = true; 314 } else { 315 entry.failToLoad = false; 316 entry.screenNail = screenNail; 317 } 318 319 for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; ++i) { 320 if (path == getPath(mCurrentIndex + i)) { 321 if (i == 0) updateTileProvider(entry); 322 mPhotoView.notifyImageChange(i); 323 break; 324 } 325 } 326 updateImageRequests(); 327 updateScreenNailUploadQueue(); 328 } 329 updateFullImage(Path path, Future<BitmapRegionDecoder> future)330 private void updateFullImage(Path path, Future<BitmapRegionDecoder> future) { 331 ImageEntry entry = mImageCache.get(path); 332 if (entry == null || entry.fullImageTask != future) { 333 BitmapRegionDecoder fullImage = future.get(); 334 if (fullImage != null) fullImage.recycle(); 335 return; 336 } 337 338 entry.fullImageTask = null; 339 entry.fullImage = future.get(); 340 if (entry.fullImage != null) { 341 if (path == getPath(mCurrentIndex)) { 342 updateTileProvider(entry); 343 mPhotoView.notifyImageChange(0); 344 } 345 } 346 updateImageRequests(); 347 } 348 349 @Override resume()350 public void resume() { 351 mIsActive = true; 352 TiledTexture.prepareResources(); 353 354 mSource.addContentListener(mSourceListener); 355 updateImageCache(); 356 updateImageRequests(); 357 358 mReloadTask = new ReloadTask(); 359 mReloadTask.start(); 360 361 fireDataChange(); 362 } 363 364 @Override pause()365 public void pause() { 366 mIsActive = false; 367 368 mReloadTask.terminate(); 369 mReloadTask = null; 370 371 mSource.removeContentListener(mSourceListener); 372 373 for (ImageEntry entry : mImageCache.values()) { 374 if (entry.fullImageTask != null) entry.fullImageTask.cancel(); 375 if (entry.screenNailTask != null) entry.screenNailTask.cancel(); 376 if (entry.screenNail != null) entry.screenNail.recycle(); 377 } 378 mImageCache.clear(); 379 mTileProvider.clear(); 380 381 mUploader.clear(); 382 TiledTexture.freeResources(); 383 } 384 getItem(int index)385 private MediaItem getItem(int index) { 386 if (index < 0 || index >= mSize || !mIsActive) return null; 387 Utils.assertTrue(index >= mActiveStart && index < mActiveEnd); 388 389 if (index >= mContentStart && index < mContentEnd) { 390 return mData[index % DATA_CACHE_SIZE]; 391 } 392 return null; 393 } 394 updateCurrentIndex(int index)395 private void updateCurrentIndex(int index) { 396 if (mCurrentIndex == index) return; 397 mCurrentIndex = index; 398 updateSlidingWindow(); 399 400 MediaItem item = mData[index % DATA_CACHE_SIZE]; 401 mItemPath = item == null ? null : item.getPath(); 402 403 updateImageCache(); 404 updateImageRequests(); 405 updateTileProvider(); 406 407 if (mDataListener != null) { 408 mDataListener.onPhotoChanged(index, mItemPath); 409 } 410 411 fireDataChange(); 412 } 413 uploadScreenNail(int offset)414 private void uploadScreenNail(int offset) { 415 int index = mCurrentIndex + offset; 416 if (index < mActiveStart || index >= mActiveEnd) return; 417 418 MediaItem item = getItem(index); 419 if (item == null) return; 420 421 ImageEntry e = mImageCache.get(item.getPath()); 422 if (e == null) return; 423 424 ScreenNail s = e.screenNail; 425 if (s instanceof TiledScreenNail) { 426 TiledTexture t = ((TiledScreenNail) s).getTexture(); 427 if (t != null && !t.isReady()) mUploader.addTexture(t); 428 } 429 } 430 updateScreenNailUploadQueue()431 private void updateScreenNailUploadQueue() { 432 mUploader.clear(); 433 uploadScreenNail(0); 434 for (int i = 1; i < IMAGE_CACHE_SIZE; ++i) { 435 uploadScreenNail(i); 436 uploadScreenNail(-i); 437 } 438 } 439 440 @Override moveTo(int index)441 public void moveTo(int index) { 442 updateCurrentIndex(index); 443 } 444 445 @Override getScreenNail(int offset)446 public ScreenNail getScreenNail(int offset) { 447 int index = mCurrentIndex + offset; 448 if (index < 0 || index >= mSize || !mIsActive) return null; 449 Utils.assertTrue(index >= mActiveStart && index < mActiveEnd); 450 451 MediaItem item = getItem(index); 452 if (item == null) return null; 453 454 ImageEntry entry = mImageCache.get(item.getPath()); 455 if (entry == null) return null; 456 457 // Create a default ScreenNail if the real one is not available yet, 458 // except for camera that a black screen is better than a gray tile. 459 if (entry.screenNail == null && !isCamera(offset)) { 460 entry.screenNail = newPlaceholderScreenNail(item); 461 if (offset == 0) updateTileProvider(entry); 462 } 463 464 return entry.screenNail; 465 } 466 467 @Override getImageSize(int offset, PhotoView.Size size)468 public void getImageSize(int offset, PhotoView.Size size) { 469 MediaItem item = getItem(mCurrentIndex + offset); 470 if (item == null) { 471 size.width = 0; 472 size.height = 0; 473 } else { 474 size.width = item.getWidth(); 475 size.height = item.getHeight(); 476 } 477 } 478 479 @Override getImageRotation(int offset)480 public int getImageRotation(int offset) { 481 MediaItem item = getItem(mCurrentIndex + offset); 482 return (item == null) ? 0 : item.getFullImageRotation(); 483 } 484 485 @Override setNeedFullImage(boolean enabled)486 public void setNeedFullImage(boolean enabled) { 487 mNeedFullImage = enabled; 488 mMainHandler.sendEmptyMessage(MSG_UPDATE_IMAGE_REQUESTS); 489 } 490 491 @Override isCamera(int offset)492 public boolean isCamera(int offset) { 493 return mCurrentIndex + offset == mCameraIndex; 494 } 495 496 @Override isPanorama(int offset)497 public boolean isPanorama(int offset) { 498 return isCamera(offset) && mIsPanorama; 499 } 500 501 @Override isStaticCamera(int offset)502 public boolean isStaticCamera(int offset) { 503 return isCamera(offset) && mIsStaticCamera; 504 } 505 506 @Override isVideo(int offset)507 public boolean isVideo(int offset) { 508 MediaItem item = getItem(mCurrentIndex + offset); 509 return (item == null) 510 ? false 511 : item.getMediaType() == MediaItem.MEDIA_TYPE_VIDEO; 512 } 513 514 @Override isDeletable(int offset)515 public boolean isDeletable(int offset) { 516 MediaItem item = getItem(mCurrentIndex + offset); 517 return (item == null) 518 ? false 519 : (item.getSupportedOperations() & MediaItem.SUPPORT_DELETE) != 0; 520 } 521 522 @Override getLoadingState(int offset)523 public int getLoadingState(int offset) { 524 ImageEntry entry = mImageCache.get(getPath(mCurrentIndex + offset)); 525 if (entry == null) return LOADING_INIT; 526 if (entry.failToLoad) return LOADING_FAIL; 527 if (entry.screenNail != null) return LOADING_COMPLETE; 528 return LOADING_INIT; 529 } 530 531 @Override getScreenNail()532 public ScreenNail getScreenNail() { 533 return getScreenNail(0); 534 } 535 536 @Override getImageHeight()537 public int getImageHeight() { 538 return mTileProvider.getImageHeight(); 539 } 540 541 @Override getImageWidth()542 public int getImageWidth() { 543 return mTileProvider.getImageWidth(); 544 } 545 546 @Override getLevelCount()547 public int getLevelCount() { 548 return mTileProvider.getLevelCount(); 549 } 550 551 @Override getTile(int level, int x, int y, int tileSize)552 public Bitmap getTile(int level, int x, int y, int tileSize) { 553 return mTileProvider.getTile(level, x, y, tileSize); 554 } 555 556 @Override isEmpty()557 public boolean isEmpty() { 558 return mSize == 0; 559 } 560 561 @Override getCurrentIndex()562 public int getCurrentIndex() { 563 return mCurrentIndex; 564 } 565 566 @Override getMediaItem(int offset)567 public MediaItem getMediaItem(int offset) { 568 int index = mCurrentIndex + offset; 569 if (index >= mContentStart && index < mContentEnd) { 570 return mData[index % DATA_CACHE_SIZE]; 571 } 572 return null; 573 } 574 575 @Override setCurrentPhoto(Path path, int indexHint)576 public void setCurrentPhoto(Path path, int indexHint) { 577 if (mItemPath == path) return; 578 mItemPath = path; 579 mCurrentIndex = indexHint; 580 updateSlidingWindow(); 581 updateImageCache(); 582 fireDataChange(); 583 584 // We need to reload content if the path doesn't match. 585 MediaItem item = getMediaItem(0); 586 if (item != null && item.getPath() != path) { 587 if (mReloadTask != null) mReloadTask.notifyDirty(); 588 } 589 } 590 591 @Override setFocusHintDirection(int direction)592 public void setFocusHintDirection(int direction) { 593 mFocusHintDirection = direction; 594 } 595 596 @Override setFocusHintPath(Path path)597 public void setFocusHintPath(Path path) { 598 mFocusHintPath = path; 599 } 600 updateTileProvider()601 private void updateTileProvider() { 602 ImageEntry entry = mImageCache.get(getPath(mCurrentIndex)); 603 if (entry == null) { // in loading 604 mTileProvider.clear(); 605 } else { 606 updateTileProvider(entry); 607 } 608 } 609 updateTileProvider(ImageEntry entry)610 private void updateTileProvider(ImageEntry entry) { 611 ScreenNail screenNail = entry.screenNail; 612 BitmapRegionDecoder fullImage = entry.fullImage; 613 if (screenNail != null) { 614 if (fullImage != null) { 615 mTileProvider.setScreenNail(screenNail, 616 fullImage.getWidth(), fullImage.getHeight()); 617 mTileProvider.setRegionDecoder(fullImage); 618 } else { 619 int width = screenNail.getWidth(); 620 int height = screenNail.getHeight(); 621 mTileProvider.setScreenNail(screenNail, width, height); 622 } 623 } else { 624 mTileProvider.clear(); 625 } 626 } 627 updateSlidingWindow()628 private void updateSlidingWindow() { 629 // 1. Update the image window 630 int start = Utils.clamp(mCurrentIndex - IMAGE_CACHE_SIZE / 2, 631 0, Math.max(0, mSize - IMAGE_CACHE_SIZE)); 632 int end = Math.min(mSize, start + IMAGE_CACHE_SIZE); 633 634 if (mActiveStart == start && mActiveEnd == end) return; 635 636 mActiveStart = start; 637 mActiveEnd = end; 638 639 // 2. Update the data window 640 start = Utils.clamp(mCurrentIndex - DATA_CACHE_SIZE / 2, 641 0, Math.max(0, mSize - DATA_CACHE_SIZE)); 642 end = Math.min(mSize, start + DATA_CACHE_SIZE); 643 if (mContentStart > mActiveStart || mContentEnd < mActiveEnd 644 || Math.abs(start - mContentStart) > MIN_LOAD_COUNT) { 645 for (int i = mContentStart; i < mContentEnd; ++i) { 646 if (i < start || i >= end) { 647 mData[i % DATA_CACHE_SIZE] = null; 648 } 649 } 650 mContentStart = start; 651 mContentEnd = end; 652 if (mReloadTask != null) mReloadTask.notifyDirty(); 653 } 654 } 655 updateImageRequests()656 private void updateImageRequests() { 657 if (!mIsActive) return; 658 659 int currentIndex = mCurrentIndex; 660 MediaItem item = mData[currentIndex % DATA_CACHE_SIZE]; 661 if (item == null || item.getPath() != mItemPath) { 662 // current item mismatch - don't request image 663 return; 664 } 665 666 // 1. Find the most wanted request and start it (if not already started). 667 Future<?> task = null; 668 for (int i = 0; i < sImageFetchSeq.length; i++) { 669 int offset = sImageFetchSeq[i].indexOffset; 670 int bit = sImageFetchSeq[i].imageBit; 671 if (bit == BIT_FULL_IMAGE && !mNeedFullImage) continue; 672 task = startTaskIfNeeded(currentIndex + offset, bit); 673 if (task != null) break; 674 } 675 676 // 2. Cancel everything else. 677 for (ImageEntry entry : mImageCache.values()) { 678 if (entry.screenNailTask != null && entry.screenNailTask != task) { 679 entry.screenNailTask.cancel(); 680 entry.screenNailTask = null; 681 entry.requestedScreenNail = MediaObject.INVALID_DATA_VERSION; 682 } 683 if (entry.fullImageTask != null && entry.fullImageTask != task) { 684 entry.fullImageTask.cancel(); 685 entry.fullImageTask = null; 686 entry.requestedFullImage = MediaObject.INVALID_DATA_VERSION; 687 } 688 } 689 } 690 691 private class ScreenNailJob implements Job<ScreenNail> { 692 private MediaItem mItem; 693 ScreenNailJob(MediaItem item)694 public ScreenNailJob(MediaItem item) { 695 mItem = item; 696 } 697 698 @Override run(JobContext jc)699 public ScreenNail run(JobContext jc) { 700 // We try to get a ScreenNail first, if it fails, we fallback to get 701 // a Bitmap and then wrap it in a BitmapScreenNail instead. 702 ScreenNail s = mItem.getScreenNail(); 703 if (s != null) return s; 704 705 // If this is a temporary item, don't try to get its bitmap because 706 // it won't be available. We will get its bitmap after a data reload. 707 if (isTemporaryItem(mItem)) { 708 return newPlaceholderScreenNail(mItem); 709 } 710 711 Bitmap bitmap = mItem.requestImage(MediaItem.TYPE_THUMBNAIL).run(jc); 712 if (jc.isCancelled()) return null; 713 if (bitmap != null) { 714 bitmap = BitmapUtils.rotateBitmap(bitmap, 715 mItem.getRotation() - mItem.getFullImageRotation(), true); 716 } 717 return bitmap == null ? null : new TiledScreenNail(bitmap); 718 } 719 } 720 721 private class FullImageJob implements Job<BitmapRegionDecoder> { 722 private MediaItem mItem; 723 FullImageJob(MediaItem item)724 public FullImageJob(MediaItem item) { 725 mItem = item; 726 } 727 728 @Override run(JobContext jc)729 public BitmapRegionDecoder run(JobContext jc) { 730 if (isTemporaryItem(mItem)) { 731 return null; 732 } 733 return mItem.requestLargeImage().run(jc); 734 } 735 } 736 737 // Returns true if we think this is a temporary item created by Camera. A 738 // temporary item is an image or a video whose data is still being 739 // processed, but an incomplete entry is created first in MediaProvider, so 740 // we can display them (in grey tile) even if they are not saved to disk 741 // yet. When the image or video data is actually saved, we will get 742 // notification from MediaProvider, reload data, and show the actual image 743 // or video data. isTemporaryItem(MediaItem mediaItem)744 private boolean isTemporaryItem(MediaItem mediaItem) { 745 // Must have camera to create a temporary item. 746 if (mCameraIndex < 0) return false; 747 // Must be an item in camera roll. 748 if (!(mediaItem instanceof LocalMediaItem)) return false; 749 LocalMediaItem item = (LocalMediaItem) mediaItem; 750 if (item.getBucketId() != MediaSetUtils.CAMERA_BUCKET_ID) return false; 751 // Must have no size, but must have width and height information 752 if (item.getSize() != 0) return false; 753 if (item.getWidth() == 0) return false; 754 if (item.getHeight() == 0) return false; 755 // Must be created in the last 10 seconds. 756 if (item.getDateInMs() - System.currentTimeMillis() > 10000) return false; 757 return true; 758 } 759 760 // Create a default ScreenNail when a ScreenNail is needed, but we don't yet 761 // have one available (because the image data is still being saved, or the 762 // Bitmap is still being loaded. newPlaceholderScreenNail(MediaItem item)763 private ScreenNail newPlaceholderScreenNail(MediaItem item) { 764 int width = item.getWidth(); 765 int height = item.getHeight(); 766 return new TiledScreenNail(width, height); 767 } 768 769 // Returns the task if we started the task or the task is already started. startTaskIfNeeded(int index, int which)770 private Future<?> startTaskIfNeeded(int index, int which) { 771 if (index < mActiveStart || index >= mActiveEnd) return null; 772 773 ImageEntry entry = mImageCache.get(getPath(index)); 774 if (entry == null) return null; 775 MediaItem item = mData[index % DATA_CACHE_SIZE]; 776 Utils.assertTrue(item != null); 777 long version = item.getDataVersion(); 778 779 if (which == BIT_SCREEN_NAIL && entry.screenNailTask != null 780 && entry.requestedScreenNail == version) { 781 return entry.screenNailTask; 782 } else if (which == BIT_FULL_IMAGE && entry.fullImageTask != null 783 && entry.requestedFullImage == version) { 784 return entry.fullImageTask; 785 } 786 787 if (which == BIT_SCREEN_NAIL && entry.requestedScreenNail != version) { 788 entry.requestedScreenNail = version; 789 entry.screenNailTask = mThreadPool.submit( 790 new ScreenNailJob(item), 791 new ScreenNailListener(item)); 792 // request screen nail 793 return entry.screenNailTask; 794 } 795 if (which == BIT_FULL_IMAGE && entry.requestedFullImage != version 796 && (item.getSupportedOperations() 797 & MediaItem.SUPPORT_FULL_IMAGE) != 0) { 798 entry.requestedFullImage = version; 799 entry.fullImageTask = mThreadPool.submit( 800 new FullImageJob(item), 801 new FullImageListener(item)); 802 // request full image 803 return entry.fullImageTask; 804 } 805 return null; 806 } 807 updateImageCache()808 private void updateImageCache() { 809 HashSet<Path> toBeRemoved = new HashSet<Path>(mImageCache.keySet()); 810 for (int i = mActiveStart; i < mActiveEnd; ++i) { 811 MediaItem item = mData[i % DATA_CACHE_SIZE]; 812 if (item == null) continue; 813 Path path = item.getPath(); 814 ImageEntry entry = mImageCache.get(path); 815 toBeRemoved.remove(path); 816 if (entry != null) { 817 if (Math.abs(i - mCurrentIndex) > 1) { 818 if (entry.fullImageTask != null) { 819 entry.fullImageTask.cancel(); 820 entry.fullImageTask = null; 821 } 822 entry.fullImage = null; 823 entry.requestedFullImage = MediaObject.INVALID_DATA_VERSION; 824 } 825 if (entry.requestedScreenNail != item.getDataVersion()) { 826 // This ScreenNail is outdated, we want to update it if it's 827 // still a placeholder. 828 if (entry.screenNail instanceof TiledScreenNail) { 829 TiledScreenNail s = (TiledScreenNail) entry.screenNail; 830 s.updatePlaceholderSize( 831 item.getWidth(), item.getHeight()); 832 } 833 } 834 } else { 835 entry = new ImageEntry(); 836 mImageCache.put(path, entry); 837 } 838 } 839 840 // Clear the data and requests for ImageEntries outside the new window. 841 for (Path path : toBeRemoved) { 842 ImageEntry entry = mImageCache.remove(path); 843 if (entry.fullImageTask != null) entry.fullImageTask.cancel(); 844 if (entry.screenNailTask != null) entry.screenNailTask.cancel(); 845 if (entry.screenNail != null) entry.screenNail.recycle(); 846 } 847 848 updateScreenNailUploadQueue(); 849 } 850 851 private class FullImageListener 852 implements Runnable, FutureListener<BitmapRegionDecoder> { 853 private final Path mPath; 854 private Future<BitmapRegionDecoder> mFuture; 855 FullImageListener(MediaItem item)856 public FullImageListener(MediaItem item) { 857 mPath = item.getPath(); 858 } 859 860 @Override onFutureDone(Future<BitmapRegionDecoder> future)861 public void onFutureDone(Future<BitmapRegionDecoder> future) { 862 mFuture = future; 863 mMainHandler.sendMessage( 864 mMainHandler.obtainMessage(MSG_RUN_OBJECT, this)); 865 } 866 867 @Override run()868 public void run() { 869 updateFullImage(mPath, mFuture); 870 } 871 } 872 873 private class ScreenNailListener 874 implements Runnable, FutureListener<ScreenNail> { 875 private final Path mPath; 876 private Future<ScreenNail> mFuture; 877 ScreenNailListener(MediaItem item)878 public ScreenNailListener(MediaItem item) { 879 mPath = item.getPath(); 880 } 881 882 @Override onFutureDone(Future<ScreenNail> future)883 public void onFutureDone(Future<ScreenNail> future) { 884 mFuture = future; 885 mMainHandler.sendMessage( 886 mMainHandler.obtainMessage(MSG_RUN_OBJECT, this)); 887 } 888 889 @Override run()890 public void run() { 891 updateScreenNail(mPath, mFuture); 892 } 893 } 894 895 private static class ImageEntry { 896 public BitmapRegionDecoder fullImage; 897 public ScreenNail screenNail; 898 public Future<ScreenNail> screenNailTask; 899 public Future<BitmapRegionDecoder> fullImageTask; 900 public long requestedScreenNail = MediaObject.INVALID_DATA_VERSION; 901 public long requestedFullImage = MediaObject.INVALID_DATA_VERSION; 902 public boolean failToLoad = false; 903 } 904 905 private class SourceListener implements ContentListener { 906 @Override onContentDirty()907 public void onContentDirty() { 908 if (mReloadTask != null) mReloadTask.notifyDirty(); 909 } 910 } 911 executeAndWait(Callable<T> callable)912 private <T> T executeAndWait(Callable<T> callable) { 913 FutureTask<T> task = new FutureTask<T>(callable); 914 mMainHandler.sendMessage( 915 mMainHandler.obtainMessage(MSG_RUN_OBJECT, task)); 916 try { 917 return task.get(); 918 } catch (InterruptedException e) { 919 return null; 920 } catch (ExecutionException e) { 921 throw new RuntimeException(e); 922 } 923 } 924 925 private static class UpdateInfo { 926 public long version; 927 public boolean reloadContent; 928 public Path target; 929 public int indexHint; 930 public int contentStart; 931 public int contentEnd; 932 933 public int size; 934 public ArrayList<MediaItem> items; 935 } 936 937 private class GetUpdateInfo implements Callable<UpdateInfo> { 938 needContentReload()939 private boolean needContentReload() { 940 for (int i = mContentStart, n = mContentEnd; i < n; ++i) { 941 if (mData[i % DATA_CACHE_SIZE] == null) return true; 942 } 943 MediaItem current = mData[mCurrentIndex % DATA_CACHE_SIZE]; 944 return current == null || current.getPath() != mItemPath; 945 } 946 947 @Override call()948 public UpdateInfo call() throws Exception { 949 // TODO: Try to load some data in first update 950 UpdateInfo info = new UpdateInfo(); 951 info.version = mSourceVersion; 952 info.reloadContent = needContentReload(); 953 info.target = mItemPath; 954 info.indexHint = mCurrentIndex; 955 info.contentStart = mContentStart; 956 info.contentEnd = mContentEnd; 957 info.size = mSize; 958 return info; 959 } 960 } 961 962 private class UpdateContent implements Callable<Void> { 963 UpdateInfo mUpdateInfo; 964 UpdateContent(UpdateInfo updateInfo)965 public UpdateContent(UpdateInfo updateInfo) { 966 mUpdateInfo = updateInfo; 967 } 968 969 @Override call()970 public Void call() throws Exception { 971 UpdateInfo info = mUpdateInfo; 972 mSourceVersion = info.version; 973 974 if (info.size != mSize) { 975 mSize = info.size; 976 if (mContentEnd > mSize) mContentEnd = mSize; 977 if (mActiveEnd > mSize) mActiveEnd = mSize; 978 } 979 980 mCurrentIndex = info.indexHint; 981 updateSlidingWindow(); 982 983 if (info.items != null) { 984 int start = Math.max(info.contentStart, mContentStart); 985 int end = Math.min(info.contentStart + info.items.size(), mContentEnd); 986 int dataIndex = start % DATA_CACHE_SIZE; 987 for (int i = start; i < end; ++i) { 988 mData[dataIndex] = info.items.get(i - info.contentStart); 989 if (++dataIndex == DATA_CACHE_SIZE) dataIndex = 0; 990 } 991 } 992 993 // update mItemPath 994 MediaItem current = mData[mCurrentIndex % DATA_CACHE_SIZE]; 995 mItemPath = current == null ? null : current.getPath(); 996 997 updateImageCache(); 998 updateTileProvider(); 999 updateImageRequests(); 1000 1001 if (mDataListener != null) { 1002 mDataListener.onPhotoChanged(mCurrentIndex, mItemPath); 1003 } 1004 1005 fireDataChange(); 1006 return null; 1007 } 1008 } 1009 1010 private class ReloadTask extends Thread { 1011 private volatile boolean mActive = true; 1012 private volatile boolean mDirty = true; 1013 1014 private boolean mIsLoading = false; 1015 updateLoading(boolean loading)1016 private void updateLoading(boolean loading) { 1017 if (mIsLoading == loading) return; 1018 mIsLoading = loading; 1019 mMainHandler.sendEmptyMessage(loading ? MSG_LOAD_START : MSG_LOAD_FINISH); 1020 } 1021 1022 @Override run()1023 public void run() { 1024 while (mActive) { 1025 synchronized (this) { 1026 if (!mDirty && mActive) { 1027 updateLoading(false); 1028 Utils.waitWithoutInterrupt(this); 1029 continue; 1030 } 1031 } 1032 mDirty = false; 1033 UpdateInfo info = executeAndWait(new GetUpdateInfo()); 1034 updateLoading(true); 1035 long version = mSource.reload(); 1036 if (info.version != version) { 1037 info.reloadContent = true; 1038 info.size = mSource.getMediaItemCount(); 1039 } 1040 if (!info.reloadContent) continue; 1041 info.items = mSource.getMediaItem( 1042 info.contentStart, info.contentEnd); 1043 1044 int index = MediaSet.INDEX_NOT_FOUND; 1045 1046 // First try to focus on the given hint path if there is one. 1047 if (mFocusHintPath != null) { 1048 index = findIndexOfPathInCache(info, mFocusHintPath); 1049 mFocusHintPath = null; 1050 } 1051 1052 // Otherwise try to see if the currently focused item can be found. 1053 if (index == MediaSet.INDEX_NOT_FOUND) { 1054 MediaItem item = findCurrentMediaItem(info); 1055 if (item != null && item.getPath() == info.target) { 1056 index = info.indexHint; 1057 } else { 1058 index = findIndexOfTarget(info); 1059 } 1060 } 1061 1062 // The image has been deleted. Focus on the next image (keep 1063 // mCurrentIndex unchanged) or the previous image (decrease 1064 // mCurrentIndex by 1). In page mode we want to see the next 1065 // image, so we focus on the next one. In film mode we want the 1066 // later images to shift left to fill the empty space, so we 1067 // focus on the previous image (so it will not move). In any 1068 // case the index needs to be limited to [0, mSize). 1069 if (index == MediaSet.INDEX_NOT_FOUND) { 1070 index = info.indexHint; 1071 int focusHintDirection = mFocusHintDirection; 1072 if (index == (mCameraIndex + 1)) { 1073 focusHintDirection = FOCUS_HINT_NEXT; 1074 } 1075 if (focusHintDirection == FOCUS_HINT_PREVIOUS 1076 && index > 0) { 1077 index--; 1078 } 1079 } 1080 1081 // Don't change index if mSize == 0 1082 if (mSize > 0) { 1083 if (index >= mSize) index = mSize - 1; 1084 } 1085 1086 info.indexHint = index; 1087 1088 executeAndWait(new UpdateContent(info)); 1089 } 1090 } 1091 notifyDirty()1092 public synchronized void notifyDirty() { 1093 mDirty = true; 1094 notifyAll(); 1095 } 1096 terminate()1097 public synchronized void terminate() { 1098 mActive = false; 1099 notifyAll(); 1100 } 1101 findCurrentMediaItem(UpdateInfo info)1102 private MediaItem findCurrentMediaItem(UpdateInfo info) { 1103 ArrayList<MediaItem> items = info.items; 1104 int index = info.indexHint - info.contentStart; 1105 return index < 0 || index >= items.size() ? null : items.get(index); 1106 } 1107 findIndexOfTarget(UpdateInfo info)1108 private int findIndexOfTarget(UpdateInfo info) { 1109 if (info.target == null) return info.indexHint; 1110 ArrayList<MediaItem> items = info.items; 1111 1112 // First, try to find the item in the data just loaded 1113 if (items != null) { 1114 int i = findIndexOfPathInCache(info, info.target); 1115 if (i != MediaSet.INDEX_NOT_FOUND) return i; 1116 } 1117 1118 // Not found, find it in mSource. 1119 return mSource.getIndexOfItem(info.target, info.indexHint); 1120 } 1121 findIndexOfPathInCache(UpdateInfo info, Path path)1122 private int findIndexOfPathInCache(UpdateInfo info, Path path) { 1123 ArrayList<MediaItem> items = info.items; 1124 for (int i = 0, n = items.size(); i < n; ++i) { 1125 MediaItem item = items.get(i); 1126 if (item != null && item.getPath() == path) { 1127 return i + info.contentStart; 1128 } 1129 } 1130 return MediaSet.INDEX_NOT_FOUND; 1131 } 1132 } 1133 } 1134