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