| /* |
| * Copyright (C) 2010 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.gallery3d.data; |
| |
| import android.net.Uri; |
| import android.provider.MediaStore; |
| |
| import com.android.gallery3d.common.ApiHelper; |
| |
| import java.lang.ref.SoftReference; |
| import java.util.ArrayList; |
| import java.util.Comparator; |
| import java.util.NoSuchElementException; |
| import java.util.SortedMap; |
| import java.util.TreeMap; |
| |
| // MergeAlbum merges items from two or more MediaSets. It uses a Comparator to |
| // determine the order of items. The items are assumed to be sorted in the input |
| // media sets (with the same order that the Comparator uses). |
| // |
| // This only handles MediaItems, not SubMediaSets. |
| public class LocalMergeAlbum extends MediaSet implements ContentListener { |
| @SuppressWarnings("unused") |
| private static final String TAG = "LocalMergeAlbum"; |
| private static final int PAGE_SIZE = 1024; |
| |
| private final Comparator<MediaItem> mComparator; |
| private final MediaSet[] mSources; |
| |
| private FetchCache[] mFetcher; |
| private int mSupportedOperation; |
| private int mBucketId; |
| |
| // mIndex maps global position to the position of each underlying media sets. |
| private TreeMap<Integer, int[]> mIndex = new TreeMap<Integer, int[]>(); |
| |
| public LocalMergeAlbum( |
| Path path, Comparator<MediaItem> comparator, MediaSet[] sources, int bucketId) { |
| super(path, INVALID_DATA_VERSION); |
| mComparator = comparator; |
| mSources = sources; |
| mBucketId = bucketId; |
| for (MediaSet set : mSources) { |
| set.addContentListener(this); |
| } |
| reload(); |
| } |
| |
| @Override |
| public boolean isCameraRoll() { |
| if (mSources.length == 0) return false; |
| for(MediaSet set : mSources) { |
| if (!set.isCameraRoll()) return false; |
| } |
| return true; |
| } |
| |
| private void updateData() { |
| ArrayList<MediaSet> matches = new ArrayList<MediaSet>(); |
| int supported = mSources.length == 0 ? 0 : MediaItem.SUPPORT_ALL; |
| mFetcher = new FetchCache[mSources.length]; |
| for (int i = 0, n = mSources.length; i < n; ++i) { |
| mFetcher[i] = new FetchCache(mSources[i]); |
| supported &= mSources[i].getSupportedOperations(); |
| } |
| mSupportedOperation = supported; |
| mIndex.clear(); |
| mIndex.put(0, new int[mSources.length]); |
| } |
| |
| private void invalidateCache() { |
| for (int i = 0, n = mSources.length; i < n; i++) { |
| mFetcher[i].invalidate(); |
| } |
| mIndex.clear(); |
| mIndex.put(0, new int[mSources.length]); |
| } |
| |
| @Override |
| public Uri getContentUri() { |
| String bucketId = String.valueOf(mBucketId); |
| if (ApiHelper.HAS_MEDIA_PROVIDER_FILES_TABLE) { |
| return MediaStore.Files.getContentUri("external").buildUpon() |
| .appendQueryParameter(LocalSource.KEY_BUCKET_ID, bucketId) |
| .build(); |
| } else { |
| // We don't have a single URL for a merged image before ICS |
| // So we used the image's URL as a substitute. |
| return MediaStore.Images.Media.EXTERNAL_CONTENT_URI.buildUpon() |
| .appendQueryParameter(LocalSource.KEY_BUCKET_ID, bucketId) |
| .build(); |
| } |
| } |
| |
| @Override |
| public String getName() { |
| return mSources.length == 0 ? "" : mSources[0].getName(); |
| } |
| |
| @Override |
| public int getMediaItemCount() { |
| return getTotalMediaItemCount(); |
| } |
| |
| @Override |
| public ArrayList<MediaItem> getMediaItem(int start, int count) { |
| |
| // First find the nearest mark position <= start. |
| SortedMap<Integer, int[]> head = mIndex.headMap(start + 1); |
| int markPos = head.lastKey(); |
| int[] subPos = head.get(markPos).clone(); |
| MediaItem[] slot = new MediaItem[mSources.length]; |
| |
| int size = mSources.length; |
| |
| // fill all slots |
| for (int i = 0; i < size; i++) { |
| slot[i] = mFetcher[i].getItem(subPos[i]); |
| } |
| |
| ArrayList<MediaItem> result = new ArrayList<MediaItem>(); |
| |
| for (int i = markPos; i < start + count; i++) { |
| int k = -1; // k points to the best slot up to now. |
| for (int j = 0; j < size; j++) { |
| if (slot[j] != null) { |
| if (k == -1 || mComparator.compare(slot[j], slot[k]) < 0) { |
| k = j; |
| } |
| } |
| } |
| |
| // If we don't have anything, all streams are exhausted. |
| if (k == -1) break; |
| |
| // Pick the best slot and refill it. |
| subPos[k]++; |
| if (i >= start) { |
| result.add(slot[k]); |
| } |
| slot[k] = mFetcher[k].getItem(subPos[k]); |
| |
| // Periodically leave a mark in the index, so we can come back later. |
| if ((i + 1) % PAGE_SIZE == 0) { |
| mIndex.put(i + 1, subPos.clone()); |
| } |
| } |
| |
| return result; |
| } |
| |
| @Override |
| public int getTotalMediaItemCount() { |
| int count = 0; |
| for (MediaSet set : mSources) { |
| count += set.getTotalMediaItemCount(); |
| } |
| return count; |
| } |
| |
| @Override |
| public long reload() { |
| boolean changed = false; |
| for (int i = 0, n = mSources.length; i < n; ++i) { |
| if (mSources[i].reload() > mDataVersion) changed = true; |
| } |
| if (changed) { |
| mDataVersion = nextVersionNumber(); |
| updateData(); |
| invalidateCache(); |
| } |
| return mDataVersion; |
| } |
| |
| @Override |
| public void onContentDirty() { |
| notifyContentChanged(); |
| } |
| |
| @Override |
| public int getSupportedOperations() { |
| return mSupportedOperation; |
| } |
| |
| @Override |
| public void delete() { |
| for (MediaSet set : mSources) { |
| set.delete(); |
| } |
| } |
| |
| @Override |
| public void rotate(int degrees) { |
| for (MediaSet set : mSources) { |
| set.rotate(degrees); |
| } |
| } |
| |
| private static class FetchCache { |
| private MediaSet mBaseSet; |
| private SoftReference<ArrayList<MediaItem>> mCacheRef; |
| private int mStartPos; |
| |
| public FetchCache(MediaSet baseSet) { |
| mBaseSet = baseSet; |
| } |
| |
| public void invalidate() { |
| mCacheRef = null; |
| } |
| |
| public MediaItem getItem(int index) { |
| boolean needLoading = false; |
| ArrayList<MediaItem> cache = null; |
| if (mCacheRef == null |
| || index < mStartPos || index >= mStartPos + PAGE_SIZE) { |
| needLoading = true; |
| } else { |
| cache = mCacheRef.get(); |
| if (cache == null) { |
| needLoading = true; |
| } |
| } |
| |
| if (needLoading) { |
| cache = mBaseSet.getMediaItem(index, PAGE_SIZE); |
| mCacheRef = new SoftReference<ArrayList<MediaItem>>(cache); |
| mStartPos = index; |
| } |
| |
| if (index < mStartPos || index >= mStartPos + cache.size()) { |
| return null; |
| } |
| |
| return cache.get(index - mStartPos); |
| } |
| } |
| |
| @Override |
| public boolean isLeafAlbum() { |
| return true; |
| } |
| } |