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