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 android.net.Uri;
20 import android.provider.MediaStore;
21 
22 import com.android.gallery3d.common.ApiHelper;
23 
24 import java.lang.ref.SoftReference;
25 import java.util.ArrayList;
26 import java.util.Comparator;
27 import java.util.NoSuchElementException;
28 import java.util.SortedMap;
29 import 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.
36 public 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 
LocalMergeAlbum( Path path, Comparator<MediaItem> comparator, MediaSet[] sources, int bucketId)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
isCameraRoll()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 
updateData()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 
invalidateCache()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
getContentUri()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
getName()110     public String getName() {
111         return mSources.length == 0 ? "" : mSources[0].getName();
112     }
113 
114     @Override
getMediaItemCount()115     public int getMediaItemCount() {
116         return getTotalMediaItemCount();
117     }
118 
119     @Override
getMediaItem(int start, int count)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
getTotalMediaItemCount()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
reload()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
onContentDirty()190     public void onContentDirty() {
191         notifyContentChanged();
192     }
193 
194     @Override
getSupportedOperations()195     public int getSupportedOperations() {
196         return mSupportedOperation;
197     }
198 
199     @Override
delete()200     public void delete() {
201         for (MediaSet set : mSources) {
202             set.delete();
203         }
204     }
205 
206     @Override
rotate(int degrees)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 
FetchCache(MediaSet baseSet)218         public FetchCache(MediaSet baseSet) {
219             mBaseSet = baseSet;
220         }
221 
invalidate()222         public void invalidate() {
223             mCacheRef = null;
224         }
225 
getItem(int index)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
isLeafAlbum()254     public boolean isLeafAlbum() {
255         return true;
256     }
257 }
258