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 
17 package com.android.gallery3d.data;
18 
19 import com.android.gallery3d.common.Utils;
20 import com.android.gallery3d.util.Future;
21 
22 import java.util.ArrayList;
23 import 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.
34 public 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          */
onSyncDone(MediaSet mediaSet, int resultCode)54         void onSyncDone(MediaSet mediaSet, int resultCode);
55     }
56 
MediaSet(Path path, long version)57     public MediaSet(Path path, long version) {
58         super(path, version);
59     }
60 
getMediaItemCount()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.
getMediaItem(int start, int count)72     public ArrayList<MediaItem> getMediaItem(int start, int count) {
73         return new ArrayList<MediaItem>();
74     }
75 
getCoverMediaItem()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 
getSubMediaSetCount()86     public int getSubMediaSetCount() {
87         return 0;
88     }
89 
getSubMediaSet(int index)90     public MediaSet getSubMediaSet(int index) {
91         throw new IndexOutOfBoundsException();
92     }
93 
isLeafAlbum()94     public boolean isLeafAlbum() {
95         return false;
96     }
97 
isCameraRoll()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      */
isLoading()106     public boolean isLoading() {
107         return false;
108     }
109 
getTotalMediaItemCount()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
getIndexOfItem(Path path, int hint)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 
getIndexOf(Path path, ArrayList<MediaItem> list)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 
getName()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.
addContentListener(ContentListener listener)158     public void addContentListener(ContentListener listener) {
159         mListeners.put(listener, null);
160     }
161 
removeContentListener(ContentListener listener)162     public void removeContentListener(ContentListener listener) {
163         mListeners.remove(listener);
164     }
165 
166     // This should be called by subclasses when the content is changed.
notifyContentChanged()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).
reload()175     public abstract long reload();
176 
177     @Override
getDetails()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.
enumerateMediaItems(ItemConsumer consumer)187     public void enumerateMediaItems(ItemConsumer consumer) {
188         enumerateMediaItems(consumer, 0);
189     }
190 
enumerateTotalMediaItems(ItemConsumer consumer)191     public void enumerateTotalMediaItems(ItemConsumer consumer) {
192         enumerateTotalMediaItems(consumer, 0);
193     }
194 
195     public static interface ItemConsumer {
consume(int index, MediaItem item)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.
enumerateMediaItems(ItemConsumer consumer, int startIndex)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.
enumerateTotalMediaItems( ItemConsumer consumer, int startIndex)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      */
requestSync(SyncListener listener)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 
requestSyncOnMultipleSets(MediaSet[] sets, SyncListener listener)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")
MultiSetSyncFuture(MediaSet[] sets, SyncListener listener)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
cancel()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
isCancelled()307         public synchronized boolean isCancelled() {
308             return mIsCancelled;
309         }
310 
311         @Override
isDone()312         public synchronized boolean isDone() {
313             return mPendingCount == 0;
314         }
315 
316         @Override
get()317         public synchronized Integer get() {
318             waitDone();
319             return mResult;
320         }
321 
322         @Override
waitDone()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
onSyncDone(MediaSet mediaSet, int resultCode)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