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