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