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