Add new thumbnail API.
diff --git a/api/current.xml b/api/current.xml
index 581d2c4..2672d53 100644
--- a/api/current.xml
+++ b/api/current.xml
@@ -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=""thumb_data""
+ 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=""_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"
abstract="true"
static="true"
diff --git a/core/java/android/provider/MediaStore.java b/core/java/android/provider/MediaStore.java
index 49b5bb1..106e833 100644
--- a/core/java/android/provider/MediaStore.java
+++ b/core/java/android/provider/MediaStore.java
@@ -23,11 +23,15 @@
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 @@
* 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";
@@ -179,7 +182,7 @@
* Common fields for most MediaProvider tables
*/
- public interface MediaColumns extends BaseColumns {
+ public interface MediaColumns extends BaseColumns {
/**
* The data stream for the file
* <P>Type: DATA STREAM</P>
@@ -227,10 +230,128 @@
}
/**
+ * 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 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 @@
* @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 @@
* @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 @@
* 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 @@
Uri url = null;
String stringUrl = null; /* value to be returned */
- try
- {
+ try {
url = cr.insert(EXTERNAL_CONTENT_URI, values);
if (source != null) {
@@ -496,28 +610,48 @@
* The default sort order for this table
*/
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 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
@@ -1182,7 +1321,7 @@
* <P>Type: INTEGER</P>
*/
public static final String FIRST_YEAR = "minyear";
-
+
/**
* The year in which the latest songs
* on this album were released. This will often
@@ -1259,8 +1398,7 @@
*/
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 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";
+ }
}
/**
diff --git a/media/java/android/media/MediaScanner.java b/media/java/android/media/MediaScanner.java
index 3ac5df5..21ad82b 100644
--- a/media/java/android/media/MediaScanner.java
+++ b/media/java/android/media/MediaScanner.java
@@ -486,6 +486,8 @@
}
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);
}
diff --git a/media/java/android/media/MiniThumbFile.java b/media/java/android/media/MiniThumbFile.java
new file mode 100644
index 0000000..c607218
--- /dev/null
+++ b/media/java/android/media/MiniThumbFile.java
@@ -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;
+ }
+}
diff --git a/media/java/android/media/ThumbnailUtil.java b/media/java/android/media/ThumbnailUtil.java
new file mode 100644
index 0000000..3db10b8
--- /dev/null
+++ b/media/java/android/media/ThumbnailUtil.java
@@ -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;
+ }
+
+
+
+}