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 android.content.Context;
20import android.database.ContentObserver;
21import android.net.Uri;
22import android.os.Handler;
23
24import com.android.gallery3d.app.GalleryApp;
25import com.android.gallery3d.app.StitchingChangeListener;
26import com.android.gallery3d.common.Utils;
27import com.android.gallery3d.data.MediaObject.PanoramaSupportCallback;
28import com.android.gallery3d.data.MediaSet.ItemConsumer;
29import com.android.gallery3d.data.MediaSource.PathId;
30import com.android.gallery3d.picasasource.PicasaSource;
31
32import java.util.ArrayList;
33import java.util.Comparator;
34import java.util.HashMap;
35import java.util.LinkedHashMap;
36import java.util.Map.Entry;
37import java.util.WeakHashMap;
38
39// DataManager manages all media sets and media items in the system.
40//
41// Each MediaSet and MediaItem has a unique 64 bits id. The most significant
42// 32 bits represents its parent, and the least significant 32 bits represents
43// the self id. For MediaSet the self id is is globally unique, but for
44// MediaItem it's unique only relative to its parent.
45//
46// To make sure the id is the same when the MediaSet is re-created, a child key
47// is provided to obtainSetId() to make sure the same self id will be used as
48// when the parent and key are the same. A sequence of child keys is called a
49// path. And it's used to identify a specific media set even if the process is
50// killed and re-created, so child keys should be stable identifiers.
51
52public class DataManager implements StitchingChangeListener {
53    public static final int INCLUDE_IMAGE = 1;
54    public static final int INCLUDE_VIDEO = 2;
55    public static final int INCLUDE_ALL = INCLUDE_IMAGE | INCLUDE_VIDEO;
56    public static final int INCLUDE_LOCAL_ONLY = 4;
57    public static final int INCLUDE_LOCAL_IMAGE_ONLY =
58            INCLUDE_LOCAL_ONLY | INCLUDE_IMAGE;
59    public static final int INCLUDE_LOCAL_VIDEO_ONLY =
60            INCLUDE_LOCAL_ONLY | INCLUDE_VIDEO;
61    public static final int INCLUDE_LOCAL_ALL_ONLY =
62            INCLUDE_LOCAL_ONLY | INCLUDE_IMAGE | INCLUDE_VIDEO;
63
64    // Any one who would like to access data should require this lock
65    // to prevent concurrency issue.
66    public static final Object LOCK = new Object();
67
68    public static DataManager from(Context context) {
69        GalleryApp app = (GalleryApp) context.getApplicationContext();
70        return app.getDataManager();
71    }
72
73    private static final String TAG = "DataManager";
74
75    // This is the path for the media set seen by the user at top level.
76    private static final String TOP_SET_PATH = "/combo/{/local/all,/picasa/all}";
77
78    private static final String TOP_IMAGE_SET_PATH = "/combo/{/local/image,/picasa/image}";
79
80    private static final String TOP_VIDEO_SET_PATH =
81            "/combo/{/local/video,/picasa/video}";
82
83    private static final String TOP_LOCAL_SET_PATH = "/local/all";
84
85    private static final String TOP_LOCAL_IMAGE_SET_PATH = "/local/image";
86
87    private static final String TOP_LOCAL_VIDEO_SET_PATH = "/local/video";
88
89    public static final Comparator<MediaItem> sDateTakenComparator =
90            new DateTakenComparator();
91
92    private static class DateTakenComparator implements Comparator<MediaItem> {
93        @Override
94        public int compare(MediaItem item1, MediaItem item2) {
95            return -Utils.compare(item1.getDateInMs(), item2.getDateInMs());
96        }
97    }
98
99    private final Handler mDefaultMainHandler;
100
101    private GalleryApp mApplication;
102    private int mActiveCount = 0;
103
104    private HashMap<Uri, NotifyBroker> mNotifierMap =
105            new HashMap<Uri, NotifyBroker>();
106
107
108    private HashMap<String, MediaSource> mSourceMap =
109            new LinkedHashMap<String, MediaSource>();
110
111    public DataManager(GalleryApp application) {
112        mApplication = application;
113        mDefaultMainHandler = new Handler(application.getMainLooper());
114    }
115
116    public synchronized void initializeSourceMap() {
117        if (!mSourceMap.isEmpty()) return;
118
119        // the order matters, the UriSource must come last
120        addSource(new LocalSource(mApplication));
121        addSource(new PicasaSource(mApplication));
122        addSource(new ComboSource(mApplication));
123        addSource(new ClusterSource(mApplication));
124        addSource(new FilterSource(mApplication));
125        addSource(new SecureSource(mApplication));
126        addSource(new UriSource(mApplication));
127        addSource(new SnailSource(mApplication));
128
129        if (mActiveCount > 0) {
130            for (MediaSource source : mSourceMap.values()) {
131                source.resume();
132            }
133        }
134    }
135
136    public String getTopSetPath(int typeBits) {
137
138        switch (typeBits) {
139            case INCLUDE_IMAGE: return TOP_IMAGE_SET_PATH;
140            case INCLUDE_VIDEO: return TOP_VIDEO_SET_PATH;
141            case INCLUDE_ALL: return TOP_SET_PATH;
142            case INCLUDE_LOCAL_IMAGE_ONLY: return TOP_LOCAL_IMAGE_SET_PATH;
143            case INCLUDE_LOCAL_VIDEO_ONLY: return TOP_LOCAL_VIDEO_SET_PATH;
144            case INCLUDE_LOCAL_ALL_ONLY: return TOP_LOCAL_SET_PATH;
145            default: throw new IllegalArgumentException();
146        }
147    }
148
149    // open for debug
150    void addSource(MediaSource source) {
151        if (source == null) return;
152        mSourceMap.put(source.getPrefix(), source);
153    }
154
155    // A common usage of this method is:
156    // synchronized (DataManager.LOCK) {
157    //     MediaObject object = peekMediaObject(path);
158    //     if (object == null) {
159    //         object = createMediaObject(...);
160    //     }
161    // }
162    public MediaObject peekMediaObject(Path path) {
163        return path.getObject();
164    }
165
166    public MediaObject getMediaObject(Path path) {
167        synchronized (LOCK) {
168            MediaObject obj = path.getObject();
169            if (obj != null) return obj;
170
171            MediaSource source = mSourceMap.get(path.getPrefix());
172            if (source == null) {
173                Log.w(TAG, "cannot find media source for path: " + path);
174                return null;
175            }
176
177            try {
178                MediaObject object = source.createMediaObject(path);
179                if (object == null) {
180                    Log.w(TAG, "cannot create media object: " + path);
181                }
182                return object;
183            } catch (Throwable t) {
184                Log.w(TAG, "exception in creating media object: " + path, t);
185                return null;
186            }
187        }
188    }
189
190    public MediaObject getMediaObject(String s) {
191        return getMediaObject(Path.fromString(s));
192    }
193
194    public MediaSet getMediaSet(Path path) {
195        return (MediaSet) getMediaObject(path);
196    }
197
198    public MediaSet getMediaSet(String s) {
199        return (MediaSet) getMediaObject(s);
200    }
201
202    public MediaSet[] getMediaSetsFromString(String segment) {
203        String[] seq = Path.splitSequence(segment);
204        int n = seq.length;
205        MediaSet[] sets = new MediaSet[n];
206        for (int i = 0; i < n; i++) {
207            sets[i] = getMediaSet(seq[i]);
208        }
209        return sets;
210    }
211
212    // Maps a list of Paths to MediaItems, and invoke consumer.consume()
213    // for each MediaItem (may not be in the same order as the input list).
214    // An index number is also passed to consumer.consume() to identify
215    // the original position in the input list of the corresponding Path (plus
216    // startIndex).
217    public void mapMediaItems(ArrayList<Path> list, ItemConsumer consumer,
218            int startIndex) {
219        HashMap<String, ArrayList<PathId>> map =
220                new HashMap<String, ArrayList<PathId>>();
221
222        // Group the path by the prefix.
223        int n = list.size();
224        for (int i = 0; i < n; i++) {
225            Path path = list.get(i);
226            String prefix = path.getPrefix();
227            ArrayList<PathId> group = map.get(prefix);
228            if (group == null) {
229                group = new ArrayList<PathId>();
230                map.put(prefix, group);
231            }
232            group.add(new PathId(path, i + startIndex));
233        }
234
235        // For each group, ask the corresponding media source to map it.
236        for (Entry<String, ArrayList<PathId>> entry : map.entrySet()) {
237            String prefix = entry.getKey();
238            MediaSource source = mSourceMap.get(prefix);
239            source.mapMediaItems(entry.getValue(), consumer);
240        }
241    }
242
243    // The following methods forward the request to the proper object.
244    public int getSupportedOperations(Path path) {
245        return getMediaObject(path).getSupportedOperations();
246    }
247
248    public void getPanoramaSupport(Path path, PanoramaSupportCallback callback) {
249        getMediaObject(path).getPanoramaSupport(callback);
250    }
251
252    public void delete(Path path) {
253        getMediaObject(path).delete();
254    }
255
256    public void rotate(Path path, int degrees) {
257        getMediaObject(path).rotate(degrees);
258    }
259
260    public Uri getContentUri(Path path) {
261        return getMediaObject(path).getContentUri();
262    }
263
264    public int getMediaType(Path path) {
265        return getMediaObject(path).getMediaType();
266    }
267
268    public Path findPathByUri(Uri uri, String type) {
269        if (uri == null) return null;
270        for (MediaSource source : mSourceMap.values()) {
271            Path path = source.findPathByUri(uri, type);
272            if (path != null) return path;
273        }
274        return null;
275    }
276
277    public Path getDefaultSetOf(Path item) {
278        MediaSource source = mSourceMap.get(item.getPrefix());
279        return source == null ? null : source.getDefaultSetOf(item);
280    }
281
282    // Returns number of bytes used by cached pictures currently downloaded.
283    public long getTotalUsedCacheSize() {
284        long sum = 0;
285        for (MediaSource source : mSourceMap.values()) {
286            sum += source.getTotalUsedCacheSize();
287        }
288        return sum;
289    }
290
291    // Returns number of bytes used by cached pictures if all pending
292    // downloads and removals are completed.
293    public long getTotalTargetCacheSize() {
294        long sum = 0;
295        for (MediaSource source : mSourceMap.values()) {
296            sum += source.getTotalTargetCacheSize();
297        }
298        return sum;
299    }
300
301    public void registerChangeNotifier(Uri uri, ChangeNotifier notifier) {
302        NotifyBroker broker = null;
303        synchronized (mNotifierMap) {
304            broker = mNotifierMap.get(uri);
305            if (broker == null) {
306                broker = new NotifyBroker(mDefaultMainHandler);
307                mApplication.getContentResolver()
308                        .registerContentObserver(uri, true, broker);
309                mNotifierMap.put(uri, broker);
310            }
311        }
312        broker.registerNotifier(notifier);
313    }
314
315    public void resume() {
316        if (++mActiveCount == 1) {
317            for (MediaSource source : mSourceMap.values()) {
318                source.resume();
319            }
320        }
321    }
322
323    public void pause() {
324        if (--mActiveCount == 0) {
325            for (MediaSource source : mSourceMap.values()) {
326                source.pause();
327            }
328        }
329    }
330
331    private static class NotifyBroker extends ContentObserver {
332        private WeakHashMap<ChangeNotifier, Object> mNotifiers =
333                new WeakHashMap<ChangeNotifier, Object>();
334
335        public NotifyBroker(Handler handler) {
336            super(handler);
337        }
338
339        public synchronized void registerNotifier(ChangeNotifier notifier) {
340            mNotifiers.put(notifier, null);
341        }
342
343        @Override
344        public synchronized void onChange(boolean selfChange) {
345            for(ChangeNotifier notifier : mNotifiers.keySet()) {
346                notifier.onChange(selfChange);
347            }
348        }
349    }
350
351    @Override
352    public void onStitchingQueued(Uri uri) {
353        // Do nothing.
354    }
355
356    @Override
357    public void onStitchingResult(Uri uri) {
358        Path path = findPathByUri(uri, null);
359        if (path != null) {
360            MediaObject mediaObject = getMediaObject(path);
361            if (mediaObject != null) {
362                mediaObject.clearCachedPanoramaSupport();
363            }
364        }
365    }
366
367    @Override
368    public void onStitchingProgress(Uri uri, int progress) {
369        // Do nothing.
370    }
371}
372