am 830207ca: Merge change 25635 into eclair

Merge commit '830207cab4047431d7a38443531a3a912bfecf3a' into eclair-plus-aosp

* commit '830207cab4047431d7a38443531a3a912bfecf3a':
  Add new thumbnail API.
This commit is contained in:
Ray Chen
2009-09-23 11:38:06 -07:00
committed by Android Git Automerger
5 changed files with 1142 additions and 32 deletions

View File

@ -114624,6 +114624,25 @@
<parameter name="volumeName" type="java.lang.String">
</parameter>
</method>
<method name="getThumbnail"
return="android.graphics.Bitmap"
abstract="false"
native="false"
synchronized="false"
static="true"
final="false"
deprecated="not deprecated"
visibility="public"
>
<parameter name="cr" type="android.content.ContentResolver">
</parameter>
<parameter name="origId" type="long">
</parameter>
<parameter name="kind" type="int">
</parameter>
<parameter name="options" type="android.graphics.BitmapFactory.Options">
</parameter>
</method>
<method name="query"
return="android.database.Cursor"
abstract="false"
@ -114787,6 +114806,17 @@
visibility="public"
>
</field>
<field name="THUMB_DATA"
type="java.lang.String"
transient="false"
volatile="false"
value="&quot;thumb_data&quot;"
static="true"
final="true"
deprecated="not deprecated"
visibility="public"
>
</field>
<field name="WIDTH"
type="java.lang.String"
transient="false"
@ -115005,6 +115035,176 @@
>
</field>
</class>
<class name="MediaStore.Video.Thumbnails"
extends="java.lang.Object"
abstract="false"
static="true"
final="false"
deprecated="not deprecated"
visibility="public"
>
<implements name="android.provider.BaseColumns">
</implements>
<constructor name="MediaStore.Video.Thumbnails"
type="android.provider.MediaStore.Video.Thumbnails"
static="false"
final="false"
deprecated="not deprecated"
visibility="public"
>
</constructor>
<method name="getContentUri"
return="android.net.Uri"
abstract="false"
native="false"
synchronized="false"
static="true"
final="false"
deprecated="not deprecated"
visibility="public"
>
<parameter name="volumeName" type="java.lang.String">
</parameter>
</method>
<method name="getThumbnail"
return="android.graphics.Bitmap"
abstract="false"
native="false"
synchronized="false"
static="true"
final="false"
deprecated="not deprecated"
visibility="public"
>
<parameter name="cr" type="android.content.ContentResolver">
</parameter>
<parameter name="origId" type="long">
</parameter>
<parameter name="kind" type="int">
</parameter>
<parameter name="options" type="android.graphics.BitmapFactory.Options">
</parameter>
</method>
<field name="DATA"
type="java.lang.String"
transient="false"
volatile="false"
value="&quot;_data&quot;"
static="true"
final="true"
deprecated="not deprecated"
visibility="public"
>
</field>
<field name="DEFAULT_SORT_ORDER"
type="java.lang.String"
transient="false"
volatile="false"
value="&quot;video_id ASC&quot;"
static="true"
final="true"
deprecated="not deprecated"
visibility="public"
>
</field>
<field name="EXTERNAL_CONTENT_URI"
type="android.net.Uri"
transient="false"
volatile="false"
static="true"
final="true"
deprecated="not deprecated"
visibility="public"
>
</field>
<field name="FULL_SCREEN_KIND"
type="int"
transient="false"
volatile="false"
value="2"
static="true"
final="true"
deprecated="not deprecated"
visibility="public"
>
</field>
<field name="HEIGHT"
type="java.lang.String"
transient="false"
volatile="false"
value="&quot;height&quot;"
static="true"
final="true"
deprecated="not deprecated"
visibility="public"
>
</field>
<field name="INTERNAL_CONTENT_URI"
type="android.net.Uri"
transient="false"
volatile="false"
static="true"
final="true"
deprecated="not deprecated"
visibility="public"
>
</field>
<field name="KIND"
type="java.lang.String"
transient="false"
volatile="false"
value="&quot;kind&quot;"
static="true"
final="true"
deprecated="not deprecated"
visibility="public"
>
</field>
<field name="MICRO_KIND"
type="int"
transient="false"
volatile="false"
value="3"
static="true"
final="true"
deprecated="not deprecated"
visibility="public"
>
</field>
<field name="MINI_KIND"
type="int"
transient="false"
volatile="false"
value="1"
static="true"
final="true"
deprecated="not deprecated"
visibility="public"
>
</field>
<field name="VIDEO_ID"
type="java.lang.String"
transient="false"
volatile="false"
value="&quot;video_id&quot;"
static="true"
final="true"
deprecated="not deprecated"
visibility="public"
>
</field>
<field name="WIDTH"
type="java.lang.String"
transient="false"
volatile="false"
value="&quot;width&quot;"
static="true"
final="true"
deprecated="not deprecated"
visibility="public"
>
</field>
</class>
<interface name="MediaStore.Video.VideoColumns"
abstract="true"
static="true"

View File

@ -23,11 +23,15 @@ import android.content.ContentValues;
import android.content.ContentUris;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.database.sqlite.SQLiteException;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.media.MiniThumbFile;
import android.media.ThumbnailUtil;
import android.net.Uri;
import android.os.Environment;
import android.os.ParcelFileDescriptor;
import android.util.Log;
import java.io.FileInputStream;
@ -42,8 +46,7 @@ import java.text.Collator;
* The Media provider contains meta data for all available media on both internal
* and external storage devices.
*/
public final class MediaStore
{
public final class MediaStore {
private final static String TAG = "MediaStore";
public static final String AUTHORITY = "media";
@ -226,11 +229,129 @@ public final class MediaStore
public static final String MIME_TYPE = "mime_type";
}
/**
* This class is used internally by Images.Thumbnails and Video.Thumbnails, it's not intended
* to be accessed elsewhere.
*/
private static class InternalThumbnails implements BaseColumns {
private static final int MINI_KIND = 1;
private static final int FULL_SCREEN_KIND = 2;
private static final int MICRO_KIND = 3;
private static final String[] PROJECTION = new String[] {_ID, MediaColumns.DATA};
/**
* This method ensure thumbnails associated with origId are generated and decode the byte
* stream from database (MICRO_KIND) or file (MINI_KIND).
*
* Special optimization has been done to avoid further IPC communication for MICRO_KIND
* thumbnails.
*
* @param cr ContentResolver
* @param origId original image or video id
* @param kind could be MINI_KIND or MICRO_KIND
* @param options this is only used for MINI_KIND when decoding the Bitmap
* @param baseUri the base URI of requested thumbnails
* @return Bitmap bitmap of specified thumbnail kind
*/
static Bitmap getThumbnail(ContentResolver cr, long origId, int kind,
BitmapFactory.Options options, Uri baseUri, boolean isVideo) {
Bitmap bitmap = null;
String filePath = null;
// some optimization for MICRO_KIND: if the magic is non-zero, we don't bother
// querying MediaProvider and simply return thumbnail.
if (kind == MICRO_KIND) {
MiniThumbFile thumbFile = MiniThumbFile.instance(baseUri);
if (thumbFile.getMagic(origId) != 0) {
byte[] data = new byte[MiniThumbFile.BYTES_PER_MINTHUMB];
if (thumbFile.getMiniThumbFromFile(origId, data) != null) {
bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
if (bitmap == null) {
Log.w(TAG, "couldn't decode byte array.");
}
}
return bitmap;
}
}
Cursor c = null;
try {
Uri blockingUri = baseUri.buildUpon().appendQueryParameter("blocking", "1")
.appendQueryParameter("orig_id", String.valueOf(origId)).build();
c = cr.query(blockingUri, PROJECTION, null, null, null);
// This happens when original image/video doesn't exist.
if (c == null) return null;
// Assuming thumbnail has been generated, at least original image exists.
if (kind == MICRO_KIND) {
MiniThumbFile thumbFile = MiniThumbFile.instance(baseUri);
byte[] data = new byte[MiniThumbFile.BYTES_PER_MINTHUMB];
if (thumbFile.getMiniThumbFromFile(origId, data) != null) {
bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
if (bitmap == null) {
Log.w(TAG, "couldn't decode byte array.");
}
}
} else if (kind == MINI_KIND) {
if (c.moveToFirst()) {
ParcelFileDescriptor pfdInput;
Uri thumbUri = null;
try {
long thumbId = c.getLong(0);
filePath = c.getString(1);
thumbUri = ContentUris.withAppendedId(baseUri, thumbId);
pfdInput = cr.openFileDescriptor(thumbUri, "r");
bitmap = BitmapFactory.decodeFileDescriptor(
pfdInput.getFileDescriptor(), null, options);
pfdInput.close();
} catch (FileNotFoundException ex) {
Log.e(TAG, "couldn't open thumbnail " + thumbUri + "; " + ex);
} catch (IOException ex) {
Log.e(TAG, "couldn't open thumbnail " + thumbUri + "; " + ex);
} catch (OutOfMemoryError ex) {
Log.e(TAG, "failed to allocate memory for thumbnail "
+ thumbUri + "; " + ex);
}
}
} else {
throw new IllegalArgumentException("Unsupported kind: " + kind);
}
// We probably run out of space, so create the thumbnail in memory.
if (bitmap == null) {
Log.v(TAG, "We probably run out of space, so create the thumbnail in memory.");
int targetSize = kind == MINI_KIND ? ThumbnailUtil.THUMBNAIL_TARGET_SIZE :
ThumbnailUtil.MINI_THUMB_TARGET_SIZE;
int maxPixelNum = kind == MINI_KIND ? ThumbnailUtil.THUMBNAIL_MAX_NUM_PIXELS :
ThumbnailUtil.MINI_THUMB_MAX_NUM_PIXELS;
Uri uri = Uri.parse(
baseUri.buildUpon().appendPath(String.valueOf(origId))
.toString().replaceFirst("thumbnails", "media"));
if (isVideo) {
c = cr.query(uri, PROJECTION, null, null, null);
if (c != null && c.moveToFirst()) {
bitmap = ThumbnailUtil.createVideoThumbnail(c.getString(1));
if (kind == MICRO_KIND) {
bitmap = ThumbnailUtil.extractMiniThumb(bitmap,
targetSize, targetSize, ThumbnailUtil.RECYCLE_INPUT);
}
}
} else {
bitmap = ThumbnailUtil.makeBitmap(targetSize, maxPixelNum, uri, cr);
}
}
} catch (SQLiteException ex) {
Log.w(TAG, ex);
} finally {
if (c != null) c.close();
}
return bitmap;
}
}
/**
* Contains meta data for all available images.
*/
public static final class Images
{
public static final class Images {
public interface ImageColumns extends MediaColumns {
/**
* The description of the image
@ -298,21 +419,18 @@ public final class MediaStore
}
public static final class Media implements ImageColumns {
public static final Cursor query(ContentResolver cr, Uri uri, String[] projection)
{
public static final Cursor query(ContentResolver cr, Uri uri, String[] projection) {
return cr.query(uri, projection, null, null, DEFAULT_SORT_ORDER);
}
public static final Cursor query(ContentResolver cr, Uri uri, String[] projection,
String where, String orderBy)
{
String where, String orderBy) {
return cr.query(uri, projection, where,
null, orderBy == null ? DEFAULT_SORT_ORDER : orderBy);
}
public static final Cursor query(ContentResolver cr, Uri uri, String[] projection,
String selection, String [] selectionArgs, String orderBy)
{
String selection, String [] selectionArgs, String orderBy) {
return cr.query(uri, projection, selection,
selectionArgs, orderBy == null ? DEFAULT_SORT_ORDER : orderBy);
}
@ -326,8 +444,7 @@ public final class MediaStore
* @throws IOException
*/
public static final Bitmap getBitmap(ContentResolver cr, Uri url)
throws FileNotFoundException, IOException
{
throws FileNotFoundException, IOException {
InputStream input = cr.openInputStream(url);
Bitmap bitmap = BitmapFactory.decodeStream(input);
input.close();
@ -344,9 +461,8 @@ public final class MediaStore
* @return The URL to the newly created image
* @throws FileNotFoundException
*/
public static final String insertImage(ContentResolver cr, String imagePath, String name,
String description) throws FileNotFoundException
{
public static final String insertImage(ContentResolver cr, String imagePath,
String name, String description) throws FileNotFoundException {
// Check if file exists with a FileInputStream
FileInputStream stream = new FileInputStream(imagePath);
try {
@ -415,8 +531,7 @@ public final class MediaStore
* for any reason.
*/
public static final String insertImage(ContentResolver cr, Bitmap source,
String title, String description)
{
String title, String description) {
ContentValues values = new ContentValues();
values.put(Images.Media.TITLE, title);
values.put(Images.Media.DESCRIPTION, description);
@ -425,8 +540,7 @@ public final class MediaStore
Uri url = null;
String stringUrl = null; /* value to be returned */
try
{
try {
url = cr.insert(EXTERNAL_CONTENT_URI, values);
if (source != null) {
@ -498,25 +612,45 @@ public final class MediaStore
public static final String DEFAULT_SORT_ORDER = ImageColumns.BUCKET_DISPLAY_NAME;
}
public static class Thumbnails implements BaseColumns
{
public static final Cursor query(ContentResolver cr, Uri uri, String[] projection)
{
/**
* This class allows developers to query and get two kinds of thumbnails:
* MINI_KIND: 512 x 384 thumbnail
* MICRO_KIND: 96 x 96 thumbnail
*/
public static class Thumbnails implements BaseColumns {
public static final Cursor query(ContentResolver cr, Uri uri, String[] projection) {
return cr.query(uri, projection, null, null, DEFAULT_SORT_ORDER);
}
public static final Cursor queryMiniThumbnails(ContentResolver cr, Uri uri, int kind, String[] projection)
{
public static final Cursor queryMiniThumbnails(ContentResolver cr, Uri uri, int kind,
String[] projection) {
return cr.query(uri, projection, "kind = " + kind, null, DEFAULT_SORT_ORDER);
}
public static final Cursor queryMiniThumbnail(ContentResolver cr, long origId, int kind, String[] projection)
{
public static final Cursor queryMiniThumbnail(ContentResolver cr, long origId, int kind,
String[] projection) {
return cr.query(EXTERNAL_CONTENT_URI, projection,
IMAGE_ID + " = " + origId + " AND " + KIND + " = " +
kind, null, null);
}
/**
* This method checks if the thumbnails of the specified image (origId) has been created.
* It will be blocked until the thumbnails are generated.
*
* @param cr ContentResolver used to dispatch queries to MediaProvider.
* @param origId Original image id associated with thumbnail of interest.
* @param kind The type of thumbnail to fetch. Should be either MINI_KIND or MICRO_KIND.
* @param options this is only used for MINI_KIND when decoding the Bitmap
* @return A Bitmap instance. It could be null if the original image
* associated with origId doesn't exist or memory is not enough.
*/
public static Bitmap getThumbnail(ContentResolver cr, long origId, int kind,
BitmapFactory.Options options) {
return InternalThumbnails.getThumbnail(cr, origId, kind, options,
EXTERNAL_CONTENT_URI, false);
}
/**
* Get the content:// style URI for the image media table on the
* given volume.
@ -568,6 +702,11 @@ public final class MediaStore
public static final int MINI_KIND = 1;
public static final int FULL_SCREEN_KIND = 2;
public static final int MICRO_KIND = 3;
/**
* The blob raw data of thumbnail
* <P>Type: DATA STREAM</P>
*/
public static final String THUMB_DATA = "thumb_data";
/**
* The width of the thumbnal
@ -1259,8 +1398,7 @@ public final class MediaStore
*/
public static final String DEFAULT_SORT_ORDER = MediaColumns.DISPLAY_NAME;
public static final Cursor query(ContentResolver cr, Uri uri, String[] projection)
{
public static final Cursor query(ContentResolver cr, Uri uri, String[] projection) {
return cr.query(uri, projection, null, null, DEFAULT_SORT_ORDER);
}
@ -1405,6 +1543,95 @@ public final class MediaStore
*/
public static final String DEFAULT_SORT_ORDER = TITLE;
}
/**
* This class allows developers to query and get two kinds of thumbnails:
* MINI_KIND: 512 x 384 thumbnail
* MICRO_KIND: 96 x 96 thumbnail
*
*/
public static class Thumbnails implements BaseColumns {
/**
* This method checks if the thumbnails of the specified image (origId) has been created.
* It will be blocked until the thumbnails are generated.
*
* @param cr ContentResolver used to dispatch queries to MediaProvider.
* @param origId Original image id associated with thumbnail of interest.
* @param kind The type of thumbnail to fetch. Should be either MINI_KIND or MICRO_KIND
* @param options this is only used for MINI_KIND when decoding the Bitmap
* @return A Bitmap instance. It could be null if the original image associated with
* origId doesn't exist or memory is not enough.
*/
public static Bitmap getThumbnail(ContentResolver cr, long origId, int kind,
BitmapFactory.Options options) {
return InternalThumbnails.getThumbnail(cr, origId, kind, options,
EXTERNAL_CONTENT_URI, true);
}
/**
* Get the content:// style URI for the image media table on the
* given volume.
*
* @param volumeName the name of the volume to get the URI for
* @return the URI to the image media table on the given volume
*/
public static Uri getContentUri(String volumeName) {
return Uri.parse(CONTENT_AUTHORITY_SLASH + volumeName +
"/video/thumbnails");
}
/**
* The content:// style URI for the internal storage.
*/
public static final Uri INTERNAL_CONTENT_URI =
getContentUri("internal");
/**
* The content:// style URI for the "primary" external storage
* volume.
*/
public static final Uri EXTERNAL_CONTENT_URI =
getContentUri("external");
/**
* The default sort order for this table
*/
public static final String DEFAULT_SORT_ORDER = "video_id ASC";
/**
* The data stream for the thumbnail
* <P>Type: DATA STREAM</P>
*/
public static final String DATA = "_data";
/**
* The original image for the thumbnal
* <P>Type: INTEGER (ID from Video table)</P>
*/
public static final String VIDEO_ID = "video_id";
/**
* The kind of the thumbnail
* <P>Type: INTEGER (One of the values below)</P>
*/
public static final String KIND = "kind";
public static final int MINI_KIND = 1;
public static final int FULL_SCREEN_KIND = 2;
public static final int MICRO_KIND = 3;
/**
* The width of the thumbnal
* <P>Type: INTEGER (long)</P>
*/
public static final String WIDTH = "width";
/**
* The height of the thumbnail
* <P>Type: INTEGER (long)</P>
*/
public static final String HEIGHT = "height";
}
}
/**

View File

@ -486,6 +486,8 @@ public class MediaScanner
}
public void scanFile(String path, long lastModified, long fileSize) {
// This is the callback funtion from native codes.
// Log.v(TAG, "scanFile: "+path);
doScanFile(path, null, lastModified, fileSize, false);
}

View File

@ -0,0 +1,280 @@
/*
* 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 android.media;
import android.graphics.Bitmap;
import android.media.ThumbnailUtil;
import android.net.Uri;
import android.os.Environment;
import android.util.Log;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.util.Hashtable;
/**
* This class handles the mini-thumb file. A mini-thumb file consists
* of blocks, indexed by id. Each block has BYTES_PER_MINTHUMB bytes in the
* following format:
*
* 1 byte status (0 = empty, 1 = mini-thumb available)
* 8 bytes magic (a magic number to match what's in the database)
* 4 bytes data length (LEN)
* LEN bytes jpeg data
* (the remaining bytes are unused)
*
* @hide This file is shared between MediaStore and MediaProvider and should remained internal use
* only.
*/
public class MiniThumbFile {
public static final int THUMBNAIL_TARGET_SIZE = 320;
public static final int MINI_THUMB_TARGET_SIZE = 96;
public static final int THUMBNAIL_MAX_NUM_PIXELS = 512 * 384;
public static final int MINI_THUMB_MAX_NUM_PIXELS = 128 * 128;
public static final int UNCONSTRAINED = -1;
private static final String TAG = "MiniThumbFile";
private static final int MINI_THUMB_DATA_FILE_VERSION = 3;
public static final int BYTES_PER_MINTHUMB = 10000;
private static final int HEADER_SIZE = 1 + 8 + 4;
private Uri mUri;
private RandomAccessFile mMiniThumbFile;
private FileChannel mChannel;
private static Hashtable<String, MiniThumbFile> sThumbFiles =
new Hashtable<String, MiniThumbFile>();
/**
* We store different types of thumbnails in different files. To remain backward compatibility,
* we should hashcode of content://media/external/images/media remains the same.
*/
public static synchronized void reset() {
sThumbFiles.clear();
}
public static synchronized MiniThumbFile instance(Uri uri) {
String type = uri.getPathSegments().get(1);
MiniThumbFile file = sThumbFiles.get(type);
// Log.v(TAG, "get minithumbfile for type: "+type);
if (file == null) {
file = new MiniThumbFile(
Uri.parse("content://media/external/" + type + "/media"));
sThumbFiles.put(type, file);
}
return file;
}
private String randomAccessFilePath(int version) {
String directoryName =
Environment.getExternalStorageDirectory().toString()
+ "/DCIM/.thumbnails";
return directoryName + "/.thumbdata" + version + "-" + mUri.hashCode();
}
private void removeOldFile() {
String oldPath = randomAccessFilePath(MINI_THUMB_DATA_FILE_VERSION - 1);
File oldFile = new File(oldPath);
if (oldFile.exists()) {
try {
oldFile.delete();
} catch (SecurityException ex) {
// ignore
}
}
}
private RandomAccessFile miniThumbDataFile() {
if (mMiniThumbFile == null) {
removeOldFile();
String path = randomAccessFilePath(MINI_THUMB_DATA_FILE_VERSION);
File directory = new File(path).getParentFile();
if (!directory.isDirectory()) {
if (!directory.mkdirs()) {
Log.e(TAG, "Unable to create .thumbnails directory "
+ directory.toString());
}
}
File f = new File(path);
try {
mMiniThumbFile = new RandomAccessFile(f, "rw");
} catch (IOException ex) {
// Open as read-only so we can at least read the existing
// thumbnails.
try {
mMiniThumbFile = new RandomAccessFile(f, "r");
} catch (IOException ex2) {
// ignore exception
}
}
mChannel = mMiniThumbFile.getChannel();
}
return mMiniThumbFile;
}
public MiniThumbFile(Uri uri) {
mUri = uri;
}
public synchronized void deactivate() {
if (mMiniThumbFile != null) {
try {
mMiniThumbFile.close();
mMiniThumbFile = null;
} catch (IOException ex) {
// ignore exception
}
}
}
// Get the magic number for the specified id in the mini-thumb file.
// Returns 0 if the magic is not available.
public long getMagic(long id) {
// check the mini thumb file for the right data. Right is
// defined as having the right magic number at the offset
// reserved for this "id".
RandomAccessFile r = miniThumbDataFile();
if (r != null) {
long pos = id * BYTES_PER_MINTHUMB;
FileLock lock = null;
try {
lock = mChannel.lock();
// check that we can read the following 9 bytes
// (1 for the "status" and 8 for the long)
if (r.length() >= pos + 1 + 8) {
r.seek(pos);
if (r.readByte() == 1) {
long fileMagic = r.readLong();
return fileMagic;
}
}
} catch (IOException ex) {
Log.v(TAG, "Got exception checking file magic: ", ex);
} catch (RuntimeException ex) {
// Other NIO related exception like disk full, read only channel..etc
Log.e(TAG, "Got exception when reading magic, id = " + id +
", disk full or mount read-only? " + ex.getClass());
} finally {
try {
if (lock != null) lock.release();
}
catch (IOException ex) {
// ignore it.
}
}
}
return 0;
}
public void saveMiniThumbToFile(Bitmap bitmap, long id, long magic)
throws IOException {
byte[] data = ThumbnailUtil.miniThumbData(bitmap);
saveMiniThumbToFile(data, id, magic);
}
public void saveMiniThumbToFile(byte[] data, long id, long magic)
throws IOException {
RandomAccessFile r = miniThumbDataFile();
if (r == null) return;
long pos = id * BYTES_PER_MINTHUMB;
FileLock lock = null;
try {
lock = mChannel.lock();
if (data != null) {
if (data.length > BYTES_PER_MINTHUMB - HEADER_SIZE) {
// not enough space to store it.
return;
}
r.seek(pos);
r.writeByte(0); // we have no data in this slot
// if magic is 0 then leave it alone
if (magic == 0) {
r.skipBytes(8);
} else {
r.writeLong(magic);
}
r.writeInt(data.length);
r.write(data);
r.seek(pos);
r.writeByte(1); // we have data in this slot
mChannel.force(true);
}
} catch (IOException ex) {
Log.e(TAG, "couldn't save mini thumbnail data for "
+ id + "; ", ex);
throw ex;
} catch (RuntimeException ex) {
// Other NIO related exception like disk full, read only channel..etc
Log.e(TAG, "couldn't save mini thumbnail data for "
+ id + "; disk full or mount read-only? " + ex.getClass());
} finally {
try {
if (lock != null) lock.release();
}
catch (IOException ex) {
// ignore it.
}
}
}
/**
* Gallery app can use this method to retrieve mini-thumbnail. Full size
* images share the same IDs with their corresponding thumbnails.
*
* @param id the ID of the image (same of full size image).
* @param data the buffer to store mini-thumbnail.
*/
public byte [] getMiniThumbFromFile(long id, byte [] data) {
RandomAccessFile r = miniThumbDataFile();
if (r == null) return null;
long pos = id * BYTES_PER_MINTHUMB;
FileLock lock = null;
try {
lock = mChannel.lock();
r.seek(pos);
if (r.readByte() == 1) {
long magic = r.readLong();
int length = r.readInt();
int got = r.read(data, 0, length);
if (got != length) return null;
return data;
} else {
return null;
}
} catch (IOException ex) {
Log.w(TAG, "got exception when reading thumbnail: " + ex);
return null;
} catch (RuntimeException ex) {
// Other NIO related exception like disk full, read only channel..etc
Log.e(TAG, "Got exception when reading thumbnail, id = " + id +
", disk full or mount read-only? " + ex.getClass());
} finally {
try {
if (lock != null) lock.release();
}
catch (IOException ex) {
// ignore it.
}
}
return null;
}
}

View File

@ -0,0 +1,401 @@
/*
* 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 android.media;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.util.Log;
import android.content.ContentResolver;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.media.MediaMetadataRetriever;
import java.io.ByteArrayOutputStream;
import java.io.FileDescriptor;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
/**
* Thumbnail generation routines for media provider. This class should only be used internaly.
* {@hide} THIS IS NOT FOR PUBLIC API.
*/
public class ThumbnailUtil {
private static final String TAG = "ThumbnailUtil";
//Whether we should recycle the input (unless the output is the input).
public static final boolean RECYCLE_INPUT = true;
public static final boolean NO_RECYCLE_INPUT = false;
public static final boolean ROTATE_AS_NEEDED = true;
public static final boolean NO_ROTATE = false;
public static final boolean USE_NATIVE = true;
public static final boolean NO_NATIVE = false;
public static final int THUMBNAIL_TARGET_SIZE = 320;
public static final int MINI_THUMB_TARGET_SIZE = 96;
public static final int THUMBNAIL_MAX_NUM_PIXELS = 512 * 384;
public static final int MINI_THUMB_MAX_NUM_PIXELS = 128 * 128;
public static final int UNCONSTRAINED = -1;
// Returns Options that set the native alloc flag for Bitmap decode.
public static BitmapFactory.Options createNativeAllocOptions() {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inNativeAlloc = true;
return options;
}
/**
* Make a bitmap from a given Uri.
*
* @param uri
*/
public static Bitmap makeBitmap(int minSideLength, int maxNumOfPixels,
Uri uri, ContentResolver cr) {
return makeBitmap(minSideLength, maxNumOfPixels, uri, cr,
NO_NATIVE);
}
/*
* Compute the sample size as a function of minSideLength
* and maxNumOfPixels.
* minSideLength is used to specify that minimal width or height of a
* bitmap.
* maxNumOfPixels is used to specify the maximal size in pixels that is
* tolerable in terms of memory usage.
*
* The function returns a sample size based on the constraints.
* Both size and minSideLength can be passed in as IImage.UNCONSTRAINED,
* which indicates no care of the corresponding constraint.
* The functions prefers returning a sample size that
* generates a smaller bitmap, unless minSideLength = IImage.UNCONSTRAINED.
*
* Also, the function rounds up the sample size to a power of 2 or multiple
* of 8 because BitmapFactory only honors sample size this way.
* For example, BitmapFactory downsamples an image by 2 even though the
* request is 3. So we round up the sample size to avoid OOM.
*/
public static int computeSampleSize(BitmapFactory.Options options,
int minSideLength, int maxNumOfPixels) {
int initialSize = computeInitialSampleSize(options, minSideLength,
maxNumOfPixels);
int roundedSize;
if (initialSize <= 8 ) {
roundedSize = 1;
while (roundedSize < initialSize) {
roundedSize <<= 1;
}
} else {
roundedSize = (initialSize + 7) / 8 * 8;
}
return roundedSize;
}
private static int computeInitialSampleSize(BitmapFactory.Options options,
int minSideLength, int maxNumOfPixels) {
double w = options.outWidth;
double h = options.outHeight;
int lowerBound = (maxNumOfPixels == UNCONSTRAINED) ? 1 :
(int) Math.ceil(Math.sqrt(w * h / maxNumOfPixels));
int upperBound = (minSideLength == UNCONSTRAINED) ? 128 :
(int) Math.min(Math.floor(w / minSideLength),
Math.floor(h / minSideLength));
if (upperBound < lowerBound) {
// return the larger one when there is no overlapping zone.
return lowerBound;
}
if ((maxNumOfPixels == UNCONSTRAINED) &&
(minSideLength == UNCONSTRAINED)) {
return 1;
} else if (minSideLength == UNCONSTRAINED) {
return lowerBound;
} else {
return upperBound;
}
}
public static Bitmap makeBitmap(int minSideLength, int maxNumOfPixels,
Uri uri, ContentResolver cr, boolean useNative) {
ParcelFileDescriptor input = null;
try {
input = cr.openFileDescriptor(uri, "r");
BitmapFactory.Options options = null;
if (useNative) {
options = createNativeAllocOptions();
}
return makeBitmap(minSideLength, maxNumOfPixels, uri, cr, input,
options);
} catch (IOException ex) {
Log.e(TAG, "", ex);
return null;
} finally {
closeSilently(input);
}
}
// Rotates the bitmap by the specified degree.
// If a new bitmap is created, the original bitmap is recycled.
public static Bitmap rotate(Bitmap b, int degrees) {
if (degrees != 0 && b != null) {
Matrix m = new Matrix();
m.setRotate(degrees,
(float) b.getWidth() / 2, (float) b.getHeight() / 2);
try {
Bitmap b2 = Bitmap.createBitmap(
b, 0, 0, b.getWidth(), b.getHeight(), m, true);
if (b != b2) {
b.recycle();
b = b2;
}
} catch (OutOfMemoryError ex) {
// We have no memory to rotate. Return the original bitmap.
}
}
return b;
}
private static void closeSilently(ParcelFileDescriptor c) {
if (c == null) return;
try {
c.close();
} catch (Throwable t) {
// do nothing
}
}
private static ParcelFileDescriptor makeInputStream(
Uri uri, ContentResolver cr) {
try {
return cr.openFileDescriptor(uri, "r");
} catch (IOException ex) {
return null;
}
}
public static Bitmap makeBitmap(int minSideLength, int maxNumOfPixels,
Uri uri, ContentResolver cr, ParcelFileDescriptor pfd,
BitmapFactory.Options options) {
Bitmap b = null;
try {
if (pfd == null) pfd = makeInputStream(uri, cr);
if (pfd == null) return null;
if (options == null) options = new BitmapFactory.Options();
FileDescriptor fd = pfd.getFileDescriptor();
options.inSampleSize = 1;
options.inJustDecodeBounds = true;
BitmapFactory.decodeFileDescriptor(fd, null, options);
if (options.mCancel || options.outWidth == -1
|| options.outHeight == -1) {
return null;
}
options.inSampleSize = computeSampleSize(
options, minSideLength, maxNumOfPixels);
options.inJustDecodeBounds = false;
options.inDither = false;
options.inPreferredConfig = Bitmap.Config.ARGB_8888;
b = BitmapFactory.decodeFileDescriptor(fd, null, options);
} catch (OutOfMemoryError ex) {
Log.e(TAG, "Got oom exception ", ex);
return null;
} finally {
closeSilently(pfd);
}
return b;
}
/**
* Creates a centered bitmap of the desired size.
* @param source
* @param recycle whether we want to recycle the input
*/
public static Bitmap extractMiniThumb(
Bitmap source, int width, int height, boolean recycle) {
if (source == null) {
return null;
}
float scale;
if (source.getWidth() < source.getHeight()) {
scale = width / (float) source.getWidth();
} else {
scale = height / (float) source.getHeight();
}
Matrix matrix = new Matrix();
matrix.setScale(scale, scale);
Bitmap miniThumbnail = transform(matrix, source, width, height, false, recycle);
return miniThumbnail;
}
/**
* Creates a byte[] for a given bitmap of the desired size. Recycles the
* input bitmap.
*/
public static byte[] miniThumbData(Bitmap source) {
if (source == null) return null;
Bitmap miniThumbnail = extractMiniThumb(
source, MINI_THUMB_TARGET_SIZE,
MINI_THUMB_TARGET_SIZE,
RECYCLE_INPUT);
ByteArrayOutputStream miniOutStream = new ByteArrayOutputStream();
miniThumbnail.compress(Bitmap.CompressFormat.JPEG, 75, miniOutStream);
miniThumbnail.recycle();
try {
miniOutStream.close();
byte [] data = miniOutStream.toByteArray();
return data;
} catch (java.io.IOException ex) {
Log.e(TAG, "got exception ex " + ex);
}
return null;
}
/**
* Create a video thumbnail for a video. May return null if the video is
* corrupt.
*
* @param filePath
*/
public static Bitmap createVideoThumbnail(String filePath) {
Bitmap bitmap = null;
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
try {
retriever.setMode(MediaMetadataRetriever.MODE_CAPTURE_FRAME_ONLY);
retriever.setDataSource(filePath);
bitmap = retriever.captureFrame();
} catch (IllegalArgumentException ex) {
// Assume this is a corrupt video file
} catch (RuntimeException ex) {
// Assume this is a corrupt video file.
} finally {
try {
retriever.release();
} catch (RuntimeException ex) {
// Ignore failures while cleaning up.
}
}
return bitmap;
}
public static Bitmap transform(Matrix scaler,
Bitmap source,
int targetWidth,
int targetHeight,
boolean scaleUp,
boolean recycle) {
int deltaX = source.getWidth() - targetWidth;
int deltaY = source.getHeight() - targetHeight;
if (!scaleUp && (deltaX < 0 || deltaY < 0)) {
/*
* In this case the bitmap is smaller, at least in one dimension,
* than the target. Transform it by placing as much of the image
* as possible into the target and leaving the top/bottom or
* left/right (or both) black.
*/
Bitmap b2 = Bitmap.createBitmap(targetWidth, targetHeight,
Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(b2);
int deltaXHalf = Math.max(0, deltaX / 2);
int deltaYHalf = Math.max(0, deltaY / 2);
Rect src = new Rect(
deltaXHalf,
deltaYHalf,
deltaXHalf + Math.min(targetWidth, source.getWidth()),
deltaYHalf + Math.min(targetHeight, source.getHeight()));
int dstX = (targetWidth - src.width()) / 2;
int dstY = (targetHeight - src.height()) / 2;
Rect dst = new Rect(
dstX,
dstY,
targetWidth - dstX,
targetHeight - dstY);
c.drawBitmap(source, src, dst, null);
if (recycle) {
source.recycle();
}
return b2;
}
float bitmapWidthF = source.getWidth();
float bitmapHeightF = source.getHeight();
float bitmapAspect = bitmapWidthF / bitmapHeightF;
float viewAspect = (float) targetWidth / targetHeight;
if (bitmapAspect > viewAspect) {
float scale = targetHeight / bitmapHeightF;
if (scale < .9F || scale > 1F) {
scaler.setScale(scale, scale);
} else {
scaler = null;
}
} else {
float scale = targetWidth / bitmapWidthF;
if (scale < .9F || scale > 1F) {
scaler.setScale(scale, scale);
} else {
scaler = null;
}
}
Bitmap b1;
if (scaler != null) {
// this is used for minithumb and crop, so we want to filter here.
b1 = Bitmap.createBitmap(source, 0, 0,
source.getWidth(), source.getHeight(), scaler, true);
} else {
b1 = source;
}
if (recycle && b1 != source) {
source.recycle();
}
int dx1 = Math.max(0, b1.getWidth() - targetWidth);
int dy1 = Math.max(0, b1.getHeight() - targetHeight);
Bitmap b2 = Bitmap.createBitmap(
b1,
dx1 / 2,
dy1 / 2,
targetWidth,
targetHeight);
if (b2 != b1) {
if (recycle || b1 != source) {
b1.recycle();
}
}
return b2;
}
}