PhotoDataAdapter.java revision 616a70fdb4473d2fbd7b70772a3a82b908aeae1e
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.DataManager;
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.ui.BitmapScreenNail;
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.util.Future;
38import com.android.gallery3d.util.FutureListener;
39import com.android.gallery3d.util.ThreadPool;
40import com.android.gallery3d.util.ThreadPool.Job;
41import com.android.gallery3d.util.ThreadPool.JobContext;
42
43import java.util.ArrayList;
44import java.util.Arrays;
45import java.util.HashMap;
46import java.util.HashSet;
47import java.util.concurrent.Callable;
48import java.util.concurrent.ExecutionException;
49import java.util.concurrent.FutureTask;
50
51public class PhotoDataAdapter implements PhotoPage.Model {
52    @SuppressWarnings("unused")
53    private static final String TAG = "PhotoDataAdapter";
54
55    private static final int MSG_LOAD_START = 1;
56    private static final int MSG_LOAD_FINISH = 2;
57    private static final int MSG_RUN_OBJECT = 3;
58    private static final int MSG_UPDATE_IMAGE_REQUESTS = 4;
59
60    private static final int MIN_LOAD_COUNT = 8;
61    private static final int DATA_CACHE_SIZE = 32;
62    private static final int SCREEN_NAIL_MAX = PhotoView.SCREEN_NAIL_MAX;
63    private static final int IMAGE_CACHE_SIZE = 2 * SCREEN_NAIL_MAX + 1;
64
65    private static final int BIT_SCREEN_NAIL = 1;
66    private static final int BIT_FULL_IMAGE = 2;
67
68    // sImageFetchSeq is the fetching sequence for images.
69    // We want to fetch the current screennail first (offset = 0), the next
70    // screennail (offset = +1), then the previous screennail (offset = -1) etc.
71    // After all the screennail are fetched, we fetch the full images (only some
72    // of them because of we don't want to use too much memory).
73    private static ImageFetch[] sImageFetchSeq;
74
75    private static class ImageFetch {
76        int indexOffset;
77        int imageBit;
78        public ImageFetch(int offset, int bit) {
79            indexOffset = offset;
80            imageBit = bit;
81        }
82    }
83
84    static {
85        int k = 0;
86        sImageFetchSeq = new ImageFetch[1 + (IMAGE_CACHE_SIZE - 1) * 2 + 3];
87        sImageFetchSeq[k++] = new ImageFetch(0, BIT_SCREEN_NAIL);
88
89        for (int i = 1; i < IMAGE_CACHE_SIZE; ++i) {
90            sImageFetchSeq[k++] = new ImageFetch(i, BIT_SCREEN_NAIL);
91            sImageFetchSeq[k++] = new ImageFetch(-i, BIT_SCREEN_NAIL);
92        }
93
94        sImageFetchSeq[k++] = new ImageFetch(0, BIT_FULL_IMAGE);
95        sImageFetchSeq[k++] = new ImageFetch(1, BIT_FULL_IMAGE);
96        sImageFetchSeq[k++] = new ImageFetch(-1, BIT_FULL_IMAGE);
97    }
98
99    private final TileImageViewAdapter mTileProvider = new TileImageViewAdapter();
100
101    // PhotoDataAdapter caches MediaItems (data) and ImageEntries (image).
102    //
103    // The MediaItems are stored in the mData array, which has DATA_CACHE_SIZE
104    // entries. The valid index range are [mContentStart, mContentEnd). We keep
105    // mContentEnd - mContentStart <= DATA_CACHE_SIZE, so we can use
106    // (i % DATA_CACHE_SIZE) as index to the array.
107    //
108    // The valid MediaItem window size (mContentEnd - mContentStart) may be
109    // smaller than DATA_CACHE_SIZE because we only update the window and reload
110    // the MediaItems when there are significant changes to the window position
111    // (>= MIN_LOAD_COUNT).
112    private final MediaItem mData[] = new MediaItem[DATA_CACHE_SIZE];
113    private int mContentStart = 0;
114    private int mContentEnd = 0;
115
116    /*
117     * The ImageCache is a version-to-ImageEntry map. It only holds
118     * the ImageEntries in the range of [mActiveStart, mActiveEnd).
119     * We also keep mActiveEnd - mActiveStart <= IMAGE_CACHE_SIZE.
120     * Besides, the [mActiveStart, mActiveEnd) range must be contained
121     * within the[mContentStart, mContentEnd) range.
122     */
123    private HashMap<Long, ImageEntry> mImageCache = new HashMap<Long, ImageEntry>();
124    private int mActiveStart = 0;
125    private int mActiveEnd = 0;
126
127    // mCurrentIndex is the "center" image the user is viewing. The change of
128    // mCurrentIndex triggers the data loading and image loading.
129    private int mCurrentIndex;
130
131    // mChanges keeps the version number (of MediaItem) about the images. If any
132    // of the version number changes, we notify the view. This is used after a
133    // database reload or mCurrentIndex changes.
134    private final long mChanges[] = new long[IMAGE_CACHE_SIZE];
135    // mPaths keeps the corresponding Path (of MediaItem) for the images. This
136    // is used to determine the item movement.
137    private final Path mPaths[] = new Path[IMAGE_CACHE_SIZE];
138
139    private final Handler mMainHandler;
140    private final ThreadPool mThreadPool;
141
142    private final PhotoView mPhotoView;
143    private final MediaSet mSource;
144    private ReloadTask mReloadTask;
145
146    private long mSourceVersion = MediaObject.INVALID_DATA_VERSION;
147    private int mSize = 0;
148    private Path mItemPath;
149    private int mCameraIndex;
150    private boolean mIsActive;
151    private boolean mNeedFullImage;
152
153    public interface DataListener extends LoadingListener {
154        public void onPhotoChanged(int index, Path item);
155    }
156
157    private DataListener mDataListener;
158
159    private final SourceListener mSourceListener = new SourceListener();
160
161    // The path of the current viewing item will be stored in mItemPath.
162    // If mItemPath is not null, mCurrentIndex is only a hint for where we
163    // can find the item. If mItemPath is null, then we use the mCurrentIndex to
164    // find the image being viewed. cameraIndex is the index of the camera
165    // preview. If cameraIndex < 0, there is no camera preview.
166    public PhotoDataAdapter(GalleryActivity activity, PhotoView view,
167            MediaSet mediaSet, Path itemPath, int indexHint, int cameraIndex) {
168        mSource = Utils.checkNotNull(mediaSet);
169        mPhotoView = Utils.checkNotNull(view);
170        mItemPath = Utils.checkNotNull(itemPath);
171        mCurrentIndex = indexHint;
172        mCameraIndex = cameraIndex;
173        mThreadPool = activity.getThreadPool();
174        mNeedFullImage = true;
175
176        Arrays.fill(mChanges, MediaObject.INVALID_DATA_VERSION);
177
178        mMainHandler = new SynchronizedHandler(activity.getGLRoot()) {
179            @SuppressWarnings("unchecked")
180            @Override
181            public void handleMessage(Message message) {
182                switch (message.what) {
183                    case MSG_RUN_OBJECT:
184                        ((Runnable) message.obj).run();
185                        return;
186                    case MSG_LOAD_START: {
187                        if (mDataListener != null) mDataListener.onLoadingStarted();
188                        return;
189                    }
190                    case MSG_LOAD_FINISH: {
191                        if (mDataListener != null) mDataListener.onLoadingFinished();
192                        return;
193                    }
194                    case MSG_UPDATE_IMAGE_REQUESTS: {
195                        updateImageRequests();
196                        return;
197                    }
198                    default: throw new AssertionError();
199                }
200            }
201        };
202
203        updateSlidingWindow();
204    }
205
206    private MediaItem getItemInternal(int index) {
207        if (index < 0 || index >= mSize) return null;
208        if (index >= mContentStart && index < mContentEnd) {
209            return mData[index % DATA_CACHE_SIZE];
210        }
211        return null;
212    }
213
214    private long getVersion(int index) {
215        MediaItem item = getItemInternal(index);
216        if (item == null) return MediaObject.INVALID_DATA_VERSION;
217        return item.getDataVersion();
218    }
219
220    private Path getPath(int index) {
221        MediaItem item = getItemInternal(index);
222        if (item == null) return null;
223        return item.getPath();
224    }
225
226    private void fireDataChange() {
227        // First check if data actually changed.
228        boolean changed = false;
229        for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; ++i) {
230            long newVersion = getVersion(mCurrentIndex + i);
231            if (mChanges[i + SCREEN_NAIL_MAX] != newVersion) {
232                mChanges[i + SCREEN_NAIL_MAX] = newVersion;
233                changed = true;
234            }
235        }
236
237        if (!changed) return;
238
239        // Now calculate the fromIndex array. fromIndex represents the item
240        // movement. It records the index where the picture come from. The
241        // special value Integer.MAX_VALUE means it's a new picture.
242        final int N = IMAGE_CACHE_SIZE;
243        int fromIndex[] = new int[N];
244
245        // Remember the old path array.
246        Path oldPaths[] = new Path[N];
247        System.arraycopy(mPaths, 0, oldPaths, 0, N);
248
249        // Update the mPaths array.
250        for (int i = 0; i < N; ++i) {
251            mPaths[i] = getPath(mCurrentIndex + i - SCREEN_NAIL_MAX);
252        }
253
254        // Calculate the fromIndex array.
255        for (int i = 0; i < N; i++) {
256            Path p = mPaths[i];
257            if (p == null) {
258                fromIndex[i] = Integer.MAX_VALUE;
259                continue;
260            }
261
262            // Try to find the same path in the old array
263            int j;
264            for (j = 0; j < N; j++) {
265                if (oldPaths[j] == p) {
266                    break;
267                }
268            }
269            fromIndex[i] = (j < N) ? j - SCREEN_NAIL_MAX : Integer.MAX_VALUE;
270        }
271
272        mPhotoView.notifyDataChange(fromIndex, -mCurrentIndex,
273                mSize - 1 - mCurrentIndex);
274    }
275
276    public void setDataListener(DataListener listener) {
277        mDataListener = listener;
278    }
279
280    private void updateScreenNail(long version, Future<ScreenNail> future) {
281        ImageEntry entry = mImageCache.get(version);
282        ScreenNail screenNail = future.get();
283
284        if (entry == null || entry.screenNailTask != future) {
285            if (screenNail != null) screenNail.recycle();
286            return;
287        }
288
289        entry.screenNailTask = null;
290        Utils.assertTrue(entry.screenNail == null);
291        entry.screenNail = screenNail;
292
293        if (screenNail == null) {
294            entry.failToLoad = true;
295        }
296
297        for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; ++i) {
298            if (version == getVersion(mCurrentIndex + i)) {
299                if (i == 0) updateTileProvider(entry);
300                mPhotoView.notifyImageChange(i);
301                break;
302            }
303        }
304        updateImageRequests();
305    }
306
307    private void updateFullImage(long version, Future<BitmapRegionDecoder> future) {
308        ImageEntry entry = mImageCache.get(version);
309        if (entry == null || entry.fullImageTask != future) {
310            BitmapRegionDecoder fullImage = future.get();
311            if (fullImage != null) fullImage.recycle();
312            return;
313        }
314
315        entry.fullImageTask = null;
316        entry.fullImage = future.get();
317        if (entry.fullImage != null) {
318            if (version == getVersion(mCurrentIndex)) {
319                updateTileProvider(entry);
320                mPhotoView.notifyImageChange(0);
321            }
322        }
323        updateImageRequests();
324    }
325
326    public void resume() {
327        mIsActive = true;
328        mSource.addContentListener(mSourceListener);
329        updateImageCache();
330        updateImageRequests();
331
332        mReloadTask = new ReloadTask();
333        mReloadTask.start();
334
335        fireDataChange();
336    }
337
338    public void pause() {
339        mIsActive = false;
340
341        mReloadTask.terminate();
342        mReloadTask = null;
343
344        mSource.removeContentListener(mSourceListener);
345
346        for (ImageEntry entry : mImageCache.values()) {
347            if (entry.fullImageTask != null) entry.fullImageTask.cancel();
348            if (entry.screenNailTask != null) entry.screenNailTask.cancel();
349            if (entry.screenNail != null) entry.screenNail.recycle();
350        }
351        mImageCache.clear();
352        mTileProvider.clear();
353    }
354
355    private ScreenNail getImage(int index) {
356        if (index < 0 || index >= mSize || !mIsActive) return null;
357        Utils.assertTrue(index >= mActiveStart && index < mActiveEnd);
358
359        ImageEntry entry = mImageCache.get(getVersion(index));
360        return entry == null ? null : entry.screenNail;
361    }
362
363    private MediaItem getItem(int index) {
364        if (index < 0 || index >= mSize || !mIsActive) return null;
365        Utils.assertTrue(index >= mActiveStart && index < mActiveEnd);
366
367        if (index >= mContentStart && index < mContentEnd) {
368            return mData[index % DATA_CACHE_SIZE];
369        }
370        return null;
371    }
372
373    private void updateCurrentIndex(int index) {
374        if (mCurrentIndex == index) return;
375        mCurrentIndex = index;
376        updateSlidingWindow();
377
378        MediaItem item = mData[index % DATA_CACHE_SIZE];
379        mItemPath = item == null ? null : item.getPath();
380
381        updateImageCache();
382        updateImageRequests();
383        updateTileProvider();
384
385        if (mDataListener != null) {
386            mDataListener.onPhotoChanged(index, mItemPath);
387        }
388
389        fireDataChange();
390    }
391
392    @Override
393    public void moveTo(int index) {
394        updateCurrentIndex(index);
395    }
396
397    @Override
398    public ScreenNail getScreenNail(int offset) {
399        return getImage(mCurrentIndex + offset);
400    }
401
402    @Override
403    public void getImageSize(int offset, PhotoView.Size size) {
404        MediaItem item = getItem(mCurrentIndex + offset);
405        if (item == null) {
406            size.width = 0;
407            size.height = 0;
408        } else {
409            size.width = item.getWidth();
410            size.height = item.getHeight();
411        }
412    }
413
414    @Override
415    public int getImageRotation(int offset) {
416        MediaItem item = getItem(mCurrentIndex + offset);
417        return (item == null) ? 0 : item.getFullImageRotation();
418    }
419
420    @Override
421    public void setNeedFullImage(boolean enabled) {
422        mNeedFullImage = enabled;
423        mMainHandler.sendEmptyMessage(MSG_UPDATE_IMAGE_REQUESTS);
424    }
425
426    @Override
427    public boolean isCamera(int offset) {
428        return mCurrentIndex + offset == mCameraIndex;
429    }
430
431    @Override
432    public boolean isVideo(int offset) {
433        MediaItem item = getItem(mCurrentIndex + offset);
434        return (item == null)
435                ? false
436                : item.getMediaType() == MediaItem.MEDIA_TYPE_VIDEO;
437    }
438
439    public ScreenNail getScreenNail() {
440        return mTileProvider.getScreenNail();
441    }
442
443    public int getImageHeight() {
444        return mTileProvider.getImageHeight();
445    }
446
447    public int getImageWidth() {
448        return mTileProvider.getImageWidth();
449    }
450
451    public int getLevelCount() {
452        return mTileProvider.getLevelCount();
453    }
454
455    public Bitmap getTile(int level, int x, int y, int tileSize,
456            int borderSize) {
457        return mTileProvider.getTile(level, x, y, tileSize, borderSize);
458    }
459
460    public boolean isFailedToLoad() {
461        return mTileProvider.isFailedToLoad();
462    }
463
464    public boolean isEmpty() {
465        return mSize == 0;
466    }
467
468    public int getCurrentIndex() {
469        return mCurrentIndex;
470    }
471
472    public MediaItem getMediaItem(int offset) {
473        int index = mCurrentIndex + offset;
474        if (index >= mContentStart && index < mContentEnd) {
475            return mData[index % DATA_CACHE_SIZE];
476        }
477        return null;
478    }
479
480    public void setCurrentPhoto(Path path, int indexHint) {
481        if (mItemPath == path) return;
482        mItemPath = path;
483        mCurrentIndex = indexHint;
484        updateSlidingWindow();
485        updateImageCache();
486        fireDataChange();
487
488        // We need to reload content if the path doesn't match.
489        MediaItem item = getMediaItem(0);
490        if (item != null && item.getPath() != path) {
491            if (mReloadTask != null) mReloadTask.notifyDirty();
492        }
493    }
494
495    private void updateTileProvider() {
496        ImageEntry entry = mImageCache.get(getVersion(mCurrentIndex));
497        if (entry == null) { // in loading
498            mTileProvider.clear();
499        } else {
500            updateTileProvider(entry);
501        }
502    }
503
504    private void updateTileProvider(ImageEntry entry) {
505        ScreenNail screenNail = entry.screenNail;
506        BitmapRegionDecoder fullImage = entry.fullImage;
507        if (screenNail != null) {
508            if (fullImage != null) {
509                mTileProvider.setScreenNail(screenNail,
510                        fullImage.getWidth(), fullImage.getHeight());
511                mTileProvider.setRegionDecoder(fullImage);
512            } else {
513                int width = screenNail.getWidth();
514                int height = screenNail.getHeight();
515                mTileProvider.setScreenNail(screenNail, width, height);
516            }
517        } else {
518            mTileProvider.clear();
519            if (entry.failToLoad) mTileProvider.setFailedToLoad();
520        }
521    }
522
523    private void updateSlidingWindow() {
524        // 1. Update the image window
525        int start = Utils.clamp(mCurrentIndex - IMAGE_CACHE_SIZE / 2,
526                0, Math.max(0, mSize - IMAGE_CACHE_SIZE));
527        int end = Math.min(mSize, start + IMAGE_CACHE_SIZE);
528
529        if (mActiveStart == start && mActiveEnd == end) return;
530
531        mActiveStart = start;
532        mActiveEnd = end;
533
534        // 2. Update the data window
535        start = Utils.clamp(mCurrentIndex - DATA_CACHE_SIZE / 2,
536                0, Math.max(0, mSize - DATA_CACHE_SIZE));
537        end = Math.min(mSize, start + DATA_CACHE_SIZE);
538        if (mContentStart > mActiveStart || mContentEnd < mActiveEnd
539                || Math.abs(start - mContentStart) > MIN_LOAD_COUNT) {
540            for (int i = mContentStart; i < mContentEnd; ++i) {
541                if (i < start || i >= end) {
542                    mData[i % DATA_CACHE_SIZE] = null;
543                }
544            }
545            mContentStart = start;
546            mContentEnd = end;
547            if (mReloadTask != null) mReloadTask.notifyDirty();
548        }
549    }
550
551    private void updateImageRequests() {
552        if (!mIsActive) return;
553
554        int currentIndex = mCurrentIndex;
555        MediaItem item = mData[currentIndex % DATA_CACHE_SIZE];
556        if (item == null || item.getPath() != mItemPath) {
557            // current item mismatch - don't request image
558            return;
559        }
560
561        // 1. Find the most wanted request and start it (if not already started).
562        Future<?> task = null;
563        for (int i = 0; i < sImageFetchSeq.length; i++) {
564            int offset = sImageFetchSeq[i].indexOffset;
565            int bit = sImageFetchSeq[i].imageBit;
566            if (bit == BIT_FULL_IMAGE && !mNeedFullImage) continue;
567            task = startTaskIfNeeded(currentIndex + offset, bit);
568            if (task != null) break;
569        }
570
571        // 2. Cancel everything else.
572        for (ImageEntry entry : mImageCache.values()) {
573            if (entry.screenNailTask != null && entry.screenNailTask != task) {
574                entry.screenNailTask.cancel();
575                entry.screenNailTask = null;
576                entry.requestedBits &= ~BIT_SCREEN_NAIL;
577            }
578            if (entry.fullImageTask != null && entry.fullImageTask != task) {
579                entry.fullImageTask.cancel();
580                entry.fullImageTask = null;
581                entry.requestedBits &= ~BIT_FULL_IMAGE;
582            }
583        }
584    }
585
586    private static class ScreenNailJob implements Job<ScreenNail> {
587        private MediaItem mItem;
588
589        public ScreenNailJob(MediaItem item) {
590            mItem = item;
591        }
592
593        @Override
594        public ScreenNail run(JobContext jc) {
595            // We try to get a ScreenNail first, if it fails, we fallback to get
596            // a Bitmap and then wrap it in a BitmapScreenNail instead.
597            ScreenNail s = mItem.getScreenNail();
598            if (s != null) return s;
599
600            Bitmap bitmap = mItem.requestImage(MediaItem.TYPE_THUMBNAIL).run(jc);
601            if (jc.isCancelled()) return null;
602            if (bitmap != null) {
603                bitmap = BitmapUtils.rotateBitmap(bitmap,
604                    mItem.getRotation() - mItem.getFullImageRotation(), true);
605            }
606            return bitmap == null ? null : new BitmapScreenNail(bitmap);
607        }
608    }
609
610    // Returns the task if we started the task or the task is already started.
611    private Future<?> startTaskIfNeeded(int index, int which) {
612        if (index < mActiveStart || index >= mActiveEnd) return null;
613
614        ImageEntry entry = mImageCache.get(getVersion(index));
615        if (entry == null) return null;
616
617        if (which == BIT_SCREEN_NAIL && entry.screenNailTask != null) {
618            return entry.screenNailTask;
619        } else if (which == BIT_FULL_IMAGE && entry.fullImageTask != null) {
620            return entry.fullImageTask;
621        }
622
623        MediaItem item = mData[index % DATA_CACHE_SIZE];
624        Utils.assertTrue(item != null);
625
626        if (which == BIT_SCREEN_NAIL
627                && (entry.requestedBits & BIT_SCREEN_NAIL) == 0) {
628            entry.requestedBits |= BIT_SCREEN_NAIL;
629            entry.screenNailTask = mThreadPool.submit(
630                    new ScreenNailJob(item),
631                    new ScreenNailListener(item.getDataVersion()));
632            // request screen nail
633            return entry.screenNailTask;
634        }
635        if (which == BIT_FULL_IMAGE
636                && (entry.requestedBits & BIT_FULL_IMAGE) == 0
637                && (item.getSupportedOperations()
638                & MediaItem.SUPPORT_FULL_IMAGE) != 0) {
639            entry.requestedBits |= BIT_FULL_IMAGE;
640            entry.fullImageTask = mThreadPool.submit(
641                    item.requestLargeImage(),
642                    new FullImageListener(item.getDataVersion()));
643            // request full image
644            return entry.fullImageTask;
645        }
646        return null;
647    }
648
649    private void updateImageCache() {
650        HashSet<Long> toBeRemoved = new HashSet<Long>(mImageCache.keySet());
651        for (int i = mActiveStart; i < mActiveEnd; ++i) {
652            MediaItem item = mData[i % DATA_CACHE_SIZE];
653            long version = item == null
654                    ? MediaObject.INVALID_DATA_VERSION
655                    : item.getDataVersion();
656            if (version == MediaObject.INVALID_DATA_VERSION) continue;
657            ImageEntry entry = mImageCache.get(version);
658            toBeRemoved.remove(version);
659            if (entry != null) {
660                if (Math.abs(i - mCurrentIndex) > 1) {
661                    if (entry.fullImageTask != null) {
662                        entry.fullImageTask.cancel();
663                        entry.fullImageTask = null;
664                    }
665                    entry.fullImage = null;
666                    entry.requestedBits &= ~BIT_FULL_IMAGE;
667                }
668            } else {
669                entry = new ImageEntry();
670                mImageCache.put(version, entry);
671            }
672        }
673
674        // Clear the data and requests for ImageEntries outside the new window.
675        for (Long version : toBeRemoved) {
676            ImageEntry entry = mImageCache.remove(version);
677            if (entry.fullImageTask != null) entry.fullImageTask.cancel();
678            if (entry.screenNailTask != null) entry.screenNailTask.cancel();
679            if (entry.screenNail != null) entry.screenNail.recycle();
680        }
681    }
682
683    private class FullImageListener
684            implements Runnable, FutureListener<BitmapRegionDecoder> {
685        private final long mVersion;
686        private Future<BitmapRegionDecoder> mFuture;
687
688        public FullImageListener(long version) {
689            mVersion = version;
690        }
691
692        @Override
693        public void onFutureDone(Future<BitmapRegionDecoder> future) {
694            mFuture = future;
695            mMainHandler.sendMessage(
696                    mMainHandler.obtainMessage(MSG_RUN_OBJECT, this));
697        }
698
699        @Override
700        public void run() {
701            updateFullImage(mVersion, mFuture);
702        }
703    }
704
705    private class ScreenNailListener
706            implements Runnable, FutureListener<ScreenNail> {
707        private final long mVersion;
708        private Future<ScreenNail> mFuture;
709
710        public ScreenNailListener(long version) {
711            mVersion = version;
712        }
713
714        @Override
715        public void onFutureDone(Future<ScreenNail> future) {
716            mFuture = future;
717            mMainHandler.sendMessage(
718                    mMainHandler.obtainMessage(MSG_RUN_OBJECT, this));
719        }
720
721        @Override
722        public void run() {
723            updateScreenNail(mVersion, mFuture);
724        }
725    }
726
727    private static class ImageEntry {
728        public int requestedBits = 0;
729        public BitmapRegionDecoder fullImage;
730        public ScreenNail screenNail;
731        public Future<ScreenNail> screenNailTask;
732        public Future<BitmapRegionDecoder> fullImageTask;
733        public boolean failToLoad = false;
734    }
735
736    private class SourceListener implements ContentListener {
737        public void onContentDirty() {
738            if (mReloadTask != null) mReloadTask.notifyDirty();
739        }
740    }
741
742    private <T> T executeAndWait(Callable<T> callable) {
743        FutureTask<T> task = new FutureTask<T>(callable);
744        mMainHandler.sendMessage(
745                mMainHandler.obtainMessage(MSG_RUN_OBJECT, task));
746        try {
747            return task.get();
748        } catch (InterruptedException e) {
749            return null;
750        } catch (ExecutionException e) {
751            throw new RuntimeException(e);
752        }
753    }
754
755    private static class UpdateInfo {
756        public long version;
757        public boolean reloadContent;
758        public Path target;
759        public int indexHint;
760        public int contentStart;
761        public int contentEnd;
762
763        public int size;
764        public ArrayList<MediaItem> items;
765    }
766
767    private class GetUpdateInfo implements Callable<UpdateInfo> {
768
769        private boolean needContentReload() {
770            for (int i = mContentStart, n = mContentEnd; i < n; ++i) {
771                if (mData[i % DATA_CACHE_SIZE] == null) return true;
772            }
773            MediaItem current = mData[mCurrentIndex % DATA_CACHE_SIZE];
774            return current == null || current.getPath() != mItemPath;
775        }
776
777        @Override
778        public UpdateInfo call() throws Exception {
779            // TODO: Try to load some data in first update
780            UpdateInfo info = new UpdateInfo();
781            info.version = mSourceVersion;
782            info.reloadContent = needContentReload();
783            info.target = mItemPath;
784            info.indexHint = mCurrentIndex;
785            info.contentStart = mContentStart;
786            info.contentEnd = mContentEnd;
787            info.size = mSize;
788            return info;
789        }
790    }
791
792    private class UpdateContent implements Callable<Void> {
793        UpdateInfo mUpdateInfo;
794
795        public UpdateContent(UpdateInfo updateInfo) {
796            mUpdateInfo = updateInfo;
797        }
798
799        @Override
800        public Void call() throws Exception {
801            UpdateInfo info = mUpdateInfo;
802            mSourceVersion = info.version;
803
804            if (info.size != mSize) {
805                mSize = info.size;
806                if (mContentEnd > mSize) mContentEnd = mSize;
807                if (mActiveEnd > mSize) mActiveEnd = mSize;
808            }
809
810            if (info.indexHint == MediaSet.INDEX_NOT_FOUND) {
811                // The image has been deleted, clear mItemPath, the
812                // mCurrentIndex will be updated in the updateCurrentItem().
813                mItemPath = null;
814                updateCurrentItem();
815            } else {
816                mCurrentIndex = info.indexHint;
817            }
818
819            updateSlidingWindow();
820
821            if (info.items != null) {
822                int start = Math.max(info.contentStart, mContentStart);
823                int end = Math.min(info.contentStart + info.items.size(), mContentEnd);
824                int dataIndex = start % DATA_CACHE_SIZE;
825                for (int i = start; i < end; ++i) {
826                    mData[dataIndex] = info.items.get(i - info.contentStart);
827                    if (++dataIndex == DATA_CACHE_SIZE) dataIndex = 0;
828                }
829            }
830            if (mItemPath == null) {
831                MediaItem current = mData[mCurrentIndex % DATA_CACHE_SIZE];
832                mItemPath = current == null ? null : current.getPath();
833            }
834            updateImageCache();
835            updateTileProvider();
836            updateImageRequests();
837            fireDataChange();
838            return null;
839        }
840
841        private void updateCurrentItem() {
842            if (mSize == 0) return;
843            if (mCurrentIndex >= mSize) {
844                mCurrentIndex = mSize - 1;
845            }
846            fireDataChange();
847        }
848    }
849
850    private class ReloadTask extends Thread {
851        private volatile boolean mActive = true;
852        private volatile boolean mDirty = true;
853
854        private boolean mIsLoading = false;
855
856        private void updateLoading(boolean loading) {
857            if (mIsLoading == loading) return;
858            mIsLoading = loading;
859            mMainHandler.sendEmptyMessage(loading ? MSG_LOAD_START : MSG_LOAD_FINISH);
860        }
861
862        @Override
863        public void run() {
864            while (mActive) {
865                synchronized (this) {
866                    if (!mDirty && mActive) {
867                        updateLoading(false);
868                        Utils.waitWithoutInterrupt(this);
869                        continue;
870                    }
871                }
872                mDirty = false;
873                UpdateInfo info = executeAndWait(new GetUpdateInfo());
874                synchronized (DataManager.LOCK) {
875                    updateLoading(true);
876                    long version = mSource.reload();
877                    if (info.version != version) {
878                        info.reloadContent = true;
879                        info.size = mSource.getMediaItemCount();
880                    }
881                    if (!info.reloadContent) continue;
882                    info.items =  mSource.getMediaItem(info.contentStart, info.contentEnd);
883                    MediaItem item = findCurrentMediaItem(info);
884                    if (item == null || item.getPath() != info.target) {
885                        info.indexHint = findIndexOfTarget(info);
886                    }
887                }
888                executeAndWait(new UpdateContent(info));
889            }
890        }
891
892        public synchronized void notifyDirty() {
893            mDirty = true;
894            notifyAll();
895        }
896
897        public synchronized void terminate() {
898            mActive = false;
899            notifyAll();
900        }
901
902        private MediaItem findCurrentMediaItem(UpdateInfo info) {
903            ArrayList<MediaItem> items = info.items;
904            int index = info.indexHint - info.contentStart;
905            return index < 0 || index >= items.size() ? null : items.get(index);
906        }
907
908        private int findIndexOfTarget(UpdateInfo info) {
909            if (info.target == null) return info.indexHint;
910            ArrayList<MediaItem> items = info.items;
911
912            // First, try to find the item in the data just loaded
913            if (items != null) {
914                for (int i = 0, n = items.size(); i < n; ++i) {
915                    if (items.get(i).getPath() == info.target) return i + info.contentStart;
916                }
917            }
918
919            // Not found, find it in mSource.
920            return mSource.getIndexOfItem(info.target, info.indexHint);
921        }
922    }
923}
924