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