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 try { 151 c = mCr.query(mThumbUri, THUMB_PROJECTION, 152 mOrigColumnName + " = " + mOrigId, null, null); 153 if (c != null && c.moveToFirst()) { 154 pfd = mCr.openFileDescriptor( 155 mThumbUri.buildUpon().appendPath(c.getString(0)).build(), "r"); 156 } 157 } catch (IOException ex) { 158 // MINI_THUMBNAIL not exists, ignore the exception and generate one. 159 } finally { 160 if (c != null) c.close(); 161 if (pfd != null) { 162 pfd.close(); 163 return; 164 } 165 } 166 } 167 } 168 169 // If we can't retrieve the thumbnail, first check if there is one 170 // embedded in the EXIF data. If not, or it's not big enough, 171 // decompress the full size image. 172 Bitmap bitmap = null; 173 174 if (mPath != null) { 175 if (mIsVideo) { 176 bitmap = ThumbnailUtils.createVideoThumbnail(mPath, 177 Video.Thumbnails.MINI_KIND); 178 } else { 179 bitmap = ThumbnailUtils.createImageThumbnail(mPath, 180 Images.Thumbnails.MINI_KIND); 181 } 182 if (bitmap == null) { 183 Log.w(TAG, "Can't create mini thumbnail for " + mPath); 184 return; 185 } 186 187 Uri uri = updateDatabase(bitmap); 188 if (uri != null) { 189 OutputStream thumbOut = mCr.openOutputStream(uri); 190 bitmap.compress(Bitmap.CompressFormat.JPEG, 85, thumbOut); 191 thumbOut.close(); 192 } 193 } 194 195 bitmap = ThumbnailUtils.extractThumbnail(bitmap, 196 ThumbnailUtils.TARGET_SIZE_MICRO_THUMBNAIL, 197 ThumbnailUtils.TARGET_SIZE_MICRO_THUMBNAIL, 198 ThumbnailUtils.OPTIONS_RECYCLE_INPUT); 199 200 if (bitmap != null) { 201 ByteArrayOutputStream miniOutStream = new ByteArrayOutputStream(); 202 bitmap.compress(Bitmap.CompressFormat.JPEG, 75, miniOutStream); 203 bitmap.recycle(); 204 byte [] data = null; 205 206 try { 207 miniOutStream.close(); 208 data = miniOutStream.toByteArray(); 209 } catch (java.io.IOException ex) { 210 Log.e(TAG, "got exception ex " + ex); 211 } 212 213 // We may consider retire this proprietary format, after all it's size is only 214 // 128 x 128 at most, which is still reasonable to be stored in database. 215 // Gallery application can use the MINI_THUMB_MAGIC value to determine if it's 216 // time to query and fetch by using Cursor.getBlob 217 if (data != null) { 218 // make a new magic number since things are out of sync 219 do { 220 magic = sRandom.nextLong(); 221 } while (magic == 0); 222 223 miniThumbFile.saveMiniThumbToFile(data, mOrigId, magic); 224 ContentValues values = new ContentValues(); 225 // both video/images table use the same column name "mini_thumb_magic" 226 values.put(ImageColumns.MINI_THUMB_MAGIC, magic); 227 try { 228 mCr.update(mUri, values, null, null); 229 mMagic = magic; 230 } catch (java.lang.IllegalStateException ex) { 231 Log.e(TAG, "got exception while updating database " + ex); 232 } 233 } 234 } else { 235 Log.w(TAG, "can't create bitmap for thumbnail."); 236 } 237 miniThumbFile.deactivate(); 238 } 239 } 240