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