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