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.Arrays;
32import java.util.concurrent.Callable;
33import java.util.concurrent.ExecutionException;
34import java.util.concurrent.FutureTask;
35
36public 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