blob: 3d7cc83fb9bbbf0ab82970bd8bc4cddb5d645589 [file] [log] [blame]
/*
* Copyright (C) 2009 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.providers.media;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Comparator;
import java.util.Random;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.media.MiniThumbFile;
import android.media.ThumbnailUtils;
import android.net.Uri;
import android.os.Binder;
import android.os.ParcelFileDescriptor;
import android.provider.BaseColumns;
import android.provider.MediaStore.Images;
import android.provider.MediaStore.Video;
import android.provider.MediaStore.MediaColumns;
import android.provider.MediaStore.Images.ImageColumns;
import android.util.Log;
/**
* Instances of this class are created and put in a queue to be executed sequentially to see if
* it needs to (re)generate the thumbnails.
*/
class MediaThumbRequest {
private static final String TAG = "MediaThumbRequest";
static final int PRIORITY_LOW = 20;
static final int PRIORITY_NORMAL = 10;
static final int PRIORITY_HIGH = 5;
static final int PRIORITY_CRITICAL = 0;
static enum State {WAIT, DONE, CANCEL}
private static final String[] THUMB_PROJECTION = new String[] {
BaseColumns._ID // 0
};
ContentResolver mCr;
String mPath;
long mRequestTime = System.currentTimeMillis();
int mCallingPid = Binder.getCallingPid();
long mGroupId;
int mPriority;
Uri mUri;
Uri mThumbUri;
String mOrigColumnName;
boolean mIsVideo;
long mOrigId;
State mState = State.WAIT;
long mMagic;
private static final Random sRandom = new Random();
static Comparator<MediaThumbRequest> getComparator() {
return new Comparator<MediaThumbRequest>() {
public int compare(MediaThumbRequest r1, MediaThumbRequest r2) {
if (r1.mPriority != r2.mPriority) {
return r1.mPriority < r2.mPriority ? -1 : 1;
}
return r1.mRequestTime == r2.mRequestTime ? 0 :
r1.mRequestTime < r2.mRequestTime ? -1 : 1;
}
};
}
MediaThumbRequest(ContentResolver cr, String path, Uri uri, int priority, long magic) {
mCr = cr;
mPath = path;
mPriority = priority;
mMagic = magic;
mUri = uri;
mIsVideo = "video".equals(uri.getPathSegments().get(1));
mOrigId = ContentUris.parseId(uri);
mThumbUri = mIsVideo
? Video.Thumbnails.EXTERNAL_CONTENT_URI
: Images.Thumbnails.EXTERNAL_CONTENT_URI;
mOrigColumnName = mIsVideo
? Video.Thumbnails.VIDEO_ID
: Images.Thumbnails.IMAGE_ID;
// Only requests from Thumbnail API has this group_id parameter. In other cases,
// mGroupId will always be zero and can't be canceled due to pid mismatch.
String groupIdParam = uri.getQueryParameter("group_id");
if (groupIdParam != null) {
mGroupId = Long.parseLong(groupIdParam);
}
}
Uri updateDatabase(Bitmap thumbnail) {
Cursor c = mCr.query(mThumbUri, THUMB_PROJECTION,
mOrigColumnName+ " = " + mOrigId, null, null);
if (c == null) return null;
try {
if (c.moveToFirst()) {
return ContentUris.withAppendedId(mThumbUri, c.getLong(0));
}
} finally {
if (c != null) c.close();
}
ContentValues values = new ContentValues(4);
values.put(Images.Thumbnails.KIND, Images.Thumbnails.MINI_KIND);
values.put(mOrigColumnName, mOrigId);
values.put(Images.Thumbnails.WIDTH, thumbnail.getWidth());
values.put(Images.Thumbnails.HEIGHT, thumbnail.getHeight());
try {
return mCr.insert(mThumbUri, values);
} catch (Exception ex) {
Log.w(TAG, ex);
return null;
}
}
/**
* Check if the corresponding thumbnail and mini-thumb have been created
* for the given uri. This method creates both of them if they do not
* exist yet or have been changed since last check. After thumbnails are
* created, MINI_KIND thumbnail is stored in JPEG file and MICRO_KIND
* thumbnail is stored in a random access file (MiniThumbFile).
*
* @throws IOException
*/
void execute() throws IOException {
MiniThumbFile miniThumbFile = MiniThumbFile.instance(mUri);
long magic = mMagic;
if (magic != 0) {
long fileMagic = miniThumbFile.getMagic(mOrigId);
if (fileMagic == magic) {
Cursor c = null;
ParcelFileDescriptor pfd = null;
// Clear calling identity as we may be handling an IPC.
final long identity = Binder.clearCallingIdentity();
try {
c = mCr.query(mThumbUri, THUMB_PROJECTION,
mOrigColumnName + " = " + mOrigId, null, null);
if (c != null && c.moveToFirst()) {
pfd = mCr.openFileDescriptor(
mThumbUri.buildUpon().appendPath(c.getString(0)).build(), "r");
}
} catch (IOException ex) {
// MINI_THUMBNAIL not exists, ignore the exception and generate one.
} finally {
Binder.restoreCallingIdentity(identity);
if (c != null) c.close();
if (pfd != null) {
pfd.close();
return;
}
}
}
}
// If we can't retrieve the thumbnail, first check if there is one
// embedded in the EXIF data. If not, or it's not big enough,
// decompress the full size image.
Bitmap bitmap = null;
if (mPath != null) {
if (mIsVideo) {
bitmap = ThumbnailUtils.createVideoThumbnail(mPath,
Video.Thumbnails.MINI_KIND);
} else {
bitmap = ThumbnailUtils.createImageThumbnail(mPath,
Images.Thumbnails.MINI_KIND);
}
if (bitmap == null) {
Log.w(TAG, "Can't create mini thumbnail for " + mPath);
return;
}
Uri uri = updateDatabase(bitmap);
if (uri != null) {
OutputStream thumbOut = mCr.openOutputStream(uri);
bitmap.compress(Bitmap.CompressFormat.JPEG, 85, thumbOut);
thumbOut.close();
}
}
bitmap = ThumbnailUtils.extractThumbnail(bitmap,
ThumbnailUtils.TARGET_SIZE_MICRO_THUMBNAIL,
ThumbnailUtils.TARGET_SIZE_MICRO_THUMBNAIL,
ThumbnailUtils.OPTIONS_RECYCLE_INPUT);
if (bitmap != null) {
ByteArrayOutputStream miniOutStream = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.JPEG, 75, miniOutStream);
bitmap.recycle();
byte [] data = null;
try {
miniOutStream.close();
data = miniOutStream.toByteArray();
} catch (java.io.IOException ex) {
Log.e(TAG, "got exception ex " + ex);
}
// We may consider retire this proprietary format, after all it's size is only
// 128 x 128 at most, which is still reasonable to be stored in database.
// Gallery application can use the MINI_THUMB_MAGIC value to determine if it's
// time to query and fetch by using Cursor.getBlob
if (data != null) {
// make a new magic number since things are out of sync
do {
magic = sRandom.nextLong();
} while (magic == 0);
miniThumbFile.saveMiniThumbToFile(data, mOrigId, magic);
ContentValues values = new ContentValues();
// both video/images table use the same column name "mini_thumb_magic"
values.put(ImageColumns.MINI_THUMB_MAGIC, magic);
try {
mCr.update(mUri, values, null, null);
mMagic = magic;
} catch (java.lang.IllegalStateException ex) {
Log.e(TAG, "got exception while updating database " + ex);
}
}
} else {
Log.w(TAG, "can't create bitmap for thumbnail.");
}
miniThumbFile.deactivate();
}
}