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.net.Uri;
20import android.provider.MediaStore;
21
22import com.android.gallery3d.common.ApiHelper;
23
24import java.lang.ref.SoftReference;
25import java.util.ArrayList;
26import java.util.Comparator;
27import java.util.NoSuchElementException;
28import java.util.SortedMap;
29import java.util.TreeMap;
30
31// MergeAlbum merges items from two or more MediaSets. It uses a Comparator to
32// determine the order of items. The items are assumed to be sorted in the input
33// media sets (with the same order that the Comparator uses).
34//
35// This only handles MediaItems, not SubMediaSets.
36public class LocalMergeAlbum extends MediaSet implements ContentListener {
37    @SuppressWarnings("unused")
38    private static final String TAG = "LocalMergeAlbum";
39    private static final int PAGE_SIZE = 64;
40
41    private final Comparator<MediaItem> mComparator;
42    private final MediaSet[] mSources;
43
44    private FetchCache[] mFetcher;
45    private int mSupportedOperation;
46    private int mBucketId;
47
48    // mIndex maps global position to the position of each underlying media sets.
49    private TreeMap<Integer, int[]> mIndex = new TreeMap<Integer, int[]>();
50
51    public LocalMergeAlbum(
52            Path path, Comparator<MediaItem> comparator, MediaSet[] sources, int bucketId) {
53        super(path, INVALID_DATA_VERSION);
54        mComparator = comparator;
55        mSources = sources;
56        mBucketId = bucketId;
57        for (MediaSet set : mSources) {
58            set.addContentListener(this);
59        }
60        reload();
61    }
62
63    @Override
64    public boolean isCameraRoll() {
65        if (mSources.length == 0) return false;
66        for(MediaSet set : mSources) {
67            if (!set.isCameraRoll()) return false;
68        }
69        return true;
70    }
71
72    private void updateData() {
73        ArrayList<MediaSet> matches = new ArrayList<MediaSet>();
74        int supported = mSources.length == 0 ? 0 : MediaItem.SUPPORT_ALL;
75        mFetcher = new FetchCache[mSources.length];
76        for (int i = 0, n = mSources.length; i < n; ++i) {
77            mFetcher[i] = new FetchCache(mSources[i]);
78            supported &= mSources[i].getSupportedOperations();
79        }
80        mSupportedOperation = supported;
81        mIndex.clear();
82        mIndex.put(0, new int[mSources.length]);
83    }
84
85    private void invalidateCache() {
86        for (int i = 0, n = mSources.length; i < n; i++) {
87            mFetcher[i].invalidate();
88        }
89        mIndex.clear();
90        mIndex.put(0, new int[mSources.length]);
91    }
92
93    @Override
94    public Uri getContentUri() {
95        String bucketId = String.valueOf(mBucketId);
96        if (ApiHelper.HAS_MEDIA_PROVIDER_FILES_TABLE) {
97            return MediaStore.Files.getContentUri("external").buildUpon()
98                    .appendQueryParameter(LocalSource.KEY_BUCKET_ID, bucketId)
99                    .build();
100        } else {
101            // We don't have a single URL for a merged image before ICS
102            // So we used the image's URL as a substitute.
103            return MediaStore.Images.Media.EXTERNAL_CONTENT_URI.buildUpon()
104                    .appendQueryParameter(LocalSource.KEY_BUCKET_ID, bucketId)
105                    .build();
106        }
107    }
108
109    @Override
110    public String getName() {
111        return mSources.length == 0 ? "" : mSources[0].getName();
112    }
113
114    @Override
115    public int getMediaItemCount() {
116        return getTotalMediaItemCount();
117    }
118
119    @Override
120    public ArrayList<MediaItem> getMediaItem(int start, int count) {
121
122        // First find the nearest mark position <= start.
123        SortedMap<Integer, int[]> head = mIndex.headMap(start + 1);
124        int markPos = head.lastKey();
125        int[] subPos = head.get(markPos).clone();
126        MediaItem[] slot = new MediaItem[mSources.length];
127
128        int size = mSources.length;
129
130        // fill all slots
131        for (int i = 0; i < size; i++) {
132            slot[i] = mFetcher[i].getItem(subPos[i]);
133        }
134
135        ArrayList<MediaItem> result = new ArrayList<MediaItem>();
136
137        for (int i = markPos; i < start + count; i++) {
138            int k = -1;  // k points to the best slot up to now.
139            for (int j = 0; j < size; j++) {
140                if (slot[j] != null) {
141                    if (k == -1 || mComparator.compare(slot[j], slot[k]) < 0) {
142                        k = j;
143                    }
144                }
145            }
146
147            // If we don't have anything, all streams are exhausted.
148            if (k == -1) break;
149
150            // Pick the best slot and refill it.
151            subPos[k]++;
152            if (i >= start) {
153                result.add(slot[k]);
154            }
155            slot[k] = mFetcher[k].getItem(subPos[k]);
156
157            // Periodically leave a mark in the index, so we can come back later.
158            if ((i + 1) % PAGE_SIZE == 0) {
159                mIndex.put(i + 1, subPos.clone());
160            }
161        }
162
163        return result;
164    }
165
166    @Override
167    public int getTotalMediaItemCount() {
168        int count = 0;
169        for (MediaSet set : mSources) {
170            count += set.getTotalMediaItemCount();
171        }
172        return count;
173    }
174
175    @Override
176    public long reload() {
177        boolean changed = false;
178        for (int i = 0, n = mSources.length; i < n; ++i) {
179            if (mSources[i].reload() > mDataVersion) changed = true;
180        }
181        if (changed) {
182            mDataVersion = nextVersionNumber();
183            updateData();
184            invalidateCache();
185        }
186        return mDataVersion;
187    }
188
189    @Override
190    public void onContentDirty() {
191        notifyContentChanged();
192    }
193
194    @Override
195    public int getSupportedOperations() {
196        return mSupportedOperation;
197    }
198
199    @Override
200    public void delete() {
201        for (MediaSet set : mSources) {
202            set.delete();
203        }
204    }
205
206    @Override
207    public void rotate(int degrees) {
208        for (MediaSet set : mSources) {
209            set.rotate(degrees);
210        }
211    }
212
213    private static class FetchCache {
214        private MediaSet mBaseSet;
215        private SoftReference<ArrayList<MediaItem>> mCacheRef;
216        private int mStartPos;
217
218        public FetchCache(MediaSet baseSet) {
219            mBaseSet = baseSet;
220        }
221
222        public void invalidate() {
223            mCacheRef = null;
224        }
225
226        public MediaItem getItem(int index) {
227            boolean needLoading = false;
228            ArrayList<MediaItem> cache = null;
229            if (mCacheRef == null
230                    || index < mStartPos || index >= mStartPos + PAGE_SIZE) {
231                needLoading = true;
232            } else {
233                cache = mCacheRef.get();
234                if (cache == null) {
235                    needLoading = true;
236                }
237            }
238
239            if (needLoading) {
240                cache = mBaseSet.getMediaItem(index, PAGE_SIZE);
241                mCacheRef = new SoftReference<ArrayList<MediaItem>>(cache);
242                mStartPos = index;
243            }
244
245            if (index < mStartPos || index >= mStartPos + cache.size()) {
246                return null;
247            }
248
249            return cache.get(index - mStartPos);
250        }
251    }
252
253    @Override
254    public boolean isLeafAlbum() {
255        return true;
256    }
257}
258