PhotoDataAdapter.java revision 6b891c6a3739f8c49d42f9db6fc76cb92c7c5f25
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 public void resume() { 345 mIsActive = true; 346 mSource.addContentListener(mSourceListener); 347 updateImageCache(); 348 updateImageRequests(); 349 350 mReloadTask = new ReloadTask(); 351 mReloadTask.start(); 352 353 fireDataChange(); 354 } 355 356 public void pause() { 357 mIsActive = false; 358 359 mReloadTask.terminate(); 360 mReloadTask = null; 361 362 mSource.removeContentListener(mSourceListener); 363 364 for (ImageEntry entry : mImageCache.values()) { 365 if (entry.fullImageTask != null) entry.fullImageTask.cancel(); 366 if (entry.screenNailTask != null) entry.screenNailTask.cancel(); 367 if (entry.screenNail != null) entry.screenNail.recycle(); 368 } 369 mImageCache.clear(); 370 mTileProvider.clear(); 371 } 372 373 private MediaItem getItem(int index) { 374 if (index < 0 || index >= mSize || !mIsActive) return null; 375 Utils.assertTrue(index >= mActiveStart && index < mActiveEnd); 376 377 if (index >= mContentStart && index < mContentEnd) { 378 return mData[index % DATA_CACHE_SIZE]; 379 } 380 return null; 381 } 382 383 private void updateCurrentIndex(int index) { 384 if (mCurrentIndex == index) return; 385 mCurrentIndex = index; 386 updateSlidingWindow(); 387 388 MediaItem item = mData[index % DATA_CACHE_SIZE]; 389 mItemPath = item == null ? null : item.getPath(); 390 391 updateImageCache(); 392 updateImageRequests(); 393 updateTileProvider(); 394 395 if (mDataListener != null) { 396 mDataListener.onPhotoChanged(index, mItemPath); 397 } 398 399 fireDataChange(); 400 } 401 402 @Override 403 public void moveTo(int index) { 404 updateCurrentIndex(index); 405 } 406 407 @Override 408 public ScreenNail getScreenNail(int offset) { 409 int index = mCurrentIndex + offset; 410 if (index < 0 || index >= mSize || !mIsActive) return null; 411 Utils.assertTrue(index >= mActiveStart && index < mActiveEnd); 412 413 MediaItem item = getItem(index); 414 if (item == null) return null; 415 416 ImageEntry entry = mImageCache.get(item.getPath()); 417 if (entry == null) return null; 418 419 // Create a default ScreenNail if the real one is not available yet, 420 // except for camera that a black screen is better than a gray tile. 421 if (entry.screenNail == null && !isCamera(offset)) { 422 entry.screenNail = newPlaceholderScreenNail(item); 423 if (offset == 0) updateTileProvider(entry); 424 } 425 426 return entry.screenNail; 427 } 428 429 @Override 430 public void getImageSize(int offset, PhotoView.Size size) { 431 MediaItem item = getItem(mCurrentIndex + offset); 432 if (item == null) { 433 size.width = 0; 434 size.height = 0; 435 } else { 436 size.width = item.getWidth(); 437 size.height = item.getHeight(); 438 } 439 } 440 441 @Override 442 public int getImageRotation(int offset) { 443 MediaItem item = getItem(mCurrentIndex + offset); 444 return (item == null) ? 0 : item.getFullImageRotation(); 445 } 446 447 @Override 448 public void setNeedFullImage(boolean enabled) { 449 mNeedFullImage = enabled; 450 mMainHandler.sendEmptyMessage(MSG_UPDATE_IMAGE_REQUESTS); 451 } 452 453 @Override 454 public boolean isCamera(int offset) { 455 return mCurrentIndex + offset == mCameraIndex; 456 } 457 458 @Override 459 public boolean isPanorama(int offset) { 460 return isCamera(offset) && mIsPanorama; 461 } 462 463 @Override 464 public boolean isVideo(int offset) { 465 MediaItem item = getItem(mCurrentIndex + offset); 466 return (item == null) 467 ? false 468 : item.getMediaType() == MediaItem.MEDIA_TYPE_VIDEO; 469 } 470 471 @Override 472 public boolean isDeletable(int offset) { 473 MediaItem item = getItem(mCurrentIndex + offset); 474 return (item == null) 475 ? false 476 : (item.getSupportedOperations() & MediaItem.SUPPORT_DELETE) != 0; 477 } 478 479 @Override 480 public int getLoadingState(int offset) { 481 ImageEntry entry = mImageCache.get(getPath(mCurrentIndex + offset)); 482 if (entry == null) return LOADING_INIT; 483 if (entry.failToLoad) return LOADING_FAIL; 484 if (entry.screenNail != null) return LOADING_COMPLETE; 485 return LOADING_INIT; 486 } 487 488 public ScreenNail getScreenNail() { 489 return getScreenNail(0); 490 } 491 492 public int getImageHeight() { 493 return mTileProvider.getImageHeight(); 494 } 495 496 public int getImageWidth() { 497 return mTileProvider.getImageWidth(); 498 } 499 500 public int getLevelCount() { 501 return mTileProvider.getLevelCount(); 502 } 503 504 public Bitmap getTile(int level, int x, int y, int tileSize, 505 int borderSize, BitmapPool pool) { 506 return mTileProvider.getTile(level, x, y, tileSize, borderSize, pool); 507 } 508 509 public boolean isEmpty() { 510 return mSize == 0; 511 } 512 513 public int getCurrentIndex() { 514 return mCurrentIndex; 515 } 516 517 public MediaItem getMediaItem(int offset) { 518 int index = mCurrentIndex + offset; 519 if (index >= mContentStart && index < mContentEnd) { 520 return mData[index % DATA_CACHE_SIZE]; 521 } 522 return null; 523 } 524 525 public void setCurrentPhoto(Path path, int indexHint) { 526 if (mItemPath == path) return; 527 mItemPath = path; 528 mCurrentIndex = indexHint; 529 updateSlidingWindow(); 530 updateImageCache(); 531 fireDataChange(); 532 533 // We need to reload content if the path doesn't match. 534 MediaItem item = getMediaItem(0); 535 if (item != null && item.getPath() != path) { 536 if (mReloadTask != null) mReloadTask.notifyDirty(); 537 } 538 } 539 540 public void setFocusHintDirection(int direction) { 541 mFocusHintDirection = direction; 542 } 543 544 public void setFocusHintPath(Path path) { 545 mFocusHintPath = path; 546 } 547 548 private void updateTileProvider() { 549 ImageEntry entry = mImageCache.get(getPath(mCurrentIndex)); 550 if (entry == null) { // in loading 551 mTileProvider.clear(); 552 } else { 553 updateTileProvider(entry); 554 } 555 } 556 557 private void updateTileProvider(ImageEntry entry) { 558 ScreenNail screenNail = entry.screenNail; 559 BitmapRegionDecoder fullImage = entry.fullImage; 560 if (screenNail != null) { 561 if (fullImage != null) { 562 mTileProvider.setScreenNail(screenNail, 563 fullImage.getWidth(), fullImage.getHeight()); 564 mTileProvider.setRegionDecoder(fullImage); 565 } else { 566 int width = screenNail.getWidth(); 567 int height = screenNail.getHeight(); 568 mTileProvider.setScreenNail(screenNail, width, height); 569 } 570 } else { 571 mTileProvider.clear(); 572 } 573 } 574 575 private void updateSlidingWindow() { 576 // 1. Update the image window 577 int start = Utils.clamp(mCurrentIndex - IMAGE_CACHE_SIZE / 2, 578 0, Math.max(0, mSize - IMAGE_CACHE_SIZE)); 579 int end = Math.min(mSize, start + IMAGE_CACHE_SIZE); 580 581 if (mActiveStart == start && mActiveEnd == end) return; 582 583 mActiveStart = start; 584 mActiveEnd = end; 585 586 // 2. Update the data window 587 start = Utils.clamp(mCurrentIndex - DATA_CACHE_SIZE / 2, 588 0, Math.max(0, mSize - DATA_CACHE_SIZE)); 589 end = Math.min(mSize, start + DATA_CACHE_SIZE); 590 if (mContentStart > mActiveStart || mContentEnd < mActiveEnd 591 || Math.abs(start - mContentStart) > MIN_LOAD_COUNT) { 592 for (int i = mContentStart; i < mContentEnd; ++i) { 593 if (i < start || i >= end) { 594 mData[i % DATA_CACHE_SIZE] = null; 595 } 596 } 597 mContentStart = start; 598 mContentEnd = end; 599 if (mReloadTask != null) mReloadTask.notifyDirty(); 600 } 601 } 602 603 private void updateImageRequests() { 604 if (!mIsActive) return; 605 606 int currentIndex = mCurrentIndex; 607 MediaItem item = mData[currentIndex % DATA_CACHE_SIZE]; 608 if (item == null || item.getPath() != mItemPath) { 609 // current item mismatch - don't request image 610 return; 611 } 612 613 // 1. Find the most wanted request and start it (if not already started). 614 Future<?> task = null; 615 for (int i = 0; i < sImageFetchSeq.length; i++) { 616 int offset = sImageFetchSeq[i].indexOffset; 617 int bit = sImageFetchSeq[i].imageBit; 618 if (bit == BIT_FULL_IMAGE && !mNeedFullImage) continue; 619 task = startTaskIfNeeded(currentIndex + offset, bit); 620 if (task != null) break; 621 } 622 623 // 2. Cancel everything else. 624 for (ImageEntry entry : mImageCache.values()) { 625 if (entry.screenNailTask != null && entry.screenNailTask != task) { 626 entry.screenNailTask.cancel(); 627 entry.screenNailTask = null; 628 entry.requestedScreenNail = MediaObject.INVALID_DATA_VERSION; 629 } 630 if (entry.fullImageTask != null && entry.fullImageTask != task) { 631 entry.fullImageTask.cancel(); 632 entry.fullImageTask = null; 633 entry.requestedFullImage = MediaObject.INVALID_DATA_VERSION; 634 } 635 } 636 } 637 638 private class ScreenNailJob implements Job<ScreenNail> { 639 private MediaItem mItem; 640 641 public ScreenNailJob(MediaItem item) { 642 mItem = item; 643 } 644 645 @Override 646 public ScreenNail run(JobContext jc) { 647 // We try to get a ScreenNail first, if it fails, we fallback to get 648 // a Bitmap and then wrap it in a BitmapScreenNail instead. 649 ScreenNail s = mItem.getScreenNail(); 650 if (s != null) return s; 651 652 // If this is a temporary item, don't try to get its bitmap because 653 // it won't be available. We will get its bitmap after a data reload. 654 if (isTemporaryItem(mItem)) { 655 return newPlaceholderScreenNail(mItem); 656 } 657 658 Bitmap bitmap = mItem.requestImage(MediaItem.TYPE_THUMBNAIL).run(jc); 659 if (jc.isCancelled()) return null; 660 if (bitmap != null) { 661 bitmap = BitmapUtils.rotateBitmap(bitmap, 662 mItem.getRotation() - mItem.getFullImageRotation(), true); 663 } 664 return bitmap == null ? null : new BitmapScreenNail(bitmap); 665 } 666 } 667 668 private class FullImageJob implements Job<BitmapRegionDecoder> { 669 private MediaItem mItem; 670 671 public FullImageJob(MediaItem item) { 672 mItem = item; 673 } 674 675 @Override 676 public BitmapRegionDecoder run(JobContext jc) { 677 if (isTemporaryItem(mItem)) { 678 return null; 679 } 680 return mItem.requestLargeImage().run(jc); 681 } 682 } 683 684 // Returns true if we think this is a temporary item created by Camera. A 685 // temporary item is an image or a video whose data is still being 686 // processed, but an incomplete entry is created first in MediaProvider, so 687 // we can display them (in grey tile) even if they are not saved to disk 688 // yet. When the image or video data is actually saved, we will get 689 // notification from MediaProvider, reload data, and show the actual image 690 // or video data. 691 private boolean isTemporaryItem(MediaItem mediaItem) { 692 // Must have camera to create a temporary item. 693 if (mCameraIndex < 0) return false; 694 // Must be an item in camera roll. 695 if (!(mediaItem instanceof LocalMediaItem)) return false; 696 LocalMediaItem item = (LocalMediaItem) mediaItem; 697 if (item.getBucketId() != MediaSetUtils.CAMERA_BUCKET_ID) return false; 698 // Must have no size, but must have width and height information 699 if (item.getSize() != 0) return false; 700 if (item.getWidth() == 0) return false; 701 if (item.getHeight() == 0) return false; 702 // Must be created in the last 10 seconds. 703 if (item.getDateInMs() - System.currentTimeMillis() > 10000) return false; 704 return true; 705 } 706 707 // Create a default ScreenNail when a ScreenNail is needed, but we don't yet 708 // have one available (because the image data is still being saved, or the 709 // Bitmap is still being loaded. 710 private ScreenNail newPlaceholderScreenNail(MediaItem item) { 711 int width = item.getWidth(); 712 int height = item.getHeight(); 713 return new BitmapScreenNail(width, height); 714 } 715 716 // Returns the task if we started the task or the task is already started. 717 private Future<?> startTaskIfNeeded(int index, int which) { 718 if (index < mActiveStart || index >= mActiveEnd) return null; 719 720 ImageEntry entry = mImageCache.get(getPath(index)); 721 if (entry == null) return null; 722 MediaItem item = mData[index % DATA_CACHE_SIZE]; 723 Utils.assertTrue(item != null); 724 long version = item.getDataVersion(); 725 726 if (which == BIT_SCREEN_NAIL && entry.screenNailTask != null 727 && entry.requestedScreenNail == version) { 728 return entry.screenNailTask; 729 } else if (which == BIT_FULL_IMAGE && entry.fullImageTask != null 730 && entry.requestedFullImage == version) { 731 return entry.fullImageTask; 732 } 733 734 if (which == BIT_SCREEN_NAIL && entry.requestedScreenNail != version) { 735 entry.requestedScreenNail = version; 736 entry.screenNailTask = mThreadPool.submit( 737 new ScreenNailJob(item), 738 new ScreenNailListener(item)); 739 // request screen nail 740 return entry.screenNailTask; 741 } 742 if (which == BIT_FULL_IMAGE && entry.requestedFullImage != version 743 && (item.getSupportedOperations() 744 & MediaItem.SUPPORT_FULL_IMAGE) != 0) { 745 entry.requestedFullImage = version; 746 entry.fullImageTask = mThreadPool.submit( 747 new FullImageJob(item), 748 new FullImageListener(item)); 749 // request full image 750 return entry.fullImageTask; 751 } 752 return null; 753 } 754 755 private void updateImageCache() { 756 HashSet<Path> toBeRemoved = new HashSet<Path>(mImageCache.keySet()); 757 for (int i = mActiveStart; i < mActiveEnd; ++i) { 758 MediaItem item = mData[i % DATA_CACHE_SIZE]; 759 if (item == null) continue; 760 Path path = item.getPath(); 761 ImageEntry entry = mImageCache.get(path); 762 toBeRemoved.remove(path); 763 if (entry != null) { 764 if (Math.abs(i - mCurrentIndex) > 1) { 765 if (entry.fullImageTask != null) { 766 entry.fullImageTask.cancel(); 767 entry.fullImageTask = null; 768 } 769 entry.fullImage = null; 770 entry.requestedFullImage = MediaObject.INVALID_DATA_VERSION; 771 } 772 if (entry.requestedScreenNail != item.getDataVersion()) { 773 // This ScreenNail is outdated, we want to update it if it's 774 // still a placeholder. 775 if (entry.screenNail instanceof BitmapScreenNail) { 776 BitmapScreenNail s = (BitmapScreenNail) entry.screenNail; 777 s.updatePlaceholderSize( 778 item.getWidth(), item.getHeight()); 779 } 780 } 781 } else { 782 entry = new ImageEntry(); 783 mImageCache.put(path, entry); 784 } 785 } 786 787 // Clear the data and requests for ImageEntries outside the new window. 788 for (Path path : toBeRemoved) { 789 ImageEntry entry = mImageCache.remove(path); 790 if (entry.fullImageTask != null) entry.fullImageTask.cancel(); 791 if (entry.screenNailTask != null) entry.screenNailTask.cancel(); 792 if (entry.screenNail != null) entry.screenNail.recycle(); 793 } 794 } 795 796 private class FullImageListener 797 implements Runnable, FutureListener<BitmapRegionDecoder> { 798 private final Path mPath; 799 private Future<BitmapRegionDecoder> mFuture; 800 801 public FullImageListener(MediaItem item) { 802 mPath = item.getPath(); 803 } 804 805 @Override 806 public void onFutureDone(Future<BitmapRegionDecoder> future) { 807 mFuture = future; 808 mMainHandler.sendMessage( 809 mMainHandler.obtainMessage(MSG_RUN_OBJECT, this)); 810 } 811 812 @Override 813 public void run() { 814 updateFullImage(mPath, mFuture); 815 } 816 } 817 818 private class ScreenNailListener 819 implements Runnable, FutureListener<ScreenNail> { 820 private final Path mPath; 821 private Future<ScreenNail> mFuture; 822 823 public ScreenNailListener(MediaItem item) { 824 mPath = item.getPath(); 825 } 826 827 @Override 828 public void onFutureDone(Future<ScreenNail> future) { 829 mFuture = future; 830 mMainHandler.sendMessage( 831 mMainHandler.obtainMessage(MSG_RUN_OBJECT, this)); 832 } 833 834 @Override 835 public void run() { 836 updateScreenNail(mPath, mFuture); 837 } 838 } 839 840 private static class ImageEntry { 841 public BitmapRegionDecoder fullImage; 842 public ScreenNail screenNail; 843 public Future<ScreenNail> screenNailTask; 844 public Future<BitmapRegionDecoder> fullImageTask; 845 public long requestedScreenNail = MediaObject.INVALID_DATA_VERSION; 846 public long requestedFullImage = MediaObject.INVALID_DATA_VERSION; 847 public boolean failToLoad = false; 848 } 849 850 private class SourceListener implements ContentListener { 851 public void onContentDirty() { 852 if (mReloadTask != null) mReloadTask.notifyDirty(); 853 } 854 } 855 856 private <T> T executeAndWait(Callable<T> callable) { 857 FutureTask<T> task = new FutureTask<T>(callable); 858 mMainHandler.sendMessage( 859 mMainHandler.obtainMessage(MSG_RUN_OBJECT, task)); 860 try { 861 return task.get(); 862 } catch (InterruptedException e) { 863 return null; 864 } catch (ExecutionException e) { 865 throw new RuntimeException(e); 866 } 867 } 868 869 private static class UpdateInfo { 870 public long version; 871 public boolean reloadContent; 872 public Path target; 873 public int indexHint; 874 public int contentStart; 875 public int contentEnd; 876 877 public int size; 878 public ArrayList<MediaItem> items; 879 } 880 881 private class GetUpdateInfo implements Callable<UpdateInfo> { 882 883 private boolean needContentReload() { 884 for (int i = mContentStart, n = mContentEnd; i < n; ++i) { 885 if (mData[i % DATA_CACHE_SIZE] == null) return true; 886 } 887 MediaItem current = mData[mCurrentIndex % DATA_CACHE_SIZE]; 888 return current == null || current.getPath() != mItemPath; 889 } 890 891 @Override 892 public UpdateInfo call() throws Exception { 893 // TODO: Try to load some data in first update 894 UpdateInfo info = new UpdateInfo(); 895 info.version = mSourceVersion; 896 info.reloadContent = needContentReload(); 897 info.target = mItemPath; 898 info.indexHint = mCurrentIndex; 899 info.contentStart = mContentStart; 900 info.contentEnd = mContentEnd; 901 info.size = mSize; 902 return info; 903 } 904 } 905 906 private class UpdateContent implements Callable<Void> { 907 UpdateInfo mUpdateInfo; 908 909 public UpdateContent(UpdateInfo updateInfo) { 910 mUpdateInfo = updateInfo; 911 } 912 913 @Override 914 public Void call() throws Exception { 915 UpdateInfo info = mUpdateInfo; 916 mSourceVersion = info.version; 917 918 if (info.size != mSize) { 919 mSize = info.size; 920 if (mContentEnd > mSize) mContentEnd = mSize; 921 if (mActiveEnd > mSize) mActiveEnd = mSize; 922 } 923 924 mCurrentIndex = info.indexHint; 925 updateSlidingWindow(); 926 927 if (info.items != null) { 928 int start = Math.max(info.contentStart, mContentStart); 929 int end = Math.min(info.contentStart + info.items.size(), mContentEnd); 930 int dataIndex = start % DATA_CACHE_SIZE; 931 for (int i = start; i < end; ++i) { 932 mData[dataIndex] = info.items.get(i - info.contentStart); 933 if (++dataIndex == DATA_CACHE_SIZE) dataIndex = 0; 934 } 935 } 936 937 // update mItemPath 938 MediaItem current = mData[mCurrentIndex % DATA_CACHE_SIZE]; 939 mItemPath = current == null ? null : current.getPath(); 940 941 updateImageCache(); 942 updateTileProvider(); 943 updateImageRequests(); 944 fireDataChange(); 945 return null; 946 } 947 } 948 949 private class ReloadTask extends Thread { 950 private volatile boolean mActive = true; 951 private volatile boolean mDirty = true; 952 953 private boolean mIsLoading = false; 954 955 private void updateLoading(boolean loading) { 956 if (mIsLoading == loading) return; 957 mIsLoading = loading; 958 mMainHandler.sendEmptyMessage(loading ? MSG_LOAD_START : MSG_LOAD_FINISH); 959 } 960 961 @Override 962 public void run() { 963 while (mActive) { 964 synchronized (this) { 965 if (!mDirty && mActive) { 966 updateLoading(false); 967 Utils.waitWithoutInterrupt(this); 968 continue; 969 } 970 } 971 mDirty = false; 972 UpdateInfo info = executeAndWait(new GetUpdateInfo()); 973 synchronized (DataManager.LOCK) { 974 updateLoading(true); 975 long version = mSource.reload(); 976 if (info.version != version) { 977 info.reloadContent = true; 978 info.size = mSource.getMediaItemCount(); 979 } 980 if (!info.reloadContent) continue; 981 info.items = mSource.getMediaItem( 982 info.contentStart, info.contentEnd); 983 984 int index = MediaSet.INDEX_NOT_FOUND; 985 986 // First try to focus on the given hint path if there is one. 987 if (mFocusHintPath != null) { 988 index = findIndexOfPathInCache(info, mFocusHintPath); 989 mFocusHintPath = null; 990 } 991 992 // Otherwise try to see if the currently focused item can be found. 993 if (index == MediaSet.INDEX_NOT_FOUND) { 994 MediaItem item = findCurrentMediaItem(info); 995 if (item != null && item.getPath() == info.target) { 996 index = info.indexHint; 997 } else { 998 index = findIndexOfTarget(info); 999 } 1000 } 1001 1002 // The image has been deleted. Focus on the next image (keep 1003 // mCurrentIndex unchanged) or the previous image (decrease 1004 // mCurrentIndex by 1). In page mode we want to see the next 1005 // image, so we focus on the next one. In film mode we want the 1006 // later images to shift left to fill the empty space, so we 1007 // focus on the previous image (so it will not move). In any 1008 // case the index needs to be limited to [0, mSize). 1009 if (index == MediaSet.INDEX_NOT_FOUND) { 1010 index = info.indexHint; 1011 if (mFocusHintDirection == FOCUS_HINT_PREVIOUS 1012 && index > 0) { 1013 index--; 1014 } 1015 } 1016 1017 // Don't change index if mSize == 0 1018 if (mSize > 0) { 1019 if (index >= mSize) index = mSize - 1; 1020 info.indexHint = index; 1021 } 1022 } 1023 1024 executeAndWait(new UpdateContent(info)); 1025 } 1026 } 1027 1028 public synchronized void notifyDirty() { 1029 mDirty = true; 1030 notifyAll(); 1031 } 1032 1033 public synchronized void terminate() { 1034 mActive = false; 1035 notifyAll(); 1036 } 1037 1038 private MediaItem findCurrentMediaItem(UpdateInfo info) { 1039 ArrayList<MediaItem> items = info.items; 1040 int index = info.indexHint - info.contentStart; 1041 return index < 0 || index >= items.size() ? null : items.get(index); 1042 } 1043 1044 private int findIndexOfTarget(UpdateInfo info) { 1045 if (info.target == null) return info.indexHint; 1046 ArrayList<MediaItem> items = info.items; 1047 1048 // First, try to find the item in the data just loaded 1049 if (items != null) { 1050 int i = findIndexOfPathInCache(info, info.target); 1051 if (i != MediaSet.INDEX_NOT_FOUND) return i; 1052 } 1053 1054 // Not found, find it in mSource. 1055 return mSource.getIndexOfItem(info.target, info.indexHint); 1056 } 1057 1058 private int findIndexOfPathInCache(UpdateInfo info, Path path) { 1059 ArrayList<MediaItem> items = info.items; 1060 for (int i = 0, n = items.size(); i < n; ++i) { 1061 if (items.get(i).getPath() == path) { 1062 return i + info.contentStart; 1063 } 1064 } 1065 return MediaSet.INDEX_NOT_FOUND; 1066 } 1067 } 1068} 1069