Home | History | Annotate | Download | only in app
      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;
     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 {
    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.
    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 
    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 
    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 
    235     private Path getPath(int index) {
    236         MediaItem item = getItemInternal(index);
    237         if (item == null) return null;
    238         return item.getPath();
    239     }
    240 
    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 
    291     public void setDataListener(DataListener listener) {
    292         mDataListener = listener;
    293     }
    294 
    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 
    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
    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
    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 
    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 
    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 
    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 
    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
    441     public void moveTo(int index) {
    442         updateCurrentIndex(index);
    443     }
    444 
    445     @Override
    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
    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
    480     public int getImageRotation(int offset) {
    481         MediaItem item = getItem(mCurrentIndex + offset);
    482         return (item == null) ? 0 : item.getFullImageRotation();
    483     }
    484 
    485     @Override
    486     public void setNeedFullImage(boolean enabled) {
    487         mNeedFullImage = enabled;
    488         mMainHandler.sendEmptyMessage(MSG_UPDATE_IMAGE_REQUESTS);
    489     }
    490 
    491     @Override
    492     public boolean isCamera(int offset) {
    493         return mCurrentIndex + offset == mCameraIndex;
    494     }
    495 
    496     @Override
    497     public boolean isPanorama(int offset) {
    498         return isCamera(offset) && mIsPanorama;
    499     }
    500 
    501     @Override
    502     public boolean isStaticCamera(int offset) {
    503         return isCamera(offset) && mIsStaticCamera;
    504     }
    505 
    506     @Override
    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
    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
    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
    532     public ScreenNail getScreenNail() {
    533         return getScreenNail(0);
    534     }
    535 
    536     @Override
    537     public int getImageHeight() {
    538         return mTileProvider.getImageHeight();
    539     }
    540 
    541     @Override
    542     public int getImageWidth() {
    543         return mTileProvider.getImageWidth();
    544     }
    545 
    546     @Override
    547     public int getLevelCount() {
    548         return mTileProvider.getLevelCount();
    549     }
    550 
    551     @Override
    552     public Bitmap getTile(int level, int x, int y, int tileSize) {
    553         return mTileProvider.getTile(level, x, y, tileSize);
    554     }
    555 
    556     @Override
    557     public boolean isEmpty() {
    558         return mSize == 0;
    559     }
    560 
    561     @Override
    562     public int getCurrentIndex() {
    563         return mCurrentIndex;
    564     }
    565 
    566     @Override
    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
    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
    592     public void setFocusHintDirection(int direction) {
    593         mFocusHintDirection = direction;
    594     }
    595 
    596     @Override
    597     public void setFocusHintPath(Path path) {
    598         mFocusHintPath = path;
    599     }
    600 
    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 
    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 
    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 
    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 
    694         public ScreenNailJob(MediaItem item) {
    695             mItem = item;
    696         }
    697 
    698         @Override
    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 
    724         public FullImageJob(MediaItem item) {
    725             mItem = item;
    726         }
    727 
    728         @Override
    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.
    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.
    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.
    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 
    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 
    856         public FullImageListener(MediaItem item) {
    857             mPath = item.getPath();
    858         }
    859 
    860         @Override
    861         public void onFutureDone(Future<BitmapRegionDecoder> future) {
    862             mFuture = future;
    863             mMainHandler.sendMessage(
    864                     mMainHandler.obtainMessage(MSG_RUN_OBJECT, this));
    865         }
    866 
    867         @Override
    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 
    878         public ScreenNailListener(MediaItem item) {
    879             mPath = item.getPath();
    880         }
    881 
    882         @Override
    883         public void onFutureDone(Future<ScreenNail> future) {
    884             mFuture = future;
    885             mMainHandler.sendMessage(
    886                     mMainHandler.obtainMessage(MSG_RUN_OBJECT, this));
    887         }
    888 
    889         @Override
    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
    907         public void onContentDirty() {
    908             if (mReloadTask != null) mReloadTask.notifyDirty();
    909         }
    910     }
    911 
    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 
    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
    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 
    965         public UpdateContent(UpdateInfo updateInfo) {
    966             mUpdateInfo = updateInfo;
    967         }
    968 
    969         @Override
    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 
   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
   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 
   1092         public synchronized void notifyDirty() {
   1093             mDirty = true;
   1094             notifyAll();
   1095         }
   1096 
   1097         public synchronized void terminate() {
   1098             mActive = false;
   1099             notifyAll();
   1100         }
   1101 
   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 
   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 
   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