1 /* 2 * Copyright (C) 2009 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.providers.media; 18 19 import java.io.ByteArrayOutputStream; 20 import java.io.IOException; 21 import java.io.OutputStream; 22 import java.util.Comparator; 23 import java.util.Random; 24 25 import android.content.ContentResolver; 26 import android.content.ContentUris; 27 import android.content.ContentValues; 28 import android.database.Cursor; 29 import android.graphics.Bitmap; 30 import android.graphics.BitmapFactory; 31 import android.media.MiniThumbFile; 32 import android.media.ThumbnailUtils; 33 import android.net.Uri; 34 import android.os.Binder; 35 import android.os.ParcelFileDescriptor; 36 import android.provider.BaseColumns; 37 import android.provider.MediaStore.Images; 38 import android.provider.MediaStore.Video; 39 import android.provider.MediaStore.MediaColumns; 40 import android.provider.MediaStore.Images.ImageColumns; 41 import android.util.Log; 42 43 /** 44 * Instances of this class are created and put in a queue to be executed sequentially to see if 45 * it needs to (re)generate the thumbnails. 46 */ 47 class MediaThumbRequest { 48 private static final String TAG = "MediaThumbRequest"; 49 static final int PRIORITY_LOW = 20; 50 static final int PRIORITY_NORMAL = 10; 51 static final int PRIORITY_HIGH = 5; 52 static final int PRIORITY_CRITICAL = 0; 53 static enum State {WAIT, DONE, CANCEL} 54 private static final String[] THUMB_PROJECTION = new String[] { 55 BaseColumns._ID // 0 56 }; 57 58 ContentResolver mCr; 59 String mPath; 60 long mRequestTime = System.currentTimeMillis(); 61 int mCallingPid = Binder.getCallingPid(); 62 long mGroupId; 63 int mPriority; 64 Uri mUri; 65 Uri mThumbUri; 66 String mOrigColumnName; 67 boolean mIsVideo; 68 long mOrigId; 69 State mState = State.WAIT; 70 long mMagic; 71 72 private static final Random sRandom = new Random(); 73 getComparator()74 static Comparator<MediaThumbRequest> getComparator() { 75 return new Comparator<MediaThumbRequest>() { 76 public int compare(MediaThumbRequest r1, MediaThumbRequest r2) { 77 if (r1.mPriority != r2.mPriority) { 78 return r1.mPriority < r2.mPriority ? -1 : 1; 79 } 80 return r1.mRequestTime == r2.mRequestTime ? 0 : 81 r1.mRequestTime < r2.mRequestTime ? -1 : 1; 82 } 83 }; 84 } 85 86 MediaThumbRequest(ContentResolver cr, String path, Uri uri, int priority, long magic) { 87 mCr = cr; 88 mPath = path; 89 mPriority = priority; 90 mMagic = magic; 91 mUri = uri; 92 mIsVideo = "video".equals(uri.getPathSegments().get(1)); 93 mOrigId = ContentUris.parseId(uri); 94 mThumbUri = mIsVideo 95 ? Video.Thumbnails.EXTERNAL_CONTENT_URI 96 : Images.Thumbnails.EXTERNAL_CONTENT_URI; 97 mOrigColumnName = mIsVideo 98 ? Video.Thumbnails.VIDEO_ID 99 : Images.Thumbnails.IMAGE_ID; 100 // Only requests from Thumbnail API has this group_id parameter. In other cases, 101 // mGroupId will always be zero and can't be canceled due to pid mismatch. 102 String groupIdParam = uri.getQueryParameter("group_id"); 103 if (groupIdParam != null) { 104 mGroupId = Long.parseLong(groupIdParam); 105 } 106 } 107 108 Uri updateDatabase(Bitmap thumbnail) { 109 Cursor c = mCr.query(mThumbUri, THUMB_PROJECTION, 110 mOrigColumnName+ " = " + mOrigId, null, null); 111 if (c == null) return null; 112 try { 113 if (c.moveToFirst()) { 114 return ContentUris.withAppendedId(mThumbUri, c.getLong(0)); 115 } 116 } finally { 117 if (c != null) c.close(); 118 } 119 120 ContentValues values = new ContentValues(4); 121 values.put(Images.Thumbnails.KIND, Images.Thumbnails.MINI_KIND); 122 values.put(mOrigColumnName, mOrigId); 123 values.put(Images.Thumbnails.WIDTH, thumbnail.getWidth()); 124 values.put(Images.Thumbnails.HEIGHT, thumbnail.getHeight()); 125 try { 126 return mCr.insert(mThumbUri, values); 127 } catch (Exception ex) { 128 Log.w(TAG, ex); 129 return null; 130 } 131 } 132 133 /** 134 * Check if the corresponding thumbnail and mini-thumb have been created 135 * for the given uri. This method creates both of them if they do not 136 * exist yet or have been changed since last check. After thumbnails are 137 * created, MINI_KIND thumbnail is stored in JPEG file and MICRO_KIND 138 * thumbnail is stored in a random access file (MiniThumbFile). 139 * 140 * @throws IOException 141 */ 142 void execute() throws IOException { 143 MiniThumbFile miniThumbFile = MiniThumbFile.instance(mUri); 144 long magic = mMagic; 145 if (magic != 0) { 146 long fileMagic = miniThumbFile.getMagic(mOrigId); 147 if (fileMagic == magic) { 148 Cursor c = null; 149 ParcelFileDescriptor pfd = null; 150 // Clear calling identity as we may be handling an IPC. 151 final long identity = Binder.clearCallingIdentity(); 152 try { 153 c = mCr.query(mThumbUri, THUMB_PROJECTION, 154 mOrigColumnName + " = " + mOrigId, null, null); 155 if (c != null && c.moveToFirst()) { 156 pfd = mCr.openFileDescriptor( 157 mThumbUri.buildUpon().appendPath(c.getString(0)).build(), "r"); 158 } 159 } catch (IOException ex) { 160 // MINI_THUMBNAIL not exists, ignore the exception and generate one. 161 } finally { 162 Binder.restoreCallingIdentity(identity); 163 if (c != null) c.close(); 164 if (pfd != null) { 165 pfd.close(); 166 return; 167 } 168 } 169 } 170 } 171 172 // If we can't retrieve the thumbnail, first check if there is one 173 // embedded in the EXIF data. If not, or it's not big enough, 174 // decompress the full size image. 175 Bitmap bitmap = null; 176 177 if (mPath != null) { 178 if (mIsVideo) { 179 bitmap = ThumbnailUtils.createVideoThumbnail(mPath, 180 Video.Thumbnails.MINI_KIND); 181 } else { 182 bitmap = ThumbnailUtils.createImageThumbnail(mPath, 183 Images.Thumbnails.MINI_KIND); 184 } 185 if (bitmap == null) { 186 Log.w(TAG, "Can't create mini thumbnail for " + mPath); 187 return; 188 } 189 190 Uri uri = updateDatabase(bitmap); 191 if (uri != null) { 192 OutputStream thumbOut = mCr.openOutputStream(uri); 193 bitmap.compress(Bitmap.CompressFormat.JPEG, 85, thumbOut); 194 thumbOut.close(); 195 } 196 } 197 198 bitmap = ThumbnailUtils.extractThumbnail(bitmap, 199 ThumbnailUtils.TARGET_SIZE_MICRO_THUMBNAIL, 200 ThumbnailUtils.TARGET_SIZE_MICRO_THUMBNAIL, 201 ThumbnailUtils.OPTIONS_RECYCLE_INPUT); 202 203 if (bitmap != null) { 204 ByteArrayOutputStream miniOutStream = new ByteArrayOutputStream(); 205 bitmap.compress(Bitmap.CompressFormat.JPEG, 75, miniOutStream); 206 bitmap.recycle(); 207 byte [] data = null; 208 209 try { 210 miniOutStream.close(); 211 data = miniOutStream.toByteArray(); 212 } catch (java.io.IOException ex) { 213 Log.e(TAG, "got exception ex " + ex); 214 } 215 216 // We may consider retire this proprietary format, after all it's size is only 217 // 128 x 128 at most, which is still reasonable to be stored in database. 218 // Gallery application can use the MINI_THUMB_MAGIC value to determine if it's 219 // time to query and fetch by using Cursor.getBlob 220 if (data != null) { 221 // make a new magic number since things are out of sync 222 do { 223 magic = sRandom.nextLong(); 224 } while (magic == 0); 225 226 miniThumbFile.saveMiniThumbToFile(data, mOrigId, magic); 227 ContentValues values = new ContentValues(); 228 // both video/images table use the same column name "mini_thumb_magic" 229 values.put(ImageColumns.MINI_THUMB_MAGIC, magic); 230 try { 231 mCr.update(mUri, values, null, null); 232 mMagic = magic; 233 } catch (java.lang.IllegalStateException ex) { 234 Log.e(TAG, "got exception while updating database " + ex); 235 } 236 } 237 } else { 238 Log.w(TAG, "can't create bitmap for thumbnail."); 239 } 240 miniThumbFile.deactivate(); 241 } 242 } 243