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