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