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.os.Handler;
     20 import android.os.Message;
     21 import android.os.Process;
     22 
     23 import com.android.gallery3d.common.Utils;
     24 import com.android.gallery3d.data.ContentListener;
     25 import com.android.gallery3d.data.MediaItem;
     26 import com.android.gallery3d.data.MediaObject;
     27 import com.android.gallery3d.data.MediaSet;
     28 import com.android.gallery3d.data.Path;
     29 import com.android.gallery3d.ui.SynchronizedHandler;
     30 
     31 import java.util.ArrayList;
     32 import java.util.Arrays;
     33 import java.util.concurrent.Callable;
     34 import java.util.concurrent.ExecutionException;
     35 import java.util.concurrent.FutureTask;
     36 
     37 public class AlbumDataLoader {
     38     @SuppressWarnings("unused")
     39     private static final String TAG = "AlbumDataAdapter";
     40     private static final int DATA_CACHE_SIZE = 1000;
     41 
     42     private static final int MSG_LOAD_START = 1;
     43     private static final int MSG_LOAD_FINISH = 2;
     44     private static final int MSG_RUN_OBJECT = 3;
     45 
     46     private static final int MIN_LOAD_COUNT = 32;
     47     private static final int MAX_LOAD_COUNT = 64;
     48 
     49     private final MediaItem[] mData;
     50     private final long[] mItemVersion;
     51     private final long[] mSetVersion;
     52 
     53     public static interface DataListener {
     54         public void onContentChanged(int index);
     55         public void onSizeChanged(int size);
     56     }
     57 
     58     private int mActiveStart = 0;
     59     private int mActiveEnd = 0;
     60 
     61     private int mContentStart = 0;
     62     private int mContentEnd = 0;
     63 
     64     private final MediaSet mSource;
     65     private long mSourceVersion = MediaObject.INVALID_DATA_VERSION;
     66 
     67     private final Handler mMainHandler;
     68     private int mSize = 0;
     69 
     70     private DataListener mDataListener;
     71     private MySourceListener mSourceListener = new MySourceListener();
     72     private LoadingListener mLoadingListener;
     73 
     74     private ReloadTask mReloadTask;
     75     // the data version on which last loading failed
     76     private long mFailedVersion = MediaObject.INVALID_DATA_VERSION;
     77 
     78     public AlbumDataLoader(AbstractGalleryActivity context, MediaSet mediaSet) {
     79         mSource = mediaSet;
     80 
     81         mData = new MediaItem[DATA_CACHE_SIZE];
     82         mItemVersion = new long[DATA_CACHE_SIZE];
     83         mSetVersion = new long[DATA_CACHE_SIZE];
     84         Arrays.fill(mItemVersion, MediaObject.INVALID_DATA_VERSION);
     85         Arrays.fill(mSetVersion, MediaObject.INVALID_DATA_VERSION);
     86 
     87         mMainHandler = new SynchronizedHandler(context.getGLRoot()) {
     88             @Override
     89             public void handleMessage(Message message) {
     90                 switch (message.what) {
     91                     case MSG_RUN_OBJECT:
     92                         ((Runnable) message.obj).run();
     93                         return;
     94                     case MSG_LOAD_START:
     95                         if (mLoadingListener != null) mLoadingListener.onLoadingStarted();
     96                         return;
     97                     case MSG_LOAD_FINISH:
     98                         if (mLoadingListener != null) {
     99                             boolean loadingFailed =
    100                                     (mFailedVersion != MediaObject.INVALID_DATA_VERSION);
    101                             mLoadingListener.onLoadingFinished(loadingFailed);
    102                         }
    103                         return;
    104                 }
    105             }
    106         };
    107     }
    108 
    109     public void resume() {
    110         mSource.addContentListener(mSourceListener);
    111         mReloadTask = new ReloadTask();
    112         mReloadTask.start();
    113     }
    114 
    115     public void pause() {
    116         mReloadTask.terminate();
    117         mReloadTask = null;
    118         mSource.removeContentListener(mSourceListener);
    119     }
    120 
    121     public MediaItem get(int index) {
    122         if (!isActive(index)) {
    123             return mSource.getMediaItem(index, 1).get(0);
    124         }
    125         return mData[index % mData.length];
    126     }
    127 
    128     public int getActiveStart() {
    129         return mActiveStart;
    130     }
    131 
    132     public boolean isActive(int index) {
    133         return index >= mActiveStart && index < mActiveEnd;
    134     }
    135 
    136     public int size() {
    137         return mSize;
    138     }
    139 
    140     // Returns the index of the MediaItem with the given path or
    141     // -1 if the path is not cached
    142     public int findItem(Path id) {
    143         for (int i = mContentStart; i < mContentEnd; i++) {
    144             MediaItem item = mData[i % DATA_CACHE_SIZE];
    145             if (item != null && id == item.getPath()) {
    146                 return i;
    147             }
    148         }
    149         return -1;
    150     }
    151 
    152     private void clearSlot(int slotIndex) {
    153         mData[slotIndex] = null;
    154         mItemVersion[slotIndex] = MediaObject.INVALID_DATA_VERSION;
    155         mSetVersion[slotIndex] = MediaObject.INVALID_DATA_VERSION;
    156     }
    157 
    158     private void setContentWindow(int contentStart, int contentEnd) {
    159         if (contentStart == mContentStart && contentEnd == mContentEnd) return;
    160         int end = mContentEnd;
    161         int start = mContentStart;
    162 
    163         // We need change the content window before calling reloadData(...)
    164         synchronized (this) {
    165             mContentStart = contentStart;
    166             mContentEnd = contentEnd;
    167         }
    168         long[] itemVersion = mItemVersion;
    169         long[] setVersion = mSetVersion;
    170         if (contentStart >= end || start >= contentEnd) {
    171             for (int i = start, n = end; i < n; ++i) {
    172                 clearSlot(i % DATA_CACHE_SIZE);
    173             }
    174         } else {
    175             for (int i = start; i < contentStart; ++i) {
    176                 clearSlot(i % DATA_CACHE_SIZE);
    177             }
    178             for (int i = contentEnd, n = end; i < n; ++i) {
    179                 clearSlot(i % DATA_CACHE_SIZE);
    180             }
    181         }
    182         if (mReloadTask != null) mReloadTask.notifyDirty();
    183     }
    184 
    185     public void setActiveWindow(int start, int end) {
    186         if (start == mActiveStart && end == mActiveEnd) return;
    187 
    188         Utils.assertTrue(start <= end
    189                 && end - start <= mData.length && end <= mSize);
    190 
    191         int length = mData.length;
    192         mActiveStart = start;
    193         mActiveEnd = end;
    194 
    195         // If no data is visible, keep the cache content
    196         if (start == end) return;
    197 
    198         int contentStart = Utils.clamp((start + end) / 2 - length / 2,
    199                 0, Math.max(0, mSize - length));
    200         int contentEnd = Math.min(contentStart + length, mSize);
    201         if (mContentStart > start || mContentEnd < end
    202                 || Math.abs(contentStart - mContentStart) > MIN_LOAD_COUNT) {
    203             setContentWindow(contentStart, contentEnd);
    204         }
    205     }
    206 
    207     private class MySourceListener implements ContentListener {
    208         @Override
    209         public void onContentDirty() {
    210             if (mReloadTask != null) mReloadTask.notifyDirty();
    211         }
    212     }
    213 
    214     public void setDataListener(DataListener listener) {
    215         mDataListener = listener;
    216     }
    217 
    218     public void setLoadingListener(LoadingListener listener) {
    219         mLoadingListener = listener;
    220     }
    221 
    222     private <T> T executeAndWait(Callable<T> callable) {
    223         FutureTask<T> task = new FutureTask<T>(callable);
    224         mMainHandler.sendMessage(
    225                 mMainHandler.obtainMessage(MSG_RUN_OBJECT, task));
    226         try {
    227             return task.get();
    228         } catch (InterruptedException e) {
    229             return null;
    230         } catch (ExecutionException e) {
    231             throw new RuntimeException(e);
    232         }
    233     }
    234 
    235     private static class UpdateInfo {
    236         public long version;
    237         public int reloadStart;
    238         public int reloadCount;
    239 
    240         public int size;
    241         public ArrayList<MediaItem> items;
    242     }
    243 
    244     private class GetUpdateInfo implements Callable<UpdateInfo> {
    245         private final long mVersion;
    246 
    247         public GetUpdateInfo(long version) {
    248             mVersion = version;
    249         }
    250 
    251         @Override
    252         public UpdateInfo call() throws Exception {
    253             if (mFailedVersion == mVersion) {
    254                 // previous loading failed, return null to pause loading
    255                 return null;
    256             }
    257             UpdateInfo info = new UpdateInfo();
    258             long version = mVersion;
    259             info.version = mSourceVersion;
    260             info.size = mSize;
    261             long setVersion[] = mSetVersion;
    262             for (int i = mContentStart, n = mContentEnd; i < n; ++i) {
    263                 int index = i % DATA_CACHE_SIZE;
    264                 if (setVersion[index] != version) {
    265                     info.reloadStart = i;
    266                     info.reloadCount = Math.min(MAX_LOAD_COUNT, n - i);
    267                     return info;
    268                 }
    269             }
    270             return mSourceVersion == mVersion ? null : info;
    271         }
    272     }
    273 
    274     private class UpdateContent implements Callable<Void> {
    275 
    276         private UpdateInfo mUpdateInfo;
    277 
    278         public UpdateContent(UpdateInfo info) {
    279             mUpdateInfo = info;
    280         }
    281 
    282         @Override
    283         public Void call() throws Exception {
    284             UpdateInfo info = mUpdateInfo;
    285             mSourceVersion = info.version;
    286             if (mSize != info.size) {
    287                 mSize = info.size;
    288                 if (mDataListener != null) mDataListener.onSizeChanged(mSize);
    289                 if (mContentEnd > mSize) mContentEnd = mSize;
    290                 if (mActiveEnd > mSize) mActiveEnd = mSize;
    291             }
    292 
    293             ArrayList<MediaItem> items = info.items;
    294 
    295             mFailedVersion = MediaObject.INVALID_DATA_VERSION;
    296             if ((items == null) || items.isEmpty()) {
    297                 if (info.reloadCount > 0) {
    298                     mFailedVersion = info.version;
    299                     Log.d(TAG, "loading failed: " + mFailedVersion);
    300                 }
    301                 return null;
    302             }
    303             int start = Math.max(info.reloadStart, mContentStart);
    304             int end = Math.min(info.reloadStart + items.size(), mContentEnd);
    305 
    306             for (int i = start; i < end; ++i) {
    307                 int index = i % DATA_CACHE_SIZE;
    308                 mSetVersion[index] = info.version;
    309                 MediaItem updateItem = items.get(i - info.reloadStart);
    310                 long itemVersion = updateItem.getDataVersion();
    311                 if (mItemVersion[index] != itemVersion) {
    312                     mItemVersion[index] = itemVersion;
    313                     mData[index] = updateItem;
    314                     if (mDataListener != null && i >= mActiveStart && i < mActiveEnd) {
    315                         mDataListener.onContentChanged(i);
    316                     }
    317                 }
    318             }
    319             return null;
    320         }
    321     }
    322 
    323     /*
    324      * The thread model of ReloadTask
    325      *      *
    326      * [Reload Task]       [Main Thread]
    327      *       |                   |
    328      * getUpdateInfo() -->       |           (synchronous call)
    329      *     (wait) <----    getUpdateInfo()
    330      *       |                   |
    331      *   Load Data               |
    332      *       |                   |
    333      * updateContent() -->       |           (synchronous call)
    334      *     (wait)          updateContent()
    335      *       |                   |
    336      *       |                   |
    337      */
    338     private class ReloadTask extends Thread {
    339 
    340         private volatile boolean mActive = true;
    341         private volatile boolean mDirty = true;
    342         private boolean mIsLoading = false;
    343 
    344         private void updateLoading(boolean loading) {
    345             if (mIsLoading == loading) return;
    346             mIsLoading = loading;
    347             mMainHandler.sendEmptyMessage(loading ? MSG_LOAD_START : MSG_LOAD_FINISH);
    348         }
    349 
    350         @Override
    351         public void run() {
    352             Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
    353 
    354             boolean updateComplete = false;
    355             while (mActive) {
    356                 synchronized (this) {
    357                     if (mActive && !mDirty && updateComplete) {
    358                         updateLoading(false);
    359                         if (mFailedVersion != MediaObject.INVALID_DATA_VERSION) {
    360                             Log.d(TAG, "reload pause");
    361                         }
    362                         Utils.waitWithoutInterrupt(this);
    363                         if (mActive && (mFailedVersion != MediaObject.INVALID_DATA_VERSION)) {
    364                             Log.d(TAG, "reload resume");
    365                         }
    366                         continue;
    367                     }
    368                     mDirty = false;
    369                 }
    370                 updateLoading(true);
    371                 long version = mSource.reload();
    372                 UpdateInfo info = executeAndWait(new GetUpdateInfo(version));
    373                 updateComplete = info == null;
    374                 if (updateComplete) continue;
    375                 if (info.version != version) {
    376                     info.size = mSource.getMediaItemCount();
    377                     info.version = version;
    378                 }
    379                 if (info.reloadCount > 0) {
    380                     info.items = mSource.getMediaItem(info.reloadStart, info.reloadCount);
    381                 }
    382                 executeAndWait(new UpdateContent(info));
    383             }
    384             updateLoading(false);
    385         }
    386 
    387         public synchronized void notifyDirty() {
    388             mDirty = true;
    389             notifyAll();
    390         }
    391 
    392         public synchronized void terminate() {
    393             mActive = false;
    394             notifyAll();
    395         }
    396     }
    397 }
    398