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