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