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:
200
api/current.xml
200
api/current.xml
@ -114624,6 +114624,25 @@
|
|||||||
<parameter name="volumeName" type="java.lang.String">
|
<parameter name="volumeName" type="java.lang.String">
|
||||||
</parameter>
|
</parameter>
|
||||||
</method>
|
</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"
|
<method name="query"
|
||||||
return="android.database.Cursor"
|
return="android.database.Cursor"
|
||||||
abstract="false"
|
abstract="false"
|
||||||
@ -114787,6 +114806,17 @@
|
|||||||
visibility="public"
|
visibility="public"
|
||||||
>
|
>
|
||||||
</field>
|
</field>
|
||||||
|
<field name="THUMB_DATA"
|
||||||
|
type="java.lang.String"
|
||||||
|
transient="false"
|
||||||
|
volatile="false"
|
||||||
|
value=""thumb_data""
|
||||||
|
static="true"
|
||||||
|
final="true"
|
||||||
|
deprecated="not deprecated"
|
||||||
|
visibility="public"
|
||||||
|
>
|
||||||
|
</field>
|
||||||
<field name="WIDTH"
|
<field name="WIDTH"
|
||||||
type="java.lang.String"
|
type="java.lang.String"
|
||||||
transient="false"
|
transient="false"
|
||||||
@ -115005,6 +115035,176 @@
|
|||||||
>
|
>
|
||||||
</field>
|
</field>
|
||||||
</class>
|
</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=""_data""
|
||||||
|
static="true"
|
||||||
|
final="true"
|
||||||
|
deprecated="not deprecated"
|
||||||
|
visibility="public"
|
||||||
|
>
|
||||||
|
</field>
|
||||||
|
<field name="DEFAULT_SORT_ORDER"
|
||||||
|
type="java.lang.String"
|
||||||
|
transient="false"
|
||||||
|
volatile="false"
|
||||||
|
value=""video_id ASC""
|
||||||
|
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=""height""
|
||||||
|
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=""kind""
|
||||||
|
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=""video_id""
|
||||||
|
static="true"
|
||||||
|
final="true"
|
||||||
|
deprecated="not deprecated"
|
||||||
|
visibility="public"
|
||||||
|
>
|
||||||
|
</field>
|
||||||
|
<field name="WIDTH"
|
||||||
|
type="java.lang.String"
|
||||||
|
transient="false"
|
||||||
|
volatile="false"
|
||||||
|
value=""width""
|
||||||
|
static="true"
|
||||||
|
final="true"
|
||||||
|
deprecated="not deprecated"
|
||||||
|
visibility="public"
|
||||||
|
>
|
||||||
|
</field>
|
||||||
|
</class>
|
||||||
<interface name="MediaStore.Video.VideoColumns"
|
<interface name="MediaStore.Video.VideoColumns"
|
||||||
abstract="true"
|
abstract="true"
|
||||||
static="true"
|
static="true"
|
||||||
|
@ -23,11 +23,15 @@ import android.content.ContentValues;
|
|||||||
import android.content.ContentUris;
|
import android.content.ContentUris;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.database.DatabaseUtils;
|
import android.database.DatabaseUtils;
|
||||||
|
import android.database.sqlite.SQLiteException;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.graphics.BitmapFactory;
|
import android.graphics.BitmapFactory;
|
||||||
import android.graphics.Matrix;
|
import android.graphics.Matrix;
|
||||||
|
import android.media.MiniThumbFile;
|
||||||
|
import android.media.ThumbnailUtil;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Environment;
|
import android.os.Environment;
|
||||||
|
import android.os.ParcelFileDescriptor;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import java.io.FileInputStream;
|
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
|
* The Media provider contains meta data for all available media on both internal
|
||||||
* and external storage devices.
|
* and external storage devices.
|
||||||
*/
|
*/
|
||||||
public final class MediaStore
|
public final class MediaStore {
|
||||||
{
|
|
||||||
private final static String TAG = "MediaStore";
|
private final static String TAG = "MediaStore";
|
||||||
|
|
||||||
public static final String AUTHORITY = "media";
|
public static final String AUTHORITY = "media";
|
||||||
@ -179,7 +182,7 @@ public final class MediaStore
|
|||||||
* Common fields for most MediaProvider tables
|
* Common fields for most MediaProvider tables
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public interface MediaColumns extends BaseColumns {
|
public interface MediaColumns extends BaseColumns {
|
||||||
/**
|
/**
|
||||||
* The data stream for the file
|
* The data stream for the file
|
||||||
* <P>Type: DATA STREAM</P>
|
* <P>Type: DATA STREAM</P>
|
||||||
@ -226,11 +229,129 @@ public final class MediaStore
|
|||||||
public static final String MIME_TYPE = "mime_type";
|
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.
|
* Contains meta data for all available images.
|
||||||
*/
|
*/
|
||||||
public static final class Images
|
public static final class Images {
|
||||||
{
|
|
||||||
public interface ImageColumns extends MediaColumns {
|
public interface ImageColumns extends MediaColumns {
|
||||||
/**
|
/**
|
||||||
* The description of the image
|
* The description of the image
|
||||||
@ -298,21 +419,18 @@ public final class MediaStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static final class Media implements ImageColumns {
|
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);
|
return cr.query(uri, projection, null, null, DEFAULT_SORT_ORDER);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static final Cursor query(ContentResolver cr, Uri uri, String[] projection,
|
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,
|
return cr.query(uri, projection, where,
|
||||||
null, orderBy == null ? DEFAULT_SORT_ORDER : orderBy);
|
null, orderBy == null ? DEFAULT_SORT_ORDER : orderBy);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static final Cursor query(ContentResolver cr, Uri uri, String[] projection,
|
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,
|
return cr.query(uri, projection, selection,
|
||||||
selectionArgs, orderBy == null ? DEFAULT_SORT_ORDER : orderBy);
|
selectionArgs, orderBy == null ? DEFAULT_SORT_ORDER : orderBy);
|
||||||
}
|
}
|
||||||
@ -326,8 +444,7 @@ public final class MediaStore
|
|||||||
* @throws IOException
|
* @throws IOException
|
||||||
*/
|
*/
|
||||||
public static final Bitmap getBitmap(ContentResolver cr, Uri url)
|
public static final Bitmap getBitmap(ContentResolver cr, Uri url)
|
||||||
throws FileNotFoundException, IOException
|
throws FileNotFoundException, IOException {
|
||||||
{
|
|
||||||
InputStream input = cr.openInputStream(url);
|
InputStream input = cr.openInputStream(url);
|
||||||
Bitmap bitmap = BitmapFactory.decodeStream(input);
|
Bitmap bitmap = BitmapFactory.decodeStream(input);
|
||||||
input.close();
|
input.close();
|
||||||
@ -344,9 +461,8 @@ public final class MediaStore
|
|||||||
* @return The URL to the newly created image
|
* @return The URL to the newly created image
|
||||||
* @throws FileNotFoundException
|
* @throws FileNotFoundException
|
||||||
*/
|
*/
|
||||||
public static final String insertImage(ContentResolver cr, String imagePath, String name,
|
public static final String insertImage(ContentResolver cr, String imagePath,
|
||||||
String description) throws FileNotFoundException
|
String name, String description) throws FileNotFoundException {
|
||||||
{
|
|
||||||
// Check if file exists with a FileInputStream
|
// Check if file exists with a FileInputStream
|
||||||
FileInputStream stream = new FileInputStream(imagePath);
|
FileInputStream stream = new FileInputStream(imagePath);
|
||||||
try {
|
try {
|
||||||
@ -415,8 +531,7 @@ public final class MediaStore
|
|||||||
* for any reason.
|
* for any reason.
|
||||||
*/
|
*/
|
||||||
public static final String insertImage(ContentResolver cr, Bitmap source,
|
public static final String insertImage(ContentResolver cr, Bitmap source,
|
||||||
String title, String description)
|
String title, String description) {
|
||||||
{
|
|
||||||
ContentValues values = new ContentValues();
|
ContentValues values = new ContentValues();
|
||||||
values.put(Images.Media.TITLE, title);
|
values.put(Images.Media.TITLE, title);
|
||||||
values.put(Images.Media.DESCRIPTION, description);
|
values.put(Images.Media.DESCRIPTION, description);
|
||||||
@ -425,8 +540,7 @@ public final class MediaStore
|
|||||||
Uri url = null;
|
Uri url = null;
|
||||||
String stringUrl = null; /* value to be returned */
|
String stringUrl = null; /* value to be returned */
|
||||||
|
|
||||||
try
|
try {
|
||||||
{
|
|
||||||
url = cr.insert(EXTERNAL_CONTENT_URI, values);
|
url = cr.insert(EXTERNAL_CONTENT_URI, values);
|
||||||
|
|
||||||
if (source != null) {
|
if (source != null) {
|
||||||
@ -496,27 +610,47 @@ public final class MediaStore
|
|||||||
* The default sort order for this table
|
* The default sort order for this table
|
||||||
*/
|
*/
|
||||||
public static final String DEFAULT_SORT_ORDER = ImageColumns.BUCKET_DISPLAY_NAME;
|
public static final String DEFAULT_SORT_ORDER = ImageColumns.BUCKET_DISPLAY_NAME;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class Thumbnails implements BaseColumns
|
/**
|
||||||
{
|
* This class allows developers to query and get two kinds of thumbnails:
|
||||||
public static final Cursor query(ContentResolver cr, Uri uri, String[] projection)
|
* 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);
|
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);
|
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,
|
return cr.query(EXTERNAL_CONTENT_URI, projection,
|
||||||
IMAGE_ID + " = " + origId + " AND " + KIND + " = " +
|
IMAGE_ID + " = " + origId + " AND " + KIND + " = " +
|
||||||
kind, null, null);
|
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
|
* Get the content:// style URI for the image media table on the
|
||||||
* given volume.
|
* given volume.
|
||||||
@ -568,6 +702,11 @@ public final class MediaStore
|
|||||||
public static final int MINI_KIND = 1;
|
public static final int MINI_KIND = 1;
|
||||||
public static final int FULL_SCREEN_KIND = 2;
|
public static final int FULL_SCREEN_KIND = 2;
|
||||||
public static final int MICRO_KIND = 3;
|
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
|
* The width of the thumbnal
|
||||||
@ -1182,7 +1321,7 @@ public final class MediaStore
|
|||||||
* <P>Type: INTEGER</P>
|
* <P>Type: INTEGER</P>
|
||||||
*/
|
*/
|
||||||
public static final String FIRST_YEAR = "minyear";
|
public static final String FIRST_YEAR = "minyear";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The year in which the latest songs
|
* The year in which the latest songs
|
||||||
* on this album were released. This will often
|
* on this album were released. This will often
|
||||||
@ -1259,8 +1398,7 @@ public final class MediaStore
|
|||||||
*/
|
*/
|
||||||
public static final String DEFAULT_SORT_ORDER = MediaColumns.DISPLAY_NAME;
|
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);
|
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;
|
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";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -486,6 +486,8 @@ public class MediaScanner
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void scanFile(String path, long lastModified, long fileSize) {
|
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);
|
doScanFile(path, null, lastModified, fileSize, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
280
media/java/android/media/MiniThumbFile.java
Normal file
280
media/java/android/media/MiniThumbFile.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
401
media/java/android/media/ThumbnailUtil.java
Normal file
401
media/java/android/media/ThumbnailUtil.java
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
Reference in New Issue
Block a user