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.data;
18
19import com.android.gallery3d.common.Utils;
20import com.android.gallery3d.util.Future;
21
22import java.util.ArrayList;
23import java.util.WeakHashMap;
24
25// MediaSet is a directory-like data structure.
26// It contains MediaItems and sub-MediaSets.
27//
28// The primary interface are:
29// getMediaItemCount(), getMediaItem() and
30// getSubMediaSetCount(), getSubMediaSet().
31//
32// getTotalMediaItemCount() returns the number of all MediaItems, including
33// those in sub-MediaSets.
34public abstract class MediaSet extends MediaObject {
35    @SuppressWarnings("unused")
36    private static final String TAG = "MediaSet";
37
38    public static final int MEDIAITEM_BATCH_FETCH_COUNT = 500;
39    public static final int INDEX_NOT_FOUND = -1;
40
41    public static final int SYNC_RESULT_SUCCESS = 0;
42    public static final int SYNC_RESULT_CANCELLED = 1;
43    public static final int SYNC_RESULT_ERROR = 2;
44
45    /** Listener to be used with requestSync(SyncListener). */
46    public static interface SyncListener {
47        /**
48         * Called when the sync task completed. Completion may be due to normal termination,
49         * an exception, or cancellation.
50         *
51         * @param mediaSet the MediaSet that's done with sync
52         * @param resultCode one of the SYNC_RESULT_* constants
53         */
54        void onSyncDone(MediaSet mediaSet, int resultCode);
55    }
56
57    public MediaSet(Path path, long version) {
58        super(path, version);
59    }
60
61    public int getMediaItemCount() {
62        return 0;
63    }
64
65    // Returns the media items in the range [start, start + count).
66    //
67    // The number of media items returned may be less than the specified count
68    // if there are not enough media items available. The number of
69    // media items available may not be consistent with the return value of
70    // getMediaItemCount() because the contents of database may have already
71    // changed.
72    public ArrayList<MediaItem> getMediaItem(int start, int count) {
73        return new ArrayList<MediaItem>();
74    }
75
76    public MediaItem getCoverMediaItem() {
77        ArrayList<MediaItem> items = getMediaItem(0, 1);
78        if (items.size() > 0) return items.get(0);
79        for (int i = 0, n = getSubMediaSetCount(); i < n; i++) {
80            MediaItem cover = getSubMediaSet(i).getCoverMediaItem();
81            if (cover != null) return cover;
82        }
83        return null;
84    }
85
86    public int getSubMediaSetCount() {
87        return 0;
88    }
89
90    public MediaSet getSubMediaSet(int index) {
91        throw new IndexOutOfBoundsException();
92    }
93
94    public boolean isLeafAlbum() {
95        return false;
96    }
97
98    public boolean isCameraRoll() {
99        return false;
100    }
101
102    /**
103     * Method {@link #reload()} may process the loading task in background, this method tells
104     * its client whether the loading is still in process or not.
105     */
106    public boolean isLoading() {
107        return false;
108    }
109
110    public int getTotalMediaItemCount() {
111        int total = getMediaItemCount();
112        for (int i = 0, n = getSubMediaSetCount(); i < n; i++) {
113            total += getSubMediaSet(i).getTotalMediaItemCount();
114        }
115        return total;
116    }
117
118    // TODO: we should have better implementation of sub classes
119    public int getIndexOfItem(Path path, int hint) {
120        // hint < 0 is handled below
121        // first, try to find it around the hint
122        int start = Math.max(0,
123                hint - MEDIAITEM_BATCH_FETCH_COUNT / 2);
124        ArrayList<MediaItem> list = getMediaItem(
125                start, MEDIAITEM_BATCH_FETCH_COUNT);
126        int index = getIndexOf(path, list);
127        if (index != INDEX_NOT_FOUND) return start + index;
128
129        // try to find it globally
130        start = start == 0 ? MEDIAITEM_BATCH_FETCH_COUNT : 0;
131        list = getMediaItem(start, MEDIAITEM_BATCH_FETCH_COUNT);
132        while (true) {
133            index = getIndexOf(path, list);
134            if (index != INDEX_NOT_FOUND) return start + index;
135            if (list.size() < MEDIAITEM_BATCH_FETCH_COUNT) return INDEX_NOT_FOUND;
136            start += MEDIAITEM_BATCH_FETCH_COUNT;
137            list = getMediaItem(start, MEDIAITEM_BATCH_FETCH_COUNT);
138        }
139    }
140
141    protected int getIndexOf(Path path, ArrayList<MediaItem> list) {
142        for (int i = 0, n = list.size(); i < n; ++i) {
143            // item could be null only in ClusterAlbum
144            MediaObject item = list.get(i);
145            if (item != null && item.mPath == path) return i;
146        }
147        return INDEX_NOT_FOUND;
148    }
149
150    public abstract String getName();
151
152    private WeakHashMap<ContentListener, Object> mListeners =
153            new WeakHashMap<ContentListener, Object>();
154
155    // NOTE: The MediaSet only keeps a weak reference to the listener. The
156    // listener is automatically removed when there is no other reference to
157    // the listener.
158    public void addContentListener(ContentListener listener) {
159        if (mListeners.containsKey(listener)) {
160            throw new IllegalArgumentException();
161        }
162        mListeners.put(listener, null);
163    }
164
165    public void removeContentListener(ContentListener listener) {
166        if (!mListeners.containsKey(listener)) {
167            throw new IllegalArgumentException();
168        }
169        mListeners.remove(listener);
170    }
171
172    // This should be called by subclasses when the content is changed.
173    public void notifyContentChanged() {
174        for (ContentListener listener : mListeners.keySet()) {
175            listener.onContentDirty();
176        }
177    }
178
179    // Reload the content. Return the current data version. reload() should be called
180    // in the same thread as getMediaItem(int, int) and getSubMediaSet(int).
181    public abstract long reload();
182
183    @Override
184    public MediaDetails getDetails() {
185        MediaDetails details = super.getDetails();
186        details.addDetail(MediaDetails.INDEX_TITLE, getName());
187        return details;
188    }
189
190    // Enumerate all media items in this media set (including the ones in sub
191    // media sets), in an efficient order. ItemConsumer.consumer() will be
192    // called for each media item with its index.
193    public void enumerateMediaItems(ItemConsumer consumer) {
194        enumerateMediaItems(consumer, 0);
195    }
196
197    public void enumerateTotalMediaItems(ItemConsumer consumer) {
198        enumerateTotalMediaItems(consumer, 0);
199    }
200
201    public static interface ItemConsumer {
202        void consume(int index, MediaItem item);
203    }
204
205    // The default implementation uses getMediaItem() for enumerateMediaItems().
206    // Subclasses may override this and use more efficient implementations.
207    // Returns the number of items enumerated.
208    protected int enumerateMediaItems(ItemConsumer consumer, int startIndex) {
209        int total = getMediaItemCount();
210        int start = 0;
211        while (start < total) {
212            int count = Math.min(MEDIAITEM_BATCH_FETCH_COUNT, total - start);
213            ArrayList<MediaItem> items = getMediaItem(start, count);
214            for (int i = 0, n = items.size(); i < n; i++) {
215                MediaItem item = items.get(i);
216                consumer.consume(startIndex + start + i, item);
217            }
218            start += count;
219        }
220        return total;
221    }
222
223    // Recursively enumerate all media items under this set.
224    // Returns the number of items enumerated.
225    protected int enumerateTotalMediaItems(
226            ItemConsumer consumer, int startIndex) {
227        int start = 0;
228        start += enumerateMediaItems(consumer, startIndex);
229        int m = getSubMediaSetCount();
230        for (int i = 0; i < m; i++) {
231            start += getSubMediaSet(i).enumerateTotalMediaItems(
232                    consumer, startIndex + start);
233        }
234        return start;
235    }
236
237    /**
238     * Requests sync on this MediaSet. It returns a Future object that can be used by the caller
239     * to query the status of the sync. The sync result code is one of the SYNC_RESULT_* constants
240     * defined in this class and can be obtained by Future.get().
241     *
242     * Subclasses should perform sync on a different thread.
243     *
244     * The default implementation here returns a Future stub that does nothing and returns
245     * SYNC_RESULT_SUCCESS by get().
246     */
247    public Future<Integer> requestSync(SyncListener listener) {
248        listener.onSyncDone(this, SYNC_RESULT_SUCCESS);
249        return FUTURE_STUB;
250    }
251
252    private static final Future<Integer> FUTURE_STUB = new Future<Integer>() {
253        @Override
254        public void cancel() {}
255
256        @Override
257        public boolean isCancelled() {
258            return false;
259        }
260
261        @Override
262        public boolean isDone() {
263            return true;
264        }
265
266        @Override
267        public Integer get() {
268            return SYNC_RESULT_SUCCESS;
269        }
270
271        @Override
272        public void waitDone() {}
273    };
274
275    protected Future<Integer> requestSyncOnMultipleSets(MediaSet[] sets, SyncListener listener) {
276        return new MultiSetSyncFuture(sets, listener);
277    }
278
279    private class MultiSetSyncFuture implements Future<Integer>, SyncListener {
280        @SuppressWarnings("hiding")
281        private static final String TAG = "Gallery.MultiSetSync";
282
283        private final SyncListener mListener;
284        private final Future<Integer> mFutures[];
285
286        private boolean mIsCancelled = false;
287        private int mResult = -1;
288        private int mPendingCount;
289
290        @SuppressWarnings("unchecked")
291        MultiSetSyncFuture(MediaSet[] sets, SyncListener listener) {
292            mListener = listener;
293            mPendingCount = sets.length;
294            mFutures = new Future[sets.length];
295
296            synchronized (this) {
297                for (int i = 0, n = sets.length; i < n; ++i) {
298                    mFutures[i] = sets[i].requestSync(this);
299                    Log.d(TAG, "  request sync: " + Utils.maskDebugInfo(sets[i].getName()));
300                }
301            }
302        }
303
304        @Override
305        public synchronized void cancel() {
306            if (mIsCancelled) return;
307            mIsCancelled = true;
308            for (Future<Integer> future : mFutures) future.cancel();
309            if (mResult < 0) mResult = SYNC_RESULT_CANCELLED;
310        }
311
312        @Override
313        public synchronized boolean isCancelled() {
314            return mIsCancelled;
315        }
316
317        @Override
318        public synchronized boolean isDone() {
319            return mPendingCount == 0;
320        }
321
322        @Override
323        public synchronized Integer get() {
324            waitDone();
325            return mResult;
326        }
327
328        @Override
329        public synchronized void waitDone() {
330            try {
331                while (!isDone()) wait();
332            } catch (InterruptedException e) {
333                Log.d(TAG, "waitDone() interrupted");
334            }
335        }
336
337        // SyncListener callback
338        @Override
339        public void onSyncDone(MediaSet mediaSet, int resultCode) {
340            SyncListener listener = null;
341            synchronized (this) {
342                if (resultCode == SYNC_RESULT_ERROR) mResult = SYNC_RESULT_ERROR;
343                --mPendingCount;
344                if (mPendingCount == 0) {
345                    listener = mListener;
346                    notifyAll();
347                }
348                Log.d(TAG, "onSyncDone: " + Utils.maskDebugInfo(mediaSet.getName())
349                        + " #pending=" + mPendingCount);
350            }
351            if (listener != null) listener.onSyncDone(MediaSet.this, mResult);
352        }
353    }
354}
355