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; 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 { 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. 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 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 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 235 private Path getPath(int index) { 236 MediaItem item = getItemInternal(index); 237 if (item == null) return null; 238 return item.getPath(); 239 } 240 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 291 public void setDataListener(DataListener listener) { 292 mDataListener = listener; 293 } 294 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 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 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 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 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 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 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 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 441 public void moveTo(int index) { 442 updateCurrentIndex(index); 443 } 444 445 @Override 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 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 480 public int getImageRotation(int offset) { 481 MediaItem item = getItem(mCurrentIndex + offset); 482 return (item == null) ? 0 : item.getFullImageRotation(); 483 } 484 485 @Override 486 public void setNeedFullImage(boolean enabled) { 487 mNeedFullImage = enabled; 488 mMainHandler.sendEmptyMessage(MSG_UPDATE_IMAGE_REQUESTS); 489 } 490 491 @Override 492 public boolean isCamera(int offset) { 493 return mCurrentIndex + offset == mCameraIndex; 494 } 495 496 @Override 497 public boolean isPanorama(int offset) { 498 return isCamera(offset) && mIsPanorama; 499 } 500 501 @Override 502 public boolean isStaticCamera(int offset) { 503 return isCamera(offset) && mIsStaticCamera; 504 } 505 506 @Override 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 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 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 532 public ScreenNail getScreenNail() { 533 return getScreenNail(0); 534 } 535 536 @Override 537 public int getImageHeight() { 538 return mTileProvider.getImageHeight(); 539 } 540 541 @Override 542 public int getImageWidth() { 543 return mTileProvider.getImageWidth(); 544 } 545 546 @Override 547 public int getLevelCount() { 548 return mTileProvider.getLevelCount(); 549 } 550 551 @Override 552 public Bitmap getTile(int level, int x, int y, int tileSize) { 553 return mTileProvider.getTile(level, x, y, tileSize); 554 } 555 556 @Override 557 public boolean isEmpty() { 558 return mSize == 0; 559 } 560 561 @Override 562 public int getCurrentIndex() { 563 return mCurrentIndex; 564 } 565 566 @Override 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 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 592 public void setFocusHintDirection(int direction) { 593 mFocusHintDirection = direction; 594 } 595 596 @Override 597 public void setFocusHintPath(Path path) { 598 mFocusHintPath = path; 599 } 600 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 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 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 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 694 public ScreenNailJob(MediaItem item) { 695 mItem = item; 696 } 697 698 @Override 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 724 public FullImageJob(MediaItem item) { 725 mItem = item; 726 } 727 728 @Override 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. 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. 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. 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 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 856 public FullImageListener(MediaItem item) { 857 mPath = item.getPath(); 858 } 859 860 @Override 861 public void onFutureDone(Future<BitmapRegionDecoder> future) { 862 mFuture = future; 863 mMainHandler.sendMessage( 864 mMainHandler.obtainMessage(MSG_RUN_OBJECT, this)); 865 } 866 867 @Override 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 878 public ScreenNailListener(MediaItem item) { 879 mPath = item.getPath(); 880 } 881 882 @Override 883 public void onFutureDone(Future<ScreenNail> future) { 884 mFuture = future; 885 mMainHandler.sendMessage( 886 mMainHandler.obtainMessage(MSG_RUN_OBJECT, this)); 887 } 888 889 @Override 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 907 public void onContentDirty() { 908 if (mReloadTask != null) mReloadTask.notifyDirty(); 909 } 910 } 911 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 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 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 965 public UpdateContent(UpdateInfo updateInfo) { 966 mUpdateInfo = updateInfo; 967 } 968 969 @Override 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 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 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 1092 public synchronized void notifyDirty() { 1093 mDirty = true; 1094 notifyAll(); 1095 } 1096 1097 public synchronized void terminate() { 1098 mActive = false; 1099 notifyAll(); 1100 } 1101 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 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 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