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