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