summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorGeorge Mount <mount@google.com>2013-03-29 23:15:21 +0000
committerAndroid (Google) Code Review <android-gerrit@google.com>2013-03-29 23:15:21 +0000
commit8e498a060dca7e0a1d2ceaa39b902d1cce7f7c4a (patch)
tree21bdc047662182dfa34aa35b0634ebdb4ba224d1 /src
parent534386cf6264df921f633f4c90edcab169778d26 (diff)
parent314de4034664ab668aea61e1ecd14946f1b4a978 (diff)
downloadandroid_packages_apps_Snap-8e498a060dca7e0a1d2ceaa39b902d1cce7f7c4a.tar.gz
android_packages_apps_Snap-8e498a060dca7e0a1d2ceaa39b902d1cce7f7c4a.tar.bz2
android_packages_apps_Snap-8e498a060dca7e0a1d2ceaa39b902d1cce7f7c4a.zip
Merge "Add initial implementation of MediaCache." into gb-ub-photos-bryce
Diffstat (limited to 'src')
-rw-r--r--src/com/android/gallery3d/app/GalleryAppImpl.java5
-rw-r--r--src/com/android/photos/data/FileRetriever.java109
-rw-r--r--src/com/android/photos/data/MediaCache.java649
-rw-r--r--src/com/android/photos/data/MediaCacheDatabase.java272
-rw-r--r--src/com/android/photos/data/MediaCacheUtils.java139
-rw-r--r--src/com/android/photos/data/MediaRetriever.java129
6 files changed, 1300 insertions, 3 deletions
diff --git a/src/com/android/gallery3d/app/GalleryAppImpl.java b/src/com/android/gallery3d/app/GalleryAppImpl.java
index 5b4a872d8..2abdaa0c1 100644
--- a/src/com/android/gallery3d/app/GalleryAppImpl.java
+++ b/src/com/android/gallery3d/app/GalleryAppImpl.java
@@ -17,12 +17,9 @@
package com.android.gallery3d.app;
import android.app.Application;
-import android.content.ComponentName;
import android.content.Context;
-import android.content.pm.PackageManager;
import android.os.AsyncTask;
-import com.android.gallery3d.common.ApiHelper;
import com.android.gallery3d.data.DataManager;
import com.android.gallery3d.data.DownloadCache;
import com.android.gallery3d.data.ImageCacheService;
@@ -32,6 +29,7 @@ import com.android.gallery3d.util.GalleryUtils;
import com.android.gallery3d.util.LightCycleHelper;
import com.android.gallery3d.util.ThreadPool;
import com.android.gallery3d.util.UsageStatistics;
+import com.android.photos.data.MediaCache;
import java.io.File;
@@ -56,6 +54,7 @@ public class GalleryAppImpl extends Application implements GalleryApp {
WidgetUtils.initialize(this);
PicasaSource.initialize(this);
UsageStatistics.initialize(this);
+ MediaCache.initialize(this);
mStitchingProgressManager = LightCycleHelper.createStitchingManagerInstance(this);
if (mStitchingProgressManager != null) {
diff --git a/src/com/android/photos/data/FileRetriever.java b/src/com/android/photos/data/FileRetriever.java
new file mode 100644
index 000000000..eb7686ef6
--- /dev/null
+++ b/src/com/android/photos/data/FileRetriever.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2013 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.photos.data;
+
+import android.graphics.Bitmap;
+import android.media.ExifInterface;
+import android.net.Uri;
+import android.util.Log;
+import android.webkit.MimeTypeMap;
+
+import com.android.gallery3d.common.BitmapUtils;
+
+import java.io.File;
+import java.io.IOException;
+
+public class FileRetriever implements MediaRetriever {
+ private static final String TAG = FileRetriever.class.getSimpleName();
+
+ @Override
+ public File getLocalFile(Uri contentUri) {
+ return new File(contentUri.getPath());
+ }
+
+ @Override
+ public MediaSize getFastImageSize(Uri contentUri, MediaSize size) {
+ if (isVideo(contentUri)) {
+ return null;
+ }
+ return MediaSize.TemporaryThumbnail;
+ }
+
+ @Override
+ public byte[] getTemporaryImage(Uri contentUri, MediaSize fastImageSize) {
+
+ try {
+ ExifInterface exif = new ExifInterface(contentUri.getPath());
+ if (exif.hasThumbnail()) {
+ return exif.getThumbnail();
+ }
+ } catch (IOException e) {
+ Log.w(TAG, "Unable to load exif for " + contentUri);
+ }
+ return null;
+ }
+
+ @Override
+ public boolean getMedia(Uri contentUri, MediaSize imageSize, File tempFile) {
+ if (imageSize == MediaSize.Original) {
+ return false; // getLocalFile should always return the original.
+ }
+ if (imageSize == MediaSize.Thumbnail) {
+ File preview = MediaCache.getInstance().getCachedFile(contentUri, MediaSize.Preview);
+ if (preview != null) {
+ // Just downsample the preview, it is faster.
+ return MediaCacheUtils.downsample(preview, imageSize, tempFile);
+ }
+ }
+ File highRes = new File(contentUri.getPath());
+ boolean success;
+ if (!isVideo(contentUri)) {
+ success = MediaCacheUtils.downsample(highRes, imageSize, tempFile);
+ } else {
+ // Video needs to extract the bitmap.
+ Bitmap bitmap = BitmapUtils.createVideoThumbnail(highRes.getPath());
+ if (bitmap == null) {
+ return false;
+ } else if (imageSize == MediaSize.Thumbnail
+ && !MediaCacheUtils.needsDownsample(bitmap, MediaSize.Preview)
+ && MediaCacheUtils.writeToFile(bitmap, tempFile)) {
+ // Opportunistically save preview
+ MediaCache mediaCache = MediaCache.getInstance();
+ mediaCache.insertIntoCache(contentUri, MediaSize.Preview, tempFile);
+ }
+ // Now scale the image
+ success = MediaCacheUtils.downsample(bitmap, imageSize, tempFile);
+ }
+ return success;
+ }
+
+ @Override
+ public Uri normalizeUri(Uri contentUri, MediaSize size) {
+ return contentUri;
+ }
+
+ @Override
+ public MediaSize normalizeMediaSize(Uri contentUri, MediaSize size) {
+ return size;
+ }
+
+ private static boolean isVideo(Uri uri) {
+ MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton();
+ String extension = MimeTypeMap.getFileExtensionFromUrl(uri.toString());
+ String mimeType = mimeTypeMap.getMimeTypeFromExtension(extension);
+ return (mimeType != null && mimeType.startsWith("video/"));
+ }
+}
diff --git a/src/com/android/photos/data/MediaCache.java b/src/com/android/photos/data/MediaCache.java
new file mode 100644
index 000000000..7b5eca558
--- /dev/null
+++ b/src/com/android/photos/data/MediaCache.java
@@ -0,0 +1,649 @@
+/*
+ * Copyright (C) 2013 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.photos.data;
+
+import android.content.Context;
+import android.database.sqlite.SQLiteDatabase;
+import android.net.Uri;
+import android.os.Environment;
+import android.util.Log;
+
+import com.android.photos.data.MediaCacheDatabase.Action;
+import com.android.photos.data.MediaRetriever.MediaSize;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Queue;
+
+/**
+ * MediaCache keeps a cache of images, videos, thumbnails and previews. Calls to
+ * retrieve a specific media item are executed asynchronously. The caller has an
+ * option to receive a notification for lower resolution images that happen to
+ * be available prior to the one requested.
+ * <p>
+ * When an media item has been retrieved, the notification for it is called on a
+ * separate notifier thread. This thread should not be held for a long time so
+ * that other notifications may happen.
+ * </p>
+ * <p>
+ * Media items are uniquely identified by their content URIs. Each
+ * scheme/authority can offer its own MediaRetriever, running in its own thread.
+ * </p>
+ * <p>
+ * The MediaCache is an LRU cache, but does not allow the thumbnail cache to
+ * drop below a minimum size. This prevents browsing through original images to
+ * wipe out the thumbnails.
+ * </p>
+ */
+public class MediaCache {
+ static final String TAG = MediaCache.class.getSimpleName();
+ /** Subdirectory containing the image cache. */
+ static final String IMAGE_CACHE_SUBDIR = "image_cache";
+ /** File name extension to use for cached images. */
+ static final String IMAGE_EXTENSION = ".cache";
+ /** File name extension to use for temporary cached images while retrieving. */
+ static final String TEMP_IMAGE_EXTENSION = ".temp";
+
+ public static interface ImageReady {
+ void imageReady(InputStream bitmapInputStream);
+ }
+
+ public static interface OriginalReady {
+ void originalReady(File originalFile);
+ }
+
+ /** A Thread for each MediaRetriever */
+ private class ProcessQueue extends Thread {
+ private Queue<ProcessingJob> mQueue;
+
+ public ProcessQueue(Queue<ProcessingJob> queue) {
+ mQueue = queue;
+ }
+
+ @Override
+ public void run() {
+ while (mRunning) {
+ ProcessingJob status;
+ synchronized (mQueue) {
+ while (mQueue.isEmpty()) {
+ try {
+ mQueue.wait();
+ } catch (InterruptedException e) {
+ if (!mRunning) {
+ return;
+ }
+ Log.w(TAG, "Unexpected interruption", e);
+ }
+ }
+ status = mQueue.remove();
+ }
+ processTask(status);
+ }
+ }
+ };
+
+ private interface NotifyReady {
+ void notifyReady();
+
+ void setFile(File file) throws FileNotFoundException;
+ }
+
+ private static class NotifyOriginalReady implements NotifyReady {
+ private final OriginalReady mCallback;
+ private File mFile;
+
+ public NotifyOriginalReady(OriginalReady callback) {
+ mCallback = callback;
+ }
+
+ @Override
+ public void notifyReady() {
+ mCallback.originalReady(mFile);
+ }
+
+ @Override
+ public void setFile(File file) {
+ mFile = file;
+ }
+ }
+
+ private static class NotifyImageReady implements NotifyReady {
+ private final ImageReady mCallback;
+ private InputStream mInputStream;
+
+ public NotifyImageReady(ImageReady callback) {
+ mCallback = callback;
+ }
+
+ @Override
+ public void notifyReady() {
+ mCallback.imageReady(mInputStream);
+ }
+
+ @Override
+ public void setFile(File file) throws FileNotFoundException {
+ mInputStream = new FileInputStream(file);
+ }
+
+ public void setBytes(byte[] bytes) {
+ mInputStream = new ByteArrayInputStream(bytes);
+ }
+ }
+
+ /** A media item to be retrieved and its notifications. */
+ private static class ProcessingJob {
+ public ProcessingJob(Uri uri, MediaSize size, NotifyReady complete,
+ NotifyImageReady lowResolution) {
+ this.contentUri = uri;
+ this.size = size;
+ this.complete = complete;
+ this.lowResolution = lowResolution;
+ }
+ public Uri contentUri;
+ public MediaSize size;
+ public NotifyImageReady lowResolution;
+ public NotifyReady complete;
+ }
+
+ private boolean mRunning = true;
+ private static MediaCache sInstance;
+ private File mCacheDir;
+ private Context mContext;
+ private Queue<NotifyReady> mCallbacks = new LinkedList<NotifyReady>();
+ private Map<String, MediaRetriever> mRetrievers = new HashMap<String, MediaRetriever>();
+ private Map<String, List<ProcessingJob>> mTasks = new HashMap<String, List<ProcessingJob>>();
+ private List<ProcessQueue> mProcessingThreads = new ArrayList<ProcessQueue>();
+ private MediaCacheDatabase mDatabaseHelper;
+ private long mTempImageNumber = 1;
+ private Object mTempImageNumberLock = new Object();
+
+ private long mMaxCacheSize = 40 * 1024 * 1024; // 40 MB
+ private long mMinThumbCacheSize = 4 * 1024 * 1024; // 4 MB
+ private long mCacheSize = -1;
+ private long mThumbCacheSize = -1;
+ private Object mCacheSizeLock = new Object();
+
+ private Action mNotifyCachedLowResolution = new Action() {
+ @Override
+ public void execute(Uri uri, long id, MediaSize size, Object parameter) {
+ ProcessingJob job = (ProcessingJob) parameter;
+ File file = createCacheImagePath(id);
+ addNotification(job.lowResolution, file);
+ }
+ };
+
+ private Action mMoveTempToCache = new Action() {
+ @Override
+ public void execute(Uri uri, long id, MediaSize size, Object parameter) {
+ File tempFile = (File) parameter;
+ File cacheFile = createCacheImagePath(id);
+ tempFile.renameTo(cacheFile);
+ }
+ };
+
+ private Action mDeleteFile = new Action() {
+ @Override
+ public void execute(Uri uri, long id, MediaSize size, Object parameter) {
+ File file = createCacheImagePath(id);
+ file.delete();
+ synchronized (mCacheSizeLock) {
+ if (mCacheSize != -1) {
+ long length = (Long) parameter;
+ mCacheSize -= length;
+ if (size == MediaSize.Thumbnail) {
+ mThumbCacheSize -= length;
+ }
+ }
+ }
+ }
+ };
+
+ /** The thread used to make ImageReady and OriginalReady callbacks. */
+ private Thread mProcessNotifications = new Thread() {
+ @Override
+ public void run() {
+ while (mRunning) {
+ NotifyReady notifyImage;
+ synchronized (mCallbacks) {
+ while (mCallbacks.isEmpty()) {
+ try {
+ mCallbacks.wait();
+ } catch (InterruptedException e) {
+ if (!mRunning) {
+ return;
+ }
+ Log.w(TAG, "Unexpected Interruption, continuing");
+ }
+ }
+ notifyImage = mCallbacks.remove();
+ }
+
+ notifyImage.notifyReady();
+ }
+ }
+ };
+
+ public static synchronized void initialize(Context context) {
+ if (sInstance == null) {
+ sInstance = new MediaCache(context);
+ MediaCacheUtils.initialize(context);
+ }
+ }
+
+ public static MediaCache getInstance() {
+ return sInstance;
+ }
+
+ public static synchronized void shutdown() {
+ sInstance.mRunning = false;
+ sInstance.mProcessNotifications.interrupt();
+ for (ProcessQueue processingThread : sInstance.mProcessingThreads) {
+ processingThread.interrupt();
+ }
+ sInstance = null;
+ }
+
+ private MediaCache(Context context) {
+ mDatabaseHelper = new MediaCacheDatabase(context);
+ mProcessNotifications.start();
+ mContext = context;
+ }
+
+ // This is used for testing.
+ public void setCacheDir(File cacheDir) {
+ cacheDir.mkdirs();
+ mCacheDir = cacheDir;
+ }
+
+ private File getCacheDir() {
+ synchronized (mContext) {
+ if (mCacheDir == null) {
+ String state = Environment.getExternalStorageState();
+ File baseDir;
+ if (Environment.MEDIA_MOUNTED.equals(state)) {
+ baseDir = mContext.getExternalCacheDir();
+ } else {
+ // Stored in internal cache
+ baseDir = mContext.getCacheDir();
+ }
+ mCacheDir = new File(baseDir, IMAGE_CACHE_SUBDIR);
+ mCacheDir.mkdirs();
+ }
+ return mCacheDir;
+ }
+ }
+
+ /**
+ * Invalidates all cached images related to a given contentUri. This call
+ * doesn't complete until the images have been removed from the cache.
+ */
+ public void invalidate(Uri contentUri) {
+ mDatabaseHelper.delete(contentUri, mDeleteFile);
+ }
+
+ public void clearCacheDir() {
+ File[] cachedFiles = getCacheDir().listFiles();
+ if (cachedFiles != null) {
+ for (File cachedFile : cachedFiles) {
+ cachedFile.delete();
+ }
+ }
+ }
+
+ /**
+ * Add a MediaRetriever for a Uri scheme and authority. This MediaRetriever
+ * will be granted its own thread for retrieving images.
+ */
+ public void addRetriever(String scheme, String authority, MediaRetriever retriever) {
+ String differentiator = getDifferentiator(scheme, authority);
+ synchronized (mRetrievers) {
+ mRetrievers.put(differentiator, retriever);
+ }
+ synchronized (mTasks) {
+ LinkedList<ProcessingJob> queue = new LinkedList<ProcessingJob>();
+ mTasks.put(differentiator, queue);
+ new ProcessQueue(queue).start();
+ }
+ }
+
+ /**
+ * Retrieves a thumbnail. complete will be called when the thumbnail is
+ * available. If lowResolution is not null and a lower resolution thumbnail
+ * is available before the thumbnail, lowResolution will be called prior to
+ * complete. All callbacks will be made on a thread other than the calling
+ * thread.
+ *
+ * @param contentUri The URI for the full resolution image to search for.
+ * @param complete Callback for when the image has been retrieved.
+ * @param lowResolution If not null and a lower resolution image is
+ * available prior to retrieving the thumbnail, this will be
+ * called with the low resolution bitmap.
+ */
+ public void retrieveThumbnail(Uri contentUri, ImageReady complete, ImageReady lowResolution) {
+ addTask(contentUri, complete, lowResolution, MediaSize.Thumbnail);
+ }
+
+ /**
+ * Retrieves a preview. complete will be called when the preview is
+ * available. If lowResolution is not null and a lower resolution preview is
+ * available before the preview, lowResolution will be called prior to
+ * complete. All callbacks will be made on a thread other than the calling
+ * thread.
+ *
+ * @param contentUri The URI for the full resolution image to search for.
+ * @param complete Callback for when the image has been retrieved.
+ * @param lowResolution If not null and a lower resolution image is
+ * available prior to retrieving the preview, this will be called
+ * with the low resolution bitmap.
+ */
+ public void retrievePreview(Uri contentUri, ImageReady complete, ImageReady lowResolution) {
+ addTask(contentUri, complete, lowResolution, MediaSize.Preview);
+ }
+
+ /**
+ * Retrieves the original image or video. complete will be called when the
+ * media is available on the local file system. If lowResolution is not null
+ * and a lower resolution preview is available before the original,
+ * lowResolution will be called prior to complete. All callbacks will be
+ * made on a thread other than the calling thread.
+ *
+ * @param contentUri The URI for the full resolution image to search for.
+ * @param complete Callback for when the image has been retrieved.
+ * @param lowResolution If not null and a lower resolution image is
+ * available prior to retrieving the preview, this will be called
+ * with the low resolution bitmap.
+ */
+ public void retrieveOriginal(Uri contentUri, OriginalReady complete, ImageReady lowResolution) {
+ File localFile = getLocalFile(contentUri);
+ if (localFile != null) {
+ addNotification(new NotifyOriginalReady(complete), localFile);
+ } else {
+ NotifyImageReady notifyLowResolution = (lowResolution == null) ? null
+ : new NotifyImageReady(lowResolution);
+ addTask(contentUri, new NotifyOriginalReady(complete), notifyLowResolution,
+ MediaSize.Original);
+ }
+ }
+
+ /**
+ * Looks for an already cached media at a specific size.
+ *
+ * @param contentUri The original media item content URI
+ * @param size The target size to search for in the cache
+ * @return The cached file location or null if it is not cached.
+ */
+ public File getCachedFile(Uri contentUri, MediaSize size) {
+ Long cachedId = mDatabaseHelper.getCached(contentUri, size);
+ File file = null;
+ if (cachedId != null) {
+ file = createCacheImagePath(cachedId);
+ }
+ return file;
+ }
+
+ /**
+ * Inserts a media item into the cache.
+ *
+ * @param contentUri The original media item URI.
+ * @param size The size of the media item to store in the cache.
+ * @param tempFile The temporary file where the image is stored. This file
+ * will no longer exist after executing this method.
+ * @return The new location, in the cache, of the media item or null if it
+ * wasn't possible to move into the cache.
+ */
+ public File insertIntoCache(Uri contentUri, MediaSize size, File tempFile) {
+ long fileSize = tempFile.length();
+ if (fileSize == 0) {
+ return null;
+ }
+ File cacheFile = null;
+ SQLiteDatabase db = mDatabaseHelper.getWritableDatabase();
+ // Ensure that this step is atomic
+ db.beginTransaction();
+ try {
+ Long id = mDatabaseHelper.getCached(contentUri, size);
+ if (id != null) {
+ cacheFile = createCacheImagePath(id);
+ if (tempFile.renameTo(cacheFile)) {
+ mDatabaseHelper.updateLength(id, fileSize);
+ } else {
+ Log.w(TAG, "Could not update cached file with " + tempFile);
+ tempFile.delete();
+ cacheFile = null;
+ }
+ } else {
+ ensureFreeCacheSpace(tempFile.length(), size);
+ id = mDatabaseHelper.insert(contentUri, size, mMoveTempToCache, tempFile);
+ cacheFile = createCacheImagePath(id);
+ }
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ return cacheFile;
+ }
+
+ /**
+ * For testing purposes.
+ */
+ public void setMaxCacheSize(long maxCacheSize) {
+ synchronized (mCacheSizeLock) {
+ mMaxCacheSize = maxCacheSize;
+ mMinThumbCacheSize = mMaxCacheSize / 10;
+ mCacheSize = -1;
+ mThumbCacheSize = -1;
+ }
+ }
+
+ private File createCacheImagePath(long id) {
+ return new File(getCacheDir(), String.valueOf(id) + IMAGE_EXTENSION);
+ }
+
+ private void addTask(Uri contentUri, ImageReady complete, ImageReady lowResolution,
+ MediaSize size) {
+ NotifyReady notifyComplete = new NotifyImageReady(complete);
+ NotifyImageReady notifyLowResolution = null;
+ if (lowResolution != null) {
+ notifyLowResolution = new NotifyImageReady(lowResolution);
+ }
+ addTask(contentUri, notifyComplete, notifyLowResolution, size);
+ }
+
+ private void addTask(Uri contentUri, NotifyReady complete, NotifyImageReady lowResolution,
+ MediaSize size) {
+ MediaRetriever retriever = getMediaRetriever(contentUri);
+ Uri uri = retriever.normalizeUri(contentUri, size);
+ if (uri == null) {
+ throw new IllegalArgumentException("No MediaRetriever for " + contentUri);
+ }
+ size = retriever.normalizeMediaSize(uri, size);
+
+ Long cachedId = mDatabaseHelper.getCached(uri, size);
+ if (cachedId != null) {
+ addNotification(complete, createCacheImagePath(cachedId));
+ return;
+ }
+ String differentiator = getDifferentiator(uri.getScheme(), uri.getAuthority());
+ synchronized (mTasks) {
+ List<ProcessingJob> tasks = mTasks.get(differentiator);
+ if (tasks == null) {
+ throw new IllegalArgumentException("Cannot find retriever for: " + uri);
+ }
+ synchronized (tasks) {
+ ProcessingJob job = new ProcessingJob(uri, size, complete, lowResolution);
+ tasks.add(job);
+ tasks.notifyAll();
+ }
+ }
+ }
+
+ private MediaRetriever getMediaRetriever(Uri uri) {
+ String differentiator = getDifferentiator(uri.getScheme(), uri.getAuthority());
+ MediaRetriever retriever;
+ synchronized (mRetrievers) {
+ retriever = mRetrievers.get(differentiator);
+ }
+ if (retriever == null) {
+ throw new IllegalArgumentException("No MediaRetriever for " + uri);
+ }
+ return retriever;
+ }
+
+ private File getLocalFile(Uri uri) {
+ MediaRetriever retriever = getMediaRetriever(uri);
+ File localFile = null;
+ if (retriever != null) {
+ localFile = retriever.getLocalFile(uri);
+ }
+ return localFile;
+ }
+
+ private MediaSize getFastImageSize(Uri uri, MediaSize size) {
+ MediaRetriever retriever = getMediaRetriever(uri);
+ return retriever.getFastImageSize(uri, size);
+ }
+
+ private boolean isFastImageBetter(MediaSize fastImageType, MediaSize size) {
+ if (fastImageType == null) {
+ return false;
+ }
+ if (size == null) {
+ return true;
+ }
+ return fastImageType.isBetterThan(size);
+ }
+
+ private byte[] getTemporaryImage(Uri uri, MediaSize fastImageType) {
+ MediaRetriever retriever = getMediaRetriever(uri);
+ return retriever.getTemporaryImage(uri, fastImageType);
+ }
+
+ private void processTask(ProcessingJob job) {
+ Long cachedId = mDatabaseHelper.getCached(job.contentUri, job.size);
+ if (cachedId != null) {
+ File file = createCacheImagePath(cachedId);
+ addNotification(job.complete, file);
+ return;
+ }
+
+ boolean hasLowResolution = job.lowResolution != null;
+ if (hasLowResolution) {
+ MediaSize cachedSize = mDatabaseHelper.executeOnBestCached(job.contentUri, job.size,
+ mNotifyCachedLowResolution);
+ MediaSize fastImageSize = getFastImageSize(job.contentUri, job.size);
+ if (isFastImageBetter(fastImageSize, cachedSize)) {
+ if (fastImageSize.isTemporary()) {
+ byte[] bytes = getTemporaryImage(job.contentUri, fastImageSize);
+ if (bytes != null) {
+ addNotification(job.lowResolution, bytes);
+ }
+ } else {
+ File lowFile = getMedia(job.contentUri, fastImageSize);
+ if (lowFile != null) {
+ addNotification(job.lowResolution, lowFile);
+ }
+ }
+ }
+ }
+
+ // Now get the full size desired
+ File fullSizeFile = getMedia(job.contentUri, job.size);
+ if (fullSizeFile != null) {
+ addNotification(job.complete, fullSizeFile);
+ }
+ }
+
+ private void addNotification(NotifyReady callback, File file) {
+ try {
+ callback.setFile(file);
+ synchronized (mCallbacks) {
+ mCallbacks.add(callback);
+ mCallbacks.notifyAll();
+ }
+ } catch (FileNotFoundException e) {
+ Log.e(TAG, "Unable to read file " + file, e);
+ }
+ }
+
+ private void addNotification(NotifyImageReady callback, byte[] bytes) {
+ callback.setBytes(bytes);
+ synchronized (mCallbacks) {
+ mCallbacks.add(callback);
+ mCallbacks.notifyAll();
+ }
+ }
+
+ private File getMedia(Uri uri, MediaSize size) {
+ long imageNumber;
+ synchronized (mTempImageNumberLock) {
+ imageNumber = mTempImageNumber++;
+ }
+ File tempFile = new File(getCacheDir(), String.valueOf(imageNumber) + TEMP_IMAGE_EXTENSION);
+ MediaRetriever retriever = getMediaRetriever(uri);
+ boolean retrieved = retriever.getMedia(uri, size, tempFile);
+ File cachedFile = null;
+ if (retrieved) {
+ ensureFreeCacheSpace(tempFile.length(), size);
+ long id = mDatabaseHelper.insert(uri, size, mMoveTempToCache, tempFile);
+ cachedFile = createCacheImagePath(id);
+ }
+ return cachedFile;
+ }
+
+ private static String getDifferentiator(String scheme, String authority) {
+ if (authority == null) {
+ return scheme;
+ }
+ StringBuilder differentiator = new StringBuilder(scheme);
+ differentiator.append(':');
+ differentiator.append(authority);
+ return differentiator.toString();
+ }
+
+ private void ensureFreeCacheSpace(long size, MediaSize mediaSize) {
+ synchronized (mCacheSizeLock) {
+ if (mCacheSize == -1 || mThumbCacheSize == -1) {
+ mCacheSize = mDatabaseHelper.getCacheSize();
+ mThumbCacheSize = mDatabaseHelper.getThumbnailCacheSize();
+ if (mCacheSize == -1 || mThumbCacheSize == -1) {
+ Log.e(TAG, "Can't determine size of the image cache");
+ return;
+ }
+ }
+ mCacheSize += size;
+ if (mediaSize == MediaSize.Thumbnail) {
+ mThumbCacheSize += size;
+ }
+ if (mCacheSize > mMaxCacheSize) {
+ shrinkCacheLocked();
+ }
+ }
+ }
+
+ private void shrinkCacheLocked() {
+ long deleteSize = mMinThumbCacheSize;
+ boolean includeThumbnails = (mThumbCacheSize - deleteSize) > mMinThumbCacheSize;
+ mDatabaseHelper.deleteOldCached(includeThumbnails, deleteSize, mDeleteFile);
+ }
+}
diff --git a/src/com/android/photos/data/MediaCacheDatabase.java b/src/com/android/photos/data/MediaCacheDatabase.java
new file mode 100644
index 000000000..16265b574
--- /dev/null
+++ b/src/com/android/photos/data/MediaCacheDatabase.java
@@ -0,0 +1,272 @@
+/*
+ * Copyright (C) 2013 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.photos.data;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.net.Uri;
+import android.provider.BaseColumns;
+
+import com.android.photos.data.MediaRetriever.MediaSize;
+
+import java.io.File;
+
+class MediaCacheDatabase extends SQLiteOpenHelper {
+ public static final int DB_VERSION = 1;
+ public static final String DB_NAME = "mediacache.db";
+
+ /** Internal database table used for the media cache */
+ public static final String TABLE = "media_cache";
+
+ private static interface Columns extends BaseColumns {
+ /** The Content URI of the original image. */
+ public static final String URI = "uri";
+ /** MediaSize.getValue() values. */
+ public static final String MEDIA_SIZE = "media_size";
+ /** The last time this image was queried. */
+ public static final String LAST_ACCESS = "last_access";
+ /** The image size in bytes. */
+ public static final String SIZE_IN_BYTES = "size";
+ }
+
+ static interface Action {
+ void execute(Uri uri, long id, MediaRetriever.MediaSize size, Object parameter);
+ }
+
+ private static final String[] PROJECTION_ID = {
+ Columns._ID,
+ };
+
+ private static final String[] PROJECTION_CACHED = {
+ Columns._ID, Columns.MEDIA_SIZE, Columns.SIZE_IN_BYTES,
+ };
+
+ private static final String[] PROJECTION_CACHE_SIZE = {
+ "SUM(" + Columns.SIZE_IN_BYTES + ")"
+ };
+
+ private static final String[] PROJECTION_DELETE_OLD = {
+ Columns._ID, Columns.URI, Columns.MEDIA_SIZE, Columns.SIZE_IN_BYTES, Columns.LAST_ACCESS,
+ };
+
+ public static final String CREATE_TABLE = "CREATE TABLE " + TABLE + "("
+ + Columns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
+ + Columns.URI + " TEXT NOT NULL,"
+ + Columns.MEDIA_SIZE + " INTEGER NOT NULL,"
+ + Columns.LAST_ACCESS + " INTEGER NOT NULL,"
+ + Columns.SIZE_IN_BYTES + " INTEGER NOT NULL,"
+ + "UNIQUE(" + Columns.URI + ", " + Columns.MEDIA_SIZE + "))";
+
+ public static final String DROP_TABLE = "DROP TABLE IF EXISTS " + TABLE;
+
+ public static final String WHERE_THUMBNAIL = Columns.MEDIA_SIZE + " = "
+ + MediaSize.Thumbnail.getValue();
+
+ public static final String WHERE_NOT_THUMBNAIL = Columns.MEDIA_SIZE + " <> "
+ + MediaSize.Thumbnail.getValue();
+
+ public static final String WHERE_CLEAR_CACHE = Columns.LAST_ACCESS + " <= ?";
+
+ public static final String WHERE_CLEAR_CACHE_LARGE = WHERE_CLEAR_CACHE + " AND "
+ + WHERE_NOT_THUMBNAIL;
+
+ static class QueryCacheResults {
+ public QueryCacheResults(long id, int sizeVal) {
+ this.id = id;
+ this.size = MediaRetriever.MediaSize.fromInteger(sizeVal);
+ }
+ public long id;
+ public MediaRetriever.MediaSize size;
+ }
+
+ public MediaCacheDatabase(Context context) {
+ super(context, DB_NAME, null, DB_VERSION);
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ db.execSQL(CREATE_TABLE);
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ db.execSQL(DROP_TABLE);
+ onCreate(db);
+ MediaCache.getInstance().clearCacheDir();
+ }
+
+ public Long getCached(Uri uri, MediaRetriever.MediaSize size) {
+ String where = Columns.URI + " = ? AND " + Columns.MEDIA_SIZE + " = ?";
+ SQLiteDatabase db = getWritableDatabase();
+ String[] whereArgs = {
+ uri.toString(), String.valueOf(size.getValue()),
+ };
+ Cursor cursor = db.query(TABLE, PROJECTION_ID, where, whereArgs, null, null, null);
+ Long id = null;
+ if (cursor.moveToNext()) {
+ id = cursor.getLong(0);
+ }
+ cursor.close();
+ if (id != null) {
+ String[] updateArgs = {
+ id.toString()
+ };
+ ContentValues values = new ContentValues();
+ values.put(Columns.LAST_ACCESS, System.currentTimeMillis());
+ db.beginTransaction();
+ try {
+ db.update(TABLE, values, Columns._ID + " = ?", updateArgs);
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ }
+ return id;
+ }
+
+ public MediaRetriever.MediaSize executeOnBestCached(Uri uri, MediaRetriever.MediaSize size, Action action) {
+ String where = Columns.URI + " = ? AND " + Columns.MEDIA_SIZE + " < ?";
+ String orderBy = Columns.MEDIA_SIZE + " DESC";
+ SQLiteDatabase db = getReadableDatabase();
+ String[] whereArgs = {
+ uri.toString(), String.valueOf(size.getValue()),
+ };
+ Cursor cursor = db.query(TABLE, PROJECTION_CACHED, where, whereArgs, null, null, orderBy);
+ MediaRetriever.MediaSize bestSize = null;
+ if (cursor.moveToNext()) {
+ long id = cursor.getLong(0);
+ bestSize = MediaRetriever.MediaSize.fromInteger(cursor.getInt(1));
+ long fileSize = cursor.getLong(2);
+ action.execute(uri, id, bestSize, fileSize);
+ }
+ cursor.close();
+ return bestSize;
+ }
+
+ public long insert(Uri uri, MediaRetriever.MediaSize size, Action action, File tempFile) {
+ SQLiteDatabase db = getWritableDatabase();
+ db.beginTransaction();
+ try {
+ ContentValues values = new ContentValues();
+ values.put(Columns.LAST_ACCESS, System.currentTimeMillis());
+ values.put(Columns.MEDIA_SIZE, size.getValue());
+ values.put(Columns.URI, uri.toString());
+ values.put(Columns.SIZE_IN_BYTES, tempFile.length());
+ long id = db.insert(TABLE, null, values);
+ if (id != -1) {
+ action.execute(uri, id, size, tempFile);
+ db.setTransactionSuccessful();
+ }
+ return id;
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ public void updateLength(long id, long fileSize) {
+ ContentValues values = new ContentValues();
+ values.put(Columns.SIZE_IN_BYTES, fileSize);
+ String[] whereArgs = {
+ String.valueOf(id)
+ };
+ SQLiteDatabase db = getWritableDatabase();
+ db.beginTransaction();
+ try {
+ db.update(TABLE, values, Columns._ID + " = ?", whereArgs);
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ public void delete(Uri uri, Action action) {
+ SQLiteDatabase db = getWritableDatabase();
+ String where = Columns.URI + " = ?";
+ String[] whereArgs = {
+ uri.toString()
+ };
+ Cursor cursor = db.query(TABLE, PROJECTION_CACHED, where, whereArgs, null, null, null);
+ while (cursor.moveToNext()) {
+ long id = cursor.getLong(0);
+ MediaRetriever.MediaSize size = MediaRetriever.MediaSize.fromInteger(cursor.getInt(1));
+ action.execute(uri, id, size, null);
+ }
+ cursor.close();
+ db.beginTransaction();
+ try {
+ db.delete(TABLE, where, whereArgs);
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ public void deleteOldCached(boolean includeThumbnails, long deleteSize, Action action) {
+ String where = includeThumbnails ? null : WHERE_NOT_THUMBNAIL;
+ long lastAccess = 0;
+ SQLiteDatabase db = getWritableDatabase();
+ db.beginTransaction();
+ try {
+ Cursor cursor = db.query(TABLE, PROJECTION_DELETE_OLD, where, null, null, null,
+ Columns.LAST_ACCESS);
+ while (cursor.moveToNext()) {
+ long id = cursor.getLong(0);
+ String uri = cursor.getString(1);
+ MediaSize size = MediaSize.fromInteger(cursor.getInt(2));
+ long length = cursor.getLong(3);
+ long imageLastAccess = cursor.getLong(4);
+
+ if (imageLastAccess != lastAccess && deleteSize < 0) {
+ break; // We've deleted enough.
+ }
+ lastAccess = imageLastAccess;
+ action.execute(Uri.parse(uri), id, size, length);
+ deleteSize -= length;
+ }
+ cursor.close();
+ String[] whereArgs = {
+ String.valueOf(lastAccess),
+ };
+ String whereDelete = includeThumbnails ? WHERE_CLEAR_CACHE : WHERE_CLEAR_CACHE_LARGE;
+ db.delete(TABLE, whereDelete, whereArgs);
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ public long getCacheSize() {
+ return getCacheSize(null);
+ }
+
+ public long getThumbnailCacheSize() {
+ return getCacheSize(WHERE_THUMBNAIL);
+ }
+
+ private long getCacheSize(String where) {
+ SQLiteDatabase db = getReadableDatabase();
+ Cursor cursor = db.query(TABLE, PROJECTION_CACHE_SIZE, where, null, null, null, null);
+ long size = -1;
+ if (cursor.moveToNext()) {
+ size = cursor.getLong(0);
+ }
+ cursor.close();
+ return size;
+ }
+}
diff --git a/src/com/android/photos/data/MediaCacheUtils.java b/src/com/android/photos/data/MediaCacheUtils.java
new file mode 100644
index 000000000..1463d5241
--- /dev/null
+++ b/src/com/android/photos/data/MediaCacheUtils.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2013 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.photos.data;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.CompressFormat;
+import android.graphics.BitmapFactory;
+import android.util.Log;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.BitmapUtils;
+import com.android.gallery3d.data.DecodeUtils;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.util.ThreadPool.CancelListener;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+import com.android.photos.data.MediaRetriever.MediaSize;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+public class MediaCacheUtils {
+ private static final String TAG = MediaCacheUtils.class.getSimpleName();
+ private static int QUALITY = 80;
+ private static final JobContext sJobStub = new JobContext() {
+
+ @Override
+ public boolean isCancelled() {
+ return false;
+ }
+
+ @Override
+ public void setCancelListener(CancelListener listener) {
+ }
+
+ @Override
+ public boolean setMode(int mode) {
+ return true;
+ }
+ };
+
+ private static int mTargetThumbnailSize;
+ private static int mTargetPreviewSize;
+
+ public static void initialize(Context context) {
+ Resources resources = context.getResources();
+ mTargetThumbnailSize = resources.getDimensionPixelSize(R.dimen.size_thumbnail);
+ mTargetPreviewSize = resources.getDimensionPixelSize(R.dimen.size_preview);
+ }
+
+ public static int getTargetSize(MediaSize size) {
+ return (size == MediaSize.Thumbnail) ? mTargetThumbnailSize : mTargetPreviewSize;
+ }
+
+ public static boolean downsample(File inBitmap, MediaSize targetSize, File outBitmap) {
+ if (MediaSize.Original == targetSize) {
+ return false; // MediaCache should use the local path for this.
+ }
+ int size = getTargetSize(targetSize);
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inPreferredConfig = Bitmap.Config.ARGB_8888;
+ // TODO: remove unnecessary job context from DecodeUtils.
+ Bitmap bitmap = DecodeUtils.decodeThumbnail(sJobStub, inBitmap.getPath(), options, size,
+ MediaItem.TYPE_THUMBNAIL);
+ boolean success = (bitmap != null);
+ if (success) {
+ success = writeAndRecycle(bitmap, outBitmap);
+ }
+ return success;
+ }
+
+ public static boolean downsample(Bitmap inBitmap, MediaSize size, File outBitmap) {
+ if (MediaSize.Original == size) {
+ return false; // MediaCache should use the local path for this.
+ }
+ int targetSize = getTargetSize(size);
+ boolean success;
+ if (!needsDownsample(inBitmap, size)) {
+ success = writeAndRecycle(inBitmap, outBitmap);
+ } else {
+ float maxDimension = Math.max(inBitmap.getWidth(), inBitmap.getHeight());
+ float scale = targetSize / maxDimension;
+ int targetWidth = Math.round(scale * inBitmap.getWidth());
+ int targetHeight = Math.round(scale * inBitmap.getHeight());
+ Bitmap scaled = Bitmap.createScaledBitmap(inBitmap, targetWidth, targetHeight, false);
+ success = writeAndRecycle(scaled, outBitmap);
+ inBitmap.recycle();
+ }
+ return success;
+ }
+
+ public static boolean extractImageFromVideo(File inVideo, File outBitmap) {
+ Bitmap bitmap = BitmapUtils.createVideoThumbnail(inVideo.getPath());
+ return writeAndRecycle(bitmap, outBitmap);
+ }
+
+ public static boolean needsDownsample(Bitmap bitmap, MediaSize size) {
+ if (size == MediaSize.Original) {
+ return false;
+ }
+ int targetSize = getTargetSize(size);
+ int maxDimension = Math.max(bitmap.getWidth(), bitmap.getHeight());
+ return maxDimension > (targetSize * 4 / 3);
+ }
+
+ public static boolean writeAndRecycle(Bitmap bitmap, File outBitmap) {
+ boolean success = writeToFile(bitmap, outBitmap);
+ bitmap.recycle();
+ return success;
+ }
+
+ public static boolean writeToFile(Bitmap bitmap, File outBitmap) {
+ boolean success = false;
+ try {
+ FileOutputStream out = new FileOutputStream(outBitmap);
+ success = bitmap.compress(CompressFormat.JPEG, QUALITY, out);
+ out.close();
+ } catch (IOException e) {
+ Log.w(TAG, "Couldn't write bitmap to cache", e);
+ // success is already false
+ }
+ return success;
+ }
+}
diff --git a/src/com/android/photos/data/MediaRetriever.java b/src/com/android/photos/data/MediaRetriever.java
new file mode 100644
index 000000000..f383e5ffa
--- /dev/null
+++ b/src/com/android/photos/data/MediaRetriever.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2013 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.photos.data;
+
+import android.net.Uri;
+
+import java.io.File;
+
+public interface MediaRetriever {
+ public enum MediaSize {
+ TemporaryThumbnail(5), Thumbnail(10), TemporaryPreview(15), Preview(20), Original(30);
+
+ private final int mValue;
+
+ private MediaSize(int value) {
+ mValue = value;
+ }
+
+ public int getValue() {
+ return mValue;
+ }
+
+ static MediaSize fromInteger(int value) {
+ switch (value) {
+ case 10:
+ return MediaSize.Thumbnail;
+ case 20:
+ return MediaSize.Preview;
+ case 30:
+ return MediaSize.Original;
+ default:
+ throw new IllegalArgumentException();
+ }
+ }
+
+ public boolean isBetterThan(MediaSize that) {
+ return mValue > that.mValue;
+ }
+
+ public boolean isTemporary() {
+ return this == TemporaryThumbnail || this == TemporaryPreview;
+ }
+ }
+
+ /**
+ * Returns the local File for the given Uri. If the image is not stored
+ * locally, null should be returned. The image should not be retrieved if it
+ * isn't already available.
+ *
+ * @param contentUri The media URI to search for.
+ * @return The local File of the image if it is available or null if it
+ * isn't.
+ */
+ File getLocalFile(Uri contentUri);
+
+ /**
+ * Returns the fast access image type for a given image size, if supported.
+ * This image should be smaller than size and should be quick to retrieve.
+ * It does not have to obey the expected aspect ratio.
+ *
+ * @param contentUri The original media Uri.
+ * @param size The target size to search for a fast-access image.
+ * @return The fast image type supported for the given image size or null of
+ * no fast image is supported.
+ */
+ MediaSize getFastImageSize(Uri contentUri, MediaSize size);
+
+ /**
+ * Returns a byte array containing the contents of the fast temporary image
+ * for a given image size. For example, a thumbnail may be smaller or of a
+ * different aspect ratio than the generated thumbnail.
+ *
+ * @param contentUri The original media Uri.
+ * @param temporarySize The target media size. Guaranteed to be a MediaSize
+ * for which isTemporary() returns true.
+ * @return A byte array of contents for for the given contentUri and
+ * fastImageType. null can be retrieved if the quick retrieval
+ * fails.
+ */
+ byte[] getTemporaryImage(Uri contentUri, MediaSize temporarySize);
+
+ /**
+ * Retrieves an image and saves it to a file.
+ *
+ * @param contentUri The original media Uri.
+ * @param size The target media size.
+ * @param tempFile The file to write the bitmap to.
+ * @return <code>true</code> on success.
+ */
+ boolean getMedia(Uri contentUri, MediaSize imageSize, File tempFile);
+
+ /**
+ * Normalizes a URI that may have additional parameters. It is fine to
+ * return contentUri. This is executed on the calling thread, so it must be
+ * a fast access operation and cannot depend, for example, on I/O.
+ *
+ * @param contentUri The URI to normalize
+ * @param size The size of the image being requested
+ * @return The normalized URI representation of contentUri.
+ */
+ Uri normalizeUri(Uri contentUri, MediaSize size);
+
+ /**
+ * Normalize the MediaSize for a given URI. Typically the size returned
+ * would be the passed-in size. Some URIs may only have one size used and
+ * should be treaded as Thumbnails, for example. This is executed on the
+ * calling thread, so it must be a fast access operation and cannot depend,
+ * for example, on I/O.
+ *
+ * @param contentUri The URI for the size being normalized.
+ * @param size The size to be normalized.
+ * @return The normalized size of the given URI.
+ */
+ MediaSize normalizeMediaSize(Uri contentUri, MediaSize size);
+}