diff options
Diffstat (limited to 'src/com/android/photos/data/MediaCache.java')
-rw-r--r-- | src/com/android/photos/data/MediaCache.java | 676 |
1 files changed, 0 insertions, 676 deletions
diff --git a/src/com/android/photos/data/MediaCache.java b/src/com/android/photos/data/MediaCache.java deleted file mode 100644 index 0952a4017..000000000 --- a/src/com/android/photos/data/MediaCache.java +++ /dev/null @@ -1,676 +0,0 @@ -/* - * 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; - - boolean isPrefetch(); - } - - private static class NotifyOriginalReady implements NotifyReady { - private final OriginalReady mCallback; - private File mFile; - - public NotifyOriginalReady(OriginalReady callback) { - mCallback = callback; - } - - @Override - public void notifyReady() { - if (mCallback != null) { - mCallback.originalReady(mFile); - } - } - - @Override - public void setFile(File file) { - mFile = file; - } - - @Override - public boolean isPrefetch() { - return mCallback == null; - } - } - - private static class NotifyImageReady implements NotifyReady { - private final ImageReady mCallback; - private InputStream mInputStream; - - public NotifyImageReady(ImageReady callback) { - mCallback = callback; - } - - @Override - public void notifyReady() { - if (mCallback != null) { - mCallback.imageReady(mInputStream); - } - } - - @Override - public void setFile(File file) throws FileNotFoundException { - mInputStream = new FileInputStream(file); - } - - public void setBytes(byte[] bytes) { - mInputStream = new ByteArrayInputStream(bytes); - } - - @Override - public boolean isPrefetch() { - return mCallback == null; - } - } - - /** 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; - } - - public 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); - if (!file.exists()) { - mDatabaseHelper.delete(contentUri, size, mDeleteFile); - file = null; - } - } - 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); - - File cachedFile = getCachedFile(uri, size); - if (cachedFile != null) { - addNotification(complete, cachedFile); - 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); - if (complete.isPrefetch()) { - tasks.add(job); - } else { - int index = tasks.size() - 1; - while (index >= 0 && tasks.get(index).complete.isPrefetch()) { - index--; - } - tasks.add(index + 1, 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) { - File cachedFile = getCachedFile(job.contentUri, job.size); - if (cachedFile != null) { - addNotification(job.complete, cachedFile); - 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); - } -} |