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