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