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