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