/* * 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.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 = 64; private final Comparator mComparator; private final MediaSet[] mSources; private String mName; private FetchCache[] mFetcher; private int mSupportedOperation; private int mBucketId; // mIndex maps global position to the position of each underlying media sets. private TreeMap mIndex = new TreeMap(); public LocalMergeAlbum( Path path, Comparator comparator, MediaSet[] sources, int bucketId) { super(path, INVALID_DATA_VERSION); mComparator = comparator; mSources = sources; mName = sources.length == 0 ? "" : sources[0].getName(); mBucketId = bucketId; for (MediaSet set : mSources) { set.addContentListener(this); } } private void updateData() { ArrayList matches = new ArrayList(); 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]); mName = mSources.length == 0 ? "" : mSources[0].getName(); } 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 mName; } @Override public int getMediaItemCount() { return getTotalMediaItemCount(); } @Override public ArrayList getMediaItem(int start, int count) { // First find the nearest mark position <= start. SortedMap 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 result = new ArrayList(); 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> mCacheRef; private int mStartPos; public FetchCache(MediaSet baseSet) { mBaseSet = baseSet; } public void invalidate() { mCacheRef = null; } public MediaItem getItem(int index) { boolean needLoading = false; ArrayList 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>(cache); mStartPos = index; } if (index < mStartPos || index >= mStartPos + cache.size()) { return null; } return cache.get(index - mStartPos); } } @Override public boolean isLeafAlbum() { return true; } }