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            throw new IllegalArgumentException(String.format(
124                    "%s not in (%s, %s)", index, mActiveStart, mActiveEnd));
125        }
126        return mData[index % mData.length];
127    }
128
129    public int getActiveStart() {
130        return mActiveStart;
131    }
132
133    public boolean isActive(int index) {
134        return index >= mActiveStart && index < mActiveEnd;
135    }
136
137    public int size() {
138        return mSize;
139    }
140
141    // Returns the index of the MediaItem with the given path or
142    // -1 if the path is not cached
143    public int findItem(Path id) {
144        for (int i = mContentStart; i < mContentEnd; i++) {
145            MediaItem item = mData[i % DATA_CACHE_SIZE];
146            if (item != null && id == item.getPath()) {
147                return i;
148            }
149        }
150        return -1;
151    }
152
153    private void clearSlot(int slotIndex) {
154        mData[slotIndex] = null;
155        mItemVersion[slotIndex] = MediaObject.INVALID_DATA_VERSION;
156        mSetVersion[slotIndex] = MediaObject.INVALID_DATA_VERSION;
157    }
158
159    private void setContentWindow(int contentStart, int contentEnd) {
160        if (contentStart == mContentStart && contentEnd == mContentEnd) return;
161        int end = mContentEnd;
162        int start = mContentStart;
163
164        // We need change the content window before calling reloadData(...)
165        synchronized (this) {
166            mContentStart = contentStart;
167            mContentEnd = contentEnd;
168        }
169        long[] itemVersion = mItemVersion;
170        long[] setVersion = mSetVersion;
171        if (contentStart >= end || start >= contentEnd) {
172            for (int i = start, n = end; i < n; ++i) {
173                clearSlot(i % DATA_CACHE_SIZE);
174            }
175        } else {
176            for (int i = start; i < contentStart; ++i) {
177                clearSlot(i % DATA_CACHE_SIZE);
178            }
179            for (int i = contentEnd, n = end; i < n; ++i) {
180                clearSlot(i % DATA_CACHE_SIZE);
181            }
182        }
183        if (mReloadTask != null) mReloadTask.notifyDirty();
184    }
185
186    public void setActiveWindow(int start, int end) {
187        if (start == mActiveStart && end == mActiveEnd) return;
188
189        Utils.assertTrue(start <= end
190                && end - start <= mData.length && end <= mSize);
191
192        int length = mData.length;
193        mActiveStart = start;
194        mActiveEnd = end;
195
196        // If no data is visible, keep the cache content
197        if (start == end) return;
198
199        int contentStart = Utils.clamp((start + end) / 2 - length / 2,
200                0, Math.max(0, mSize - length));
201        int contentEnd = Math.min(contentStart + length, mSize);
202        if (mContentStart > start || mContentEnd < end
203                || Math.abs(contentStart - mContentStart) > MIN_LOAD_COUNT) {
204            setContentWindow(contentStart, contentEnd);
205        }
206    }
207
208    private class MySourceListener implements ContentListener {
209        @Override
210        public void onContentDirty() {
211            if (mReloadTask != null) mReloadTask.notifyDirty();
212        }
213    }
214
215    public void setDataListener(DataListener listener) {
216        mDataListener = listener;
217    }
218
219    public void setLoadingListener(LoadingListener listener) {
220        mLoadingListener = listener;
221    }
222
223    private <T> T executeAndWait(Callable<T> callable) {
224        FutureTask<T> task = new FutureTask<T>(callable);
225        mMainHandler.sendMessage(
226                mMainHandler.obtainMessage(MSG_RUN_OBJECT, task));
227        try {
228            return task.get();
229        } catch (InterruptedException e) {
230            return null;
231        } catch (ExecutionException e) {
232            throw new RuntimeException(e);
233        }
234    }
235
236    private static class UpdateInfo {
237        public long version;
238        public int reloadStart;
239        public int reloadCount;
240
241        public int size;
242        public ArrayList<MediaItem> items;
243    }
244
245    private class GetUpdateInfo implements Callable<UpdateInfo> {
246        private final long mVersion;
247
248        public GetUpdateInfo(long version) {
249            mVersion = version;
250        }
251
252        @Override
253        public UpdateInfo call() throws Exception {
254            if (mFailedVersion == mVersion) {
255                // previous loading failed, return null to pause loading
256                return null;
257            }
258            UpdateInfo info = new UpdateInfo();
259            long version = mVersion;
260            info.version = mSourceVersion;
261            info.size = mSize;
262            long setVersion[] = mSetVersion;
263            for (int i = mContentStart, n = mContentEnd; i < n; ++i) {
264                int index = i % DATA_CACHE_SIZE;
265                if (setVersion[index] != version) {
266                    info.reloadStart = i;
267                    info.reloadCount = Math.min(MAX_LOAD_COUNT, n - i);
268                    return info;
269                }
270            }
271            return mSourceVersion == mVersion ? null : info;
272        }
273    }
274
275    private class UpdateContent implements Callable<Void> {
276
277        private UpdateInfo mUpdateInfo;
278
279        public UpdateContent(UpdateInfo info) {
280            mUpdateInfo = info;
281        }
282
283        @Override
284        public Void call() throws Exception {
285            UpdateInfo info = mUpdateInfo;
286            mSourceVersion = info.version;
287            if (mSize != info.size) {
288                mSize = info.size;
289                if (mDataListener != null) mDataListener.onSizeChanged(mSize);
290                if (mContentEnd > mSize) mContentEnd = mSize;
291                if (mActiveEnd > mSize) mActiveEnd = mSize;
292            }
293
294            ArrayList<MediaItem> items = info.items;
295
296            mFailedVersion = MediaObject.INVALID_DATA_VERSION;
297            if ((items == null) || items.isEmpty()) {
298                if (info.reloadCount > 0) {
299                    mFailedVersion = info.version;
300                    Log.d(TAG, "loading failed: " + mFailedVersion);
301                }
302                return null;
303            }
304            int start = Math.max(info.reloadStart, mContentStart);
305            int end = Math.min(info.reloadStart + items.size(), mContentEnd);
306
307            for (int i = start; i < end; ++i) {
308                int index = i % DATA_CACHE_SIZE;
309                mSetVersion[index] = info.version;
310                MediaItem updateItem = items.get(i - info.reloadStart);
311                long itemVersion = updateItem.getDataVersion();
312                if (mItemVersion[index] != itemVersion) {
313                    mItemVersion[index] = itemVersion;
314                    mData[index] = updateItem;
315                    if (mDataListener != null && i >= mActiveStart && i < mActiveEnd) {
316                        mDataListener.onContentChanged(i);
317                    }
318                }
319            }
320            return null;
321        }
322    }
323
324    /*
325     * The thread model of ReloadTask
326     *      *
327     * [Reload Task]       [Main Thread]
328     *       |                   |
329     * getUpdateInfo() -->       |           (synchronous call)
330     *     (wait) <----    getUpdateInfo()
331     *       |                   |
332     *   Load Data               |
333     *       |                   |
334     * updateContent() -->       |           (synchronous call)
335     *     (wait)          updateContent()
336     *       |                   |
337     *       |                   |
338     */
339    private class ReloadTask extends Thread {
340
341        private volatile boolean mActive = true;
342        private volatile boolean mDirty = true;
343        private boolean mIsLoading = false;
344
345        private void updateLoading(boolean loading) {
346            if (mIsLoading == loading) return;
347            mIsLoading = loading;
348            mMainHandler.sendEmptyMessage(loading ? MSG_LOAD_START : MSG_LOAD_FINISH);
349        }
350
351        @Override
352        public void run() {
353            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
354
355            boolean updateComplete = false;
356            while (mActive) {
357                synchronized (this) {
358                    if (mActive && !mDirty && updateComplete) {
359                        updateLoading(false);
360                        if (mFailedVersion != MediaObject.INVALID_DATA_VERSION) {
361                            Log.d(TAG, "reload pause");
362                        }
363                        Utils.waitWithoutInterrupt(this);
364                        if (mActive && (mFailedVersion != MediaObject.INVALID_DATA_VERSION)) {
365                            Log.d(TAG, "reload resume");
366                        }
367                        continue;
368                    }
369                    mDirty = false;
370                }
371                updateLoading(true);
372                long version = mSource.reload();
373                UpdateInfo info = executeAndWait(new GetUpdateInfo(version));
374                updateComplete = info == null;
375                if (updateComplete) continue;
376                if (info.version != version) {
377                    info.size = mSource.getMediaItemCount();
378                    info.version = version;
379                }
380                if (info.reloadCount > 0) {
381                    info.items = mSource.getMediaItem(info.reloadStart, info.reloadCount);
382                }
383                executeAndWait(new UpdateContent(info));
384            }
385            updateLoading(false);
386        }
387
388        public synchronized void notifyDirty() {
389            mDirty = true;
390            notifyAll();
391        }
392
393        public synchronized void terminate() {
394            mActive = false;
395            notifyAll();
396        }
397    }
398}
399