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.Arrays;
     32 import java.util.concurrent.Callable;
     33 import java.util.concurrent.ExecutionException;
     34 import java.util.concurrent.FutureTask;
     35 
     36 public class AlbumSetDataLoader {
     37     @SuppressWarnings("unused")
     38     private static final String TAG = "AlbumSetDataAdapter";
     39 
     40     private static final int INDEX_NONE = -1;
     41 
     42     private static final int MIN_LOAD_COUNT = 4;
     43 
     44     private static final int MSG_LOAD_START = 1;
     45     private static final int MSG_LOAD_FINISH = 2;
     46     private static final int MSG_RUN_OBJECT = 3;
     47 
     48     public static interface DataListener {
     49         public void onContentChanged(int index);
     50         public void onSizeChanged(int size);
     51     }
     52 
     53     private final MediaSet[] mData;
     54     private final MediaItem[] mCoverItem;
     55     private final int[] mTotalCount;
     56     private final long[] mItemVersion;
     57     private final long[] mSetVersion;
     58 
     59     private int mActiveStart = 0;
     60     private int mActiveEnd = 0;
     61 
     62     private int mContentStart = 0;
     63     private int mContentEnd = 0;
     64 
     65     private final MediaSet mSource;
     66     private long mSourceVersion = MediaObject.INVALID_DATA_VERSION;
     67     private int mSize;
     68 
     69     private DataListener mDataListener;
     70     private LoadingListener mLoadingListener;
     71     private ReloadTask mReloadTask;
     72 
     73     private final Handler mMainHandler;
     74 
     75     private final MySourceListener mSourceListener = new MySourceListener();
     76 
     77     public AlbumSetDataLoader(AbstractGalleryActivity activity, MediaSet albumSet, int cacheSize) {
     78         mSource = Utils.checkNotNull(albumSet);
     79         mCoverItem = new MediaItem[cacheSize];
     80         mData = new MediaSet[cacheSize];
     81         mTotalCount = new int[cacheSize];
     82         mItemVersion = new long[cacheSize];
     83         mSetVersion = new long[cacheSize];
     84         Arrays.fill(mItemVersion, MediaObject.INVALID_DATA_VERSION);
     85         Arrays.fill(mSetVersion, MediaObject.INVALID_DATA_VERSION);
     86 
     87         mMainHandler = new SynchronizedHandler(activity.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) mLoadingListener.onLoadingFinished(false);
     99                         return;
    100                 }
    101             }
    102         };
    103     }
    104 
    105     public void pause() {
    106         mReloadTask.terminate();
    107         mReloadTask = null;
    108         mSource.removeContentListener(mSourceListener);
    109     }
    110 
    111     public void resume() {
    112         mSource.addContentListener(mSourceListener);
    113         mReloadTask = new ReloadTask();
    114         mReloadTask.start();
    115     }
    116 
    117     private void assertIsActive(int index) {
    118         if (index < mActiveStart && index >= mActiveEnd) {
    119             throw new IllegalArgumentException(String.format(
    120                     "%s not in (%s, %s)", index, mActiveStart, mActiveEnd));
    121         }
    122     }
    123 
    124     public MediaSet getMediaSet(int index) {
    125         assertIsActive(index);
    126         return mData[index % mData.length];
    127     }
    128 
    129     public MediaItem getCoverItem(int index) {
    130         assertIsActive(index);
    131         return mCoverItem[index % mCoverItem.length];
    132     }
    133 
    134     public int getTotalCount(int index) {
    135         assertIsActive(index);
    136         return mTotalCount[index % mTotalCount.length];
    137     }
    138 
    139     public int getActiveStart() {
    140         return mActiveStart;
    141     }
    142 
    143     public boolean isActive(int index) {
    144         return index >= mActiveStart && index < mActiveEnd;
    145     }
    146 
    147     public int size() {
    148         return mSize;
    149     }
    150 
    151     // Returns the index of the MediaSet with the given path or
    152     // -1 if the path is not cached
    153     public int findSet(Path id) {
    154         int length = mData.length;
    155         for (int i = mContentStart; i < mContentEnd; i++) {
    156             MediaSet set = mData[i % length];
    157             if (set != null && id == set.getPath()) {
    158                 return i;
    159             }
    160         }
    161         return -1;
    162     }
    163 
    164     private void clearSlot(int slotIndex) {
    165         mData[slotIndex] = null;
    166         mCoverItem[slotIndex] = null;
    167         mTotalCount[slotIndex] = 0;
    168         mItemVersion[slotIndex] = MediaObject.INVALID_DATA_VERSION;
    169         mSetVersion[slotIndex] = MediaObject.INVALID_DATA_VERSION;
    170     }
    171 
    172     private void setContentWindow(int contentStart, int contentEnd) {
    173         if (contentStart == mContentStart && contentEnd == mContentEnd) return;
    174         int length = mCoverItem.length;
    175 
    176         int start = this.mContentStart;
    177         int end = this.mContentEnd;
    178 
    179         mContentStart = contentStart;
    180         mContentEnd = contentEnd;
    181 
    182         if (contentStart >= end || start >= contentEnd) {
    183             for (int i = start, n = end; i < n; ++i) {
    184                 clearSlot(i % length);
    185             }
    186         } else {
    187             for (int i = start; i < contentStart; ++i) {
    188                 clearSlot(i % length);
    189             }
    190             for (int i = contentEnd, n = end; i < n; ++i) {
    191                 clearSlot(i % length);
    192             }
    193         }
    194         mReloadTask.notifyDirty();
    195     }
    196 
    197     public void setActiveWindow(int start, int end) {
    198         if (start == mActiveStart && end == mActiveEnd) return;
    199 
    200         Utils.assertTrue(start <= end
    201                 && end - start <= mCoverItem.length && end <= mSize);
    202 
    203         mActiveStart = start;
    204         mActiveEnd = end;
    205 
    206         int length = mCoverItem.length;
    207         // If no data is visible, keep the cache content
    208         if (start == end) return;
    209 
    210         int contentStart = Utils.clamp((start + end) / 2 - length / 2,
    211                 0, Math.max(0, mSize - length));
    212         int contentEnd = Math.min(contentStart + length, mSize);
    213         if (mContentStart > start || mContentEnd < end
    214                 || Math.abs(contentStart - mContentStart) > MIN_LOAD_COUNT) {
    215             setContentWindow(contentStart, contentEnd);
    216         }
    217     }
    218 
    219     private class MySourceListener implements ContentListener {
    220         @Override
    221         public void onContentDirty() {
    222             mReloadTask.notifyDirty();
    223         }
    224     }
    225 
    226     public void setModelListener(DataListener listener) {
    227         mDataListener = listener;
    228     }
    229 
    230     public void setLoadingListener(LoadingListener listener) {
    231         mLoadingListener = listener;
    232     }
    233 
    234     private static class UpdateInfo {
    235         public long version;
    236         public int index;
    237 
    238         public int size;
    239         public MediaSet item;
    240         public MediaItem cover;
    241         public int totalCount;
    242     }
    243 
    244     private class GetUpdateInfo implements Callable<UpdateInfo> {
    245 
    246         private final long mVersion;
    247 
    248         public GetUpdateInfo(long version) {
    249             mVersion = version;
    250         }
    251 
    252         private int getInvalidIndex(long version) {
    253             long setVersion[] = mSetVersion;
    254             int length = setVersion.length;
    255             for (int i = mContentStart, n = mContentEnd; i < n; ++i) {
    256                 int index = i % length;
    257                 if (setVersion[i % length] != version) return i;
    258             }
    259             return INDEX_NONE;
    260         }
    261 
    262         @Override
    263         public UpdateInfo call() throws Exception {
    264             int index = getInvalidIndex(mVersion);
    265             if (index == INDEX_NONE && mSourceVersion == mVersion) return null;
    266             UpdateInfo info = new UpdateInfo();
    267             info.version = mSourceVersion;
    268             info.index = index;
    269             info.size = mSize;
    270             return info;
    271         }
    272     }
    273 
    274     private class UpdateContent implements Callable<Void> {
    275         private final UpdateInfo mUpdateInfo;
    276 
    277         public UpdateContent(UpdateInfo info) {
    278             mUpdateInfo = info;
    279         }
    280 
    281         @Override
    282         public Void call() {
    283             // Avoid notifying listeners of status change after pause
    284             // Otherwise gallery will be in inconsistent state after resume.
    285             if (mReloadTask == null) return null;
    286             UpdateInfo info = mUpdateInfo;
    287             mSourceVersion = info.version;
    288             if (mSize != info.size) {
    289                 mSize = info.size;
    290                 if (mDataListener != null) mDataListener.onSizeChanged(mSize);
    291                 if (mContentEnd > mSize) mContentEnd = mSize;
    292                 if (mActiveEnd > mSize) mActiveEnd = mSize;
    293             }
    294             // Note: info.index could be INDEX_NONE, i.e., -1
    295             if (info.index >= mContentStart && info.index < mContentEnd) {
    296                 int pos = info.index % mCoverItem.length;
    297                 mSetVersion[pos] = info.version;
    298                 long itemVersion = info.item.getDataVersion();
    299                 if (mItemVersion[pos] == itemVersion) return null;
    300                 mItemVersion[pos] = itemVersion;
    301                 mData[pos] = info.item;
    302                 mCoverItem[pos] = info.cover;
    303                 mTotalCount[pos] = info.totalCount;
    304                 if (mDataListener != null
    305                         && info.index >= mActiveStart && info.index < mActiveEnd) {
    306                     mDataListener.onContentChanged(info.index);
    307                 }
    308             }
    309             return null;
    310         }
    311     }
    312 
    313     private <T> T executeAndWait(Callable<T> callable) {
    314         FutureTask<T> task = new FutureTask<T>(callable);
    315         mMainHandler.sendMessage(
    316                 mMainHandler.obtainMessage(MSG_RUN_OBJECT, task));
    317         try {
    318             return task.get();
    319         } catch (InterruptedException e) {
    320             return null;
    321         } catch (ExecutionException e) {
    322             throw new RuntimeException(e);
    323         }
    324     }
    325 
    326     // TODO: load active range first
    327     private class ReloadTask extends Thread {
    328         private volatile boolean mActive = true;
    329         private volatile boolean mDirty = true;
    330         private volatile boolean mIsLoading = false;
    331 
    332         private void updateLoading(boolean loading) {
    333             if (mIsLoading == loading) return;
    334             mIsLoading = loading;
    335             mMainHandler.sendEmptyMessage(loading ? MSG_LOAD_START : MSG_LOAD_FINISH);
    336         }
    337 
    338         @Override
    339         public void run() {
    340             Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
    341 
    342             boolean updateComplete = false;
    343             while (mActive) {
    344                 synchronized (this) {
    345                     if (mActive && !mDirty && updateComplete) {
    346                         if (!mSource.isLoading()) updateLoading(false);
    347                         Utils.waitWithoutInterrupt(this);
    348                         continue;
    349                     }
    350                 }
    351                 mDirty = false;
    352                 updateLoading(true);
    353 
    354                 long version = mSource.reload();
    355                 UpdateInfo info = executeAndWait(new GetUpdateInfo(version));
    356                 updateComplete = info == null;
    357                 if (updateComplete) continue;
    358                 if (info.version != version) {
    359                     info.version = version;
    360                     info.size = mSource.getSubMediaSetCount();
    361 
    362                     // If the size becomes smaller after reload(), we may
    363                     // receive from GetUpdateInfo an index which is too
    364                     // big. Because the main thread is not aware of the size
    365                     // change until we call UpdateContent.
    366                     if (info.index >= info.size) {
    367                         info.index = INDEX_NONE;
    368                     }
    369                 }
    370                 if (info.index != INDEX_NONE) {
    371                     info.item = mSource.getSubMediaSet(info.index);
    372                     if (info.item == null) continue;
    373                     info.cover = info.item.getCoverMediaItem();
    374                     info.totalCount = info.item.getTotalMediaItemCount();
    375                 }
    376                 executeAndWait(new UpdateContent(info));
    377             }
    378             updateLoading(false);
    379         }
    380 
    381         public synchronized void notifyDirty() {
    382             mDirty = true;
    383             notifyAll();
    384         }
    385 
    386         public synchronized void terminate() {
    387             mActive = false;
    388             notifyAll();
    389         }
    390     }
    391 }
    392 
    393 
    394