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        mListeners.put(listener, null);
160    }
161
162    public void removeContentListener(ContentListener listener) {
163        mListeners.remove(listener);
164    }
165
166    // This should be called by subclasses when the content is changed.
167    public void notifyContentChanged() {
168        for (ContentListener listener : mListeners.keySet()) {
169            listener.onContentDirty();
170        }
171    }
172
173    // Reload the content. Return the current data version. reload() should be called
174    // in the same thread as getMediaItem(int, int) and getSubMediaSet(int).
175    public abstract long reload();
176
177    @Override
178    public MediaDetails getDetails() {
179        MediaDetails details = super.getDetails();
180        details.addDetail(MediaDetails.INDEX_TITLE, getName());
181        return details;
182    }
183
184    // Enumerate all media items in this media set (including the ones in sub
185    // media sets), in an efficient order. ItemConsumer.consumer() will be
186    // called for each media item with its index.
187    public void enumerateMediaItems(ItemConsumer consumer) {
188        enumerateMediaItems(consumer, 0);
189    }
190
191    public void enumerateTotalMediaItems(ItemConsumer consumer) {
192        enumerateTotalMediaItems(consumer, 0);
193    }
194
195    public static interface ItemConsumer {
196        void consume(int index, MediaItem item);
197    }
198
199    // The default implementation uses getMediaItem() for enumerateMediaItems().
200    // Subclasses may override this and use more efficient implementations.
201    // Returns the number of items enumerated.
202    protected int enumerateMediaItems(ItemConsumer consumer, int startIndex) {
203        int total = getMediaItemCount();
204        int start = 0;
205        while (start < total) {
206            int count = Math.min(MEDIAITEM_BATCH_FETCH_COUNT, total - start);
207            ArrayList<MediaItem> items = getMediaItem(start, count);
208            for (int i = 0, n = items.size(); i < n; i++) {
209                MediaItem item = items.get(i);
210                consumer.consume(startIndex + start + i, item);
211            }
212            start += count;
213        }
214        return total;
215    }
216
217    // Recursively enumerate all media items under this set.
218    // Returns the number of items enumerated.
219    protected int enumerateTotalMediaItems(
220            ItemConsumer consumer, int startIndex) {
221        int start = 0;
222        start += enumerateMediaItems(consumer, startIndex);
223        int m = getSubMediaSetCount();
224        for (int i = 0; i < m; i++) {
225            start += getSubMediaSet(i).enumerateTotalMediaItems(
226                    consumer, startIndex + start);
227        }
228        return start;
229    }
230
231    /**
232     * Requests sync on this MediaSet. It returns a Future object that can be used by the caller
233     * to query the status of the sync. The sync result code is one of the SYNC_RESULT_* constants
234     * defined in this class and can be obtained by Future.get().
235     *
236     * Subclasses should perform sync on a different thread.
237     *
238     * The default implementation here returns a Future stub that does nothing and returns
239     * SYNC_RESULT_SUCCESS by get().
240     */
241    public Future<Integer> requestSync(SyncListener listener) {
242        listener.onSyncDone(this, SYNC_RESULT_SUCCESS);
243        return FUTURE_STUB;
244    }
245
246    private static final Future<Integer> FUTURE_STUB = new Future<Integer>() {
247        @Override
248        public void cancel() {}
249
250        @Override
251        public boolean isCancelled() {
252            return false;
253        }
254
255        @Override
256        public boolean isDone() {
257            return true;
258        }
259
260        @Override
261        public Integer get() {
262            return SYNC_RESULT_SUCCESS;
263        }
264
265        @Override
266        public void waitDone() {}
267    };
268
269    protected Future<Integer> requestSyncOnMultipleSets(MediaSet[] sets, SyncListener listener) {
270        return new MultiSetSyncFuture(sets, listener);
271    }
272
273    private class MultiSetSyncFuture implements Future<Integer>, SyncListener {
274        @SuppressWarnings("hiding")
275        private static final String TAG = "Gallery.MultiSetSync";
276
277        private final SyncListener mListener;
278        private final Future<Integer> mFutures[];
279
280        private boolean mIsCancelled = false;
281        private int mResult = -1;
282        private int mPendingCount;
283
284        @SuppressWarnings("unchecked")
285        MultiSetSyncFuture(MediaSet[] sets, SyncListener listener) {
286            mListener = listener;
287            mPendingCount = sets.length;
288            mFutures = new Future[sets.length];
289
290            synchronized (this) {
291                for (int i = 0, n = sets.length; i < n; ++i) {
292                    mFutures[i] = sets[i].requestSync(this);
293                    Log.d(TAG, "  request sync: " + Utils.maskDebugInfo(sets[i].getName()));
294                }
295            }
296        }
297
298        @Override
299        public synchronized void cancel() {
300            if (mIsCancelled) return;
301            mIsCancelled = true;
302            for (Future<Integer> future : mFutures) future.cancel();
303            if (mResult < 0) mResult = SYNC_RESULT_CANCELLED;
304        }
305
306        @Override
307        public synchronized boolean isCancelled() {
308            return mIsCancelled;
309        }
310
311        @Override
312        public synchronized boolean isDone() {
313            return mPendingCount == 0;
314        }
315
316        @Override
317        public synchronized Integer get() {
318            waitDone();
319            return mResult;
320        }
321
322        @Override
323        public synchronized void waitDone() {
324            try {
325                while (!isDone()) wait();
326            } catch (InterruptedException e) {
327                Log.d(TAG, "waitDone() interrupted");
328            }
329        }
330
331        // SyncListener callback
332        @Override
333        public void onSyncDone(MediaSet mediaSet, int resultCode) {
334            SyncListener listener = null;
335            synchronized (this) {
336                if (resultCode == SYNC_RESULT_ERROR) mResult = SYNC_RESULT_ERROR;
337                --mPendingCount;
338                if (mPendingCount == 0) {
339                    listener = mListener;
340                    notifyAll();
341                }
342                Log.d(TAG, "onSyncDone: " + Utils.maskDebugInfo(mediaSet.getName())
343                        + " #pending=" + mPendingCount);
344            }
345            if (listener != null) listener.onSyncDone(MediaSet.this, mResult);
346        }
347    }
348}
349