diff options
author | Ray Chen <raychen@google.com> | 2009-08-28 14:12:15 -0700 |
---|---|---|
committer | Ray Chen <raychen@google.com> | 2009-09-23 11:23:45 -0700 |
commit | 00c575a3fccb9d3065e913f1b8fcf93e18d44eaf (patch) | |
tree | 56ca36895b1d2f9b95b3d1cf3bd673a04dbfb2f5 /media | |
parent | 9d12fdb1b55500d69df5a1bdc2fcba57a2f1876c (diff) | |
download | frameworks_base-00c575a3fccb9d3065e913f1b8fcf93e18d44eaf.zip frameworks_base-00c575a3fccb9d3065e913f1b8fcf93e18d44eaf.tar.gz frameworks_base-00c575a3fccb9d3065e913f1b8fcf93e18d44eaf.tar.bz2 |
Add new thumbnail API.
Diffstat (limited to 'media')
-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 |
3 files changed, 683 insertions, 0 deletions
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; + } + + + +} |