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 {
onContentChanged(int index)49         public void onContentChanged(int index);
onSizeChanged(int size)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 
AlbumSetDataLoader(AbstractGalleryActivity activity, MediaSet albumSet, int cacheSize)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 
pause()105     public void pause() {
106         mReloadTask.terminate();
107         mReloadTask = null;
108         mSource.removeContentListener(mSourceListener);
109     }
110 
resume()111     public void resume() {
112         mSource.addContentListener(mSourceListener);
113         mReloadTask = new ReloadTask();
114         mReloadTask.start();
115     }
116 
assertIsActive(int index)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 
getMediaSet(int index)124     public MediaSet getMediaSet(int index) {
125         assertIsActive(index);
126         return mData[index % mData.length];
127     }
128 
getCoverItem(int index)129     public MediaItem getCoverItem(int index) {
130         assertIsActive(index);
131         return mCoverItem[index % mCoverItem.length];
132     }
133 
getTotalCount(int index)134     public int getTotalCount(int index) {
135         assertIsActive(index);
136         return mTotalCount[index % mTotalCount.length];
137     }
138 
getActiveStart()139     public int getActiveStart() {
140         return mActiveStart;
141     }
142 
isActive(int index)143     public boolean isActive(int index) {
144         return index >= mActiveStart && index < mActiveEnd;
145     }
146 
size()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
findSet(Path id)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 
clearSlot(int slotIndex)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 
setContentWindow(int contentStart, int contentEnd)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 
setActiveWindow(int start, int end)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
onContentDirty()221         public void onContentDirty() {
222             mReloadTask.notifyDirty();
223         }
224     }
225 
setModelListener(DataListener listener)226     public void setModelListener(DataListener listener) {
227         mDataListener = listener;
228     }
229 
setLoadingListener(LoadingListener listener)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 
GetUpdateInfo(long version)248         public GetUpdateInfo(long version) {
249             mVersion = version;
250         }
251 
getInvalidIndex(long version)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
call()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 
UpdateContent(UpdateInfo info)277         public UpdateContent(UpdateInfo info) {
278             mUpdateInfo = info;
279         }
280 
281         @Override
call()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 
executeAndWait(Callable<T> callable)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 
updateLoading(boolean loading)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
run()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 
notifyDirty()381         public synchronized void notifyDirty() {
382             mDirty = true;
383             notifyAll();
384         }
385 
terminate()386         public synchronized void terminate() {
387             mActive = false;
388             notifyAll();
389         }
390     }
391 }
392 
393 
394