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
17package com.android.gallery3d.app;
18
19import android.os.Handler;
20import android.os.Message;
21import android.os.Process;
22
23import com.android.gallery3d.common.Utils;
24import com.android.gallery3d.data.ContentListener;
25import com.android.gallery3d.data.MediaItem;
26import com.android.gallery3d.data.MediaObject;
27import com.android.gallery3d.data.MediaSet;
28import com.android.gallery3d.data.Path;
29import com.android.gallery3d.ui.SynchronizedHandler;
30
31import java.util.ArrayList;
32import java.util.Arrays;
33import java.util.concurrent.Callable;
34import java.util.concurrent.ExecutionException;
35import java.util.concurrent.FutureTask;
36
37public 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