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