Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit fe2aa4ba authored by George Mount's avatar George Mount Committed by Android (Google) Code Review
Browse files

Merge "Add initial implementation of MediaCache." into gb-ub-photos-bryce

parents 3f312857 49508252
Loading
Loading
Loading
Loading
+2 −3
Original line number Diff line number Diff line
@@ -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) {
+109 −0
Original line number Diff line number Diff line
/*
 * 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/"));
    }
}
+649 −0

File added.

Preview size limit exceeded, changes collapsed.

+272 −0
Original line number Diff line number Diff line
/*
 * 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;
    }
}
+139 −0
Original line number Diff line number Diff line
/*
 * 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;
    }
}
Loading