| /* |
| * 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 com.android.providers.media; |
| |
| import java.io.ByteArrayOutputStream; |
| import java.io.IOException; |
| import java.io.OutputStream; |
| import java.util.Comparator; |
| import java.util.Random; |
| |
| import android.content.ContentResolver; |
| import android.content.ContentUris; |
| import android.content.ContentValues; |
| import android.database.Cursor; |
| import android.graphics.Bitmap; |
| import android.graphics.BitmapFactory; |
| import android.media.MiniThumbFile; |
| import android.media.ThumbnailUtils; |
| import android.net.Uri; |
| import android.os.Binder; |
| import android.os.ParcelFileDescriptor; |
| import android.provider.BaseColumns; |
| import android.provider.MediaStore.Images; |
| import android.provider.MediaStore.Video; |
| import android.provider.MediaStore.MediaColumns; |
| import android.provider.MediaStore.Images.ImageColumns; |
| import android.util.Log; |
| |
| /** |
| * Instances of this class are created and put in a queue to be executed sequentially to see if |
| * it needs to (re)generate the thumbnails. |
| */ |
| class MediaThumbRequest { |
| private static final String TAG = "MediaThumbRequest"; |
| static final int PRIORITY_LOW = 20; |
| static final int PRIORITY_NORMAL = 10; |
| static final int PRIORITY_HIGH = 5; |
| static final int PRIORITY_CRITICAL = 0; |
| static enum State {WAIT, DONE, CANCEL} |
| private static final String[] THUMB_PROJECTION = new String[] { |
| BaseColumns._ID // 0 |
| }; |
| |
| ContentResolver mCr; |
| String mPath; |
| long mRequestTime = System.currentTimeMillis(); |
| int mCallingPid = Binder.getCallingPid(); |
| long mGroupId; |
| int mPriority; |
| Uri mUri; |
| Uri mThumbUri; |
| String mOrigColumnName; |
| boolean mIsVideo; |
| long mOrigId; |
| State mState = State.WAIT; |
| long mMagic; |
| |
| private static final Random sRandom = new Random(); |
| |
| static Comparator<MediaThumbRequest> getComparator() { |
| return new Comparator<MediaThumbRequest>() { |
| public int compare(MediaThumbRequest r1, MediaThumbRequest r2) { |
| if (r1.mPriority != r2.mPriority) { |
| return r1.mPriority < r2.mPriority ? -1 : 1; |
| } |
| return r1.mRequestTime == r2.mRequestTime ? 0 : |
| r1.mRequestTime < r2.mRequestTime ? -1 : 1; |
| } |
| }; |
| } |
| |
| MediaThumbRequest(ContentResolver cr, String path, Uri uri, int priority, long magic) { |
| mCr = cr; |
| mPath = path; |
| mPriority = priority; |
| mMagic = magic; |
| mUri = uri; |
| mIsVideo = "video".equals(uri.getPathSegments().get(1)); |
| mOrigId = ContentUris.parseId(uri); |
| mThumbUri = mIsVideo |
| ? Video.Thumbnails.EXTERNAL_CONTENT_URI |
| : Images.Thumbnails.EXTERNAL_CONTENT_URI; |
| mOrigColumnName = mIsVideo |
| ? Video.Thumbnails.VIDEO_ID |
| : Images.Thumbnails.IMAGE_ID; |
| // Only requests from Thumbnail API has this group_id parameter. In other cases, |
| // mGroupId will always be zero and can't be canceled due to pid mismatch. |
| String groupIdParam = uri.getQueryParameter("group_id"); |
| if (groupIdParam != null) { |
| mGroupId = Long.parseLong(groupIdParam); |
| } |
| } |
| |
| Uri updateDatabase(Bitmap thumbnail) { |
| Cursor c = mCr.query(mThumbUri, THUMB_PROJECTION, |
| mOrigColumnName+ " = " + mOrigId, null, null); |
| if (c == null) return null; |
| try { |
| if (c.moveToFirst()) { |
| return ContentUris.withAppendedId(mThumbUri, c.getLong(0)); |
| } |
| } finally { |
| if (c != null) c.close(); |
| } |
| |
| ContentValues values = new ContentValues(4); |
| values.put(Images.Thumbnails.KIND, Images.Thumbnails.MINI_KIND); |
| values.put(mOrigColumnName, mOrigId); |
| values.put(Images.Thumbnails.WIDTH, thumbnail.getWidth()); |
| values.put(Images.Thumbnails.HEIGHT, thumbnail.getHeight()); |
| try { |
| return mCr.insert(mThumbUri, values); |
| } catch (Exception ex) { |
| Log.w(TAG, ex); |
| return null; |
| } |
| } |
| |
| /** |
| * Check if the corresponding thumbnail and mini-thumb have been created |
| * for the given uri. This method creates both of them if they do not |
| * exist yet or have been changed since last check. After thumbnails are |
| * created, MINI_KIND thumbnail is stored in JPEG file and MICRO_KIND |
| * thumbnail is stored in a random access file (MiniThumbFile). |
| * |
| * @throws IOException |
| */ |
| void execute() throws IOException { |
| MiniThumbFile miniThumbFile = MiniThumbFile.instance(mUri); |
| long magic = mMagic; |
| if (magic != 0) { |
| long fileMagic = miniThumbFile.getMagic(mOrigId); |
| if (fileMagic == magic) { |
| Cursor c = null; |
| ParcelFileDescriptor pfd = null; |
| // Clear calling identity as we may be handling an IPC. |
| final long identity = Binder.clearCallingIdentity(); |
| try { |
| c = mCr.query(mThumbUri, THUMB_PROJECTION, |
| mOrigColumnName + " = " + mOrigId, null, null); |
| if (c != null && c.moveToFirst()) { |
| pfd = mCr.openFileDescriptor( |
| mThumbUri.buildUpon().appendPath(c.getString(0)).build(), "r"); |
| } |
| } catch (IOException ex) { |
| // MINI_THUMBNAIL not exists, ignore the exception and generate one. |
| } finally { |
| Binder.restoreCallingIdentity(identity); |
| if (c != null) c.close(); |
| if (pfd != null) { |
| pfd.close(); |
| return; |
| } |
| } |
| } |
| } |
| |
| // If we can't retrieve the thumbnail, first check if there is one |
| // embedded in the EXIF data. If not, or it's not big enough, |
| // decompress the full size image. |
| Bitmap bitmap = null; |
| |
| if (mPath != null) { |
| if (mIsVideo) { |
| bitmap = ThumbnailUtils.createVideoThumbnail(mPath, |
| Video.Thumbnails.MINI_KIND); |
| } else { |
| bitmap = ThumbnailUtils.createImageThumbnail(mPath, |
| Images.Thumbnails.MINI_KIND); |
| } |
| if (bitmap == null) { |
| Log.w(TAG, "Can't create mini thumbnail for " + mPath); |
| return; |
| } |
| |
| Uri uri = updateDatabase(bitmap); |
| if (uri != null) { |
| OutputStream thumbOut = mCr.openOutputStream(uri); |
| bitmap.compress(Bitmap.CompressFormat.JPEG, 85, thumbOut); |
| thumbOut.close(); |
| } |
| } |
| |
| bitmap = ThumbnailUtils.extractThumbnail(bitmap, |
| ThumbnailUtils.TARGET_SIZE_MICRO_THUMBNAIL, |
| ThumbnailUtils.TARGET_SIZE_MICRO_THUMBNAIL, |
| ThumbnailUtils.OPTIONS_RECYCLE_INPUT); |
| |
| if (bitmap != null) { |
| ByteArrayOutputStream miniOutStream = new ByteArrayOutputStream(); |
| bitmap.compress(Bitmap.CompressFormat.JPEG, 75, miniOutStream); |
| bitmap.recycle(); |
| byte [] data = null; |
| |
| try { |
| miniOutStream.close(); |
| data = miniOutStream.toByteArray(); |
| } catch (java.io.IOException ex) { |
| Log.e(TAG, "got exception ex " + ex); |
| } |
| |
| // We may consider retire this proprietary format, after all it's size is only |
| // 128 x 128 at most, which is still reasonable to be stored in database. |
| // Gallery application can use the MINI_THUMB_MAGIC value to determine if it's |
| // time to query and fetch by using Cursor.getBlob |
| if (data != null) { |
| // make a new magic number since things are out of sync |
| do { |
| magic = sRandom.nextLong(); |
| } while (magic == 0); |
| |
| miniThumbFile.saveMiniThumbToFile(data, mOrigId, magic); |
| ContentValues values = new ContentValues(); |
| // both video/images table use the same column name "mini_thumb_magic" |
| values.put(ImageColumns.MINI_THUMB_MAGIC, magic); |
| try { |
| mCr.update(mUri, values, null, null); |
| mMagic = magic; |
| } catch (java.lang.IllegalStateException ex) { |
| Log.e(TAG, "got exception while updating database " + ex); |
| } |
| } |
| } else { |
| Log.w(TAG, "can't create bitmap for thumbnail."); |
| } |
| miniThumbFile.deactivate(); |
| } |
| } |