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 {
onContentChanged(int index)54         public void onContentChanged(int index);
onSizeChanged(int size)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 
AlbumDataLoader(AbstractGalleryActivity context, MediaSet mediaSet)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 
resume()109     public void resume() {
110         mSource.addContentListener(mSourceListener);
111         mReloadTask = new ReloadTask();
112         mReloadTask.start();
113     }
114 
pause()115     public void pause() {
116         mReloadTask.terminate();
117         mReloadTask = null;
118         mSource.removeContentListener(mSourceListener);
119     }
120 
get(int index)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 
getActiveStart()128     public int getActiveStart() {
129         return mActiveStart;
130     }
131 
isActive(int index)132     public boolean isActive(int index) {
133         return index >= mActiveStart && index < mActiveEnd;
134     }
135 
size()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
findItem(Path id)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 
clearSlot(int slotIndex)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 
setContentWindow(int contentStart, int contentEnd)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 
setActiveWindow(int start, int end)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
onContentDirty()209         public void onContentDirty() {
210             if (mReloadTask != null) mReloadTask.notifyDirty();
211         }
212     }
213 
setDataListener(DataListener listener)214     public void setDataListener(DataListener listener) {
215         mDataListener = listener;
216     }
217 
setLoadingListener(LoadingListener listener)218     public void setLoadingListener(LoadingListener listener) {
219         mLoadingListener = listener;
220     }
221 
executeAndWait(Callable<T> callable)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 
GetUpdateInfo(long version)247         public GetUpdateInfo(long version) {
248             mVersion = version;
249         }
250 
251         @Override
call()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 
UpdateContent(UpdateInfo info)278         public UpdateContent(UpdateInfo info) {
279             mUpdateInfo = info;
280         }
281 
282         @Override
call()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 
updateLoading(boolean loading)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
run()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 
notifyDirty()387         public synchronized void notifyDirty() {
388             mDirty = true;
389             notifyAll();
390         }
391 
terminate()392         public synchronized void terminate() {
393             mActive = false;
394             notifyAll();
395         }
396     }
397 }
398