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