1 /*
2  * Copyright (C) 2011 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.camera;
18 
19 import android.content.ContentResolver;
20 import android.content.ContentUris;
21 import android.database.Cursor;
22 import android.graphics.Bitmap;
23 import android.graphics.BitmapFactory;
24 import android.graphics.Matrix;
25 import android.media.MediaMetadataRetriever;
26 import android.net.Uri;
27 import android.provider.MediaStore.Images;
28 import android.provider.MediaStore.Images.ImageColumns;
29 import android.provider.MediaStore.MediaColumns;
30 import android.provider.MediaStore.Video;
31 import android.provider.MediaStore.Video.VideoColumns;
32 import android.util.Log;
33 
34 import java.io.BufferedInputStream;
35 import java.io.BufferedOutputStream;
36 import java.io.DataInputStream;
37 import java.io.DataOutputStream;
38 import java.io.File;
39 import java.io.FileDescriptor;
40 import java.io.FileInputStream;
41 import java.io.FileOutputStream;
42 import java.io.IOException;
43 
44 public class Thumbnail {
45     private static final String TAG = "Thumbnail";
46 
47     public static final String LAST_THUMB_FILENAME = "last_thumb";
48     private static final int BUFSIZE = 4096;
49 
50     private Uri mUri;
51     private Bitmap mBitmap;
52     // whether this thumbnail is read from file
53     private boolean mFromFile = false;
54 
55     // Camera, VideoCamera, and Panorama share the same thumbnail. Use sLock
56     // to serialize the access.
57     private static Object sLock = new Object();
58 
Thumbnail(Uri uri, Bitmap bitmap, int orientation)59     public Thumbnail(Uri uri, Bitmap bitmap, int orientation) {
60         mUri = uri;
61         mBitmap = rotateImage(bitmap, orientation);
62         if (mBitmap == null) throw new IllegalArgumentException("null bitmap");
63     }
64 
getUri()65     public Uri getUri() {
66         return mUri;
67     }
68 
getBitmap()69     public Bitmap getBitmap() {
70         return mBitmap;
71     }
72 
setFromFile(boolean fromFile)73     public void setFromFile(boolean fromFile) {
74         mFromFile = fromFile;
75     }
76 
fromFile()77     public boolean fromFile() {
78         return mFromFile;
79     }
80 
rotateImage(Bitmap bitmap, int orientation)81     private static Bitmap rotateImage(Bitmap bitmap, int orientation) {
82         if (orientation != 0) {
83             // We only rotate the thumbnail once even if we get OOM.
84             Matrix m = new Matrix();
85             m.setRotate(orientation, bitmap.getWidth() * 0.5f,
86                     bitmap.getHeight() * 0.5f);
87 
88             try {
89                 Bitmap rotated = Bitmap.createBitmap(bitmap, 0, 0,
90                         bitmap.getWidth(), bitmap.getHeight(), m, true);
91                 // If the rotated bitmap is the original bitmap, then it
92                 // should not be recycled.
93                 if (rotated != bitmap) bitmap.recycle();
94                 return rotated;
95             } catch (Throwable t) {
96                 Log.w(TAG, "Failed to rotate thumbnail", t);
97             }
98         }
99         return bitmap;
100     }
101 
102     // Stores the bitmap to the specified file.
saveTo(File file)103     public void saveTo(File file) {
104         FileOutputStream f = null;
105         BufferedOutputStream b = null;
106         DataOutputStream d = null;
107         synchronized (sLock) {
108             try {
109                 f = new FileOutputStream(file);
110                 b = new BufferedOutputStream(f, BUFSIZE);
111                 d = new DataOutputStream(b);
112                 d.writeUTF(mUri.toString());
113                 mBitmap.compress(Bitmap.CompressFormat.JPEG, 90, d);
114                 d.close();
115             } catch (IOException e) {
116                 Log.e(TAG, "Fail to store bitmap. path=" + file.getPath(), e);
117             } finally {
118                 Util.closeSilently(f);
119                 Util.closeSilently(b);
120                 Util.closeSilently(d);
121             }
122         }
123     }
124 
125     // Loads the data from the specified file.
126     // Returns null if failure.
loadFrom(File file)127     public static Thumbnail loadFrom(File file) {
128         Uri uri = null;
129         Bitmap bitmap = null;
130         FileInputStream f = null;
131         BufferedInputStream b = null;
132         DataInputStream d = null;
133         synchronized (sLock) {
134             try {
135                 f = new FileInputStream(file);
136                 b = new BufferedInputStream(f, BUFSIZE);
137                 d = new DataInputStream(b);
138                 uri = Uri.parse(d.readUTF());
139                 bitmap = BitmapFactory.decodeStream(d);
140                 d.close();
141             } catch (IOException e) {
142                 Log.i(TAG, "Fail to load bitmap. " + e);
143                 return null;
144             } finally {
145                 Util.closeSilently(f);
146                 Util.closeSilently(b);
147                 Util.closeSilently(d);
148             }
149         }
150         Thumbnail thumbnail = createThumbnail(uri, bitmap, 0);
151         if (thumbnail != null) thumbnail.setFromFile(true);
152         return thumbnail;
153     }
154 
getLastThumbnail(ContentResolver resolver)155     public static Thumbnail getLastThumbnail(ContentResolver resolver) {
156         Media image = getLastImageThumbnail(resolver);
157         Media video = getLastVideoThumbnail(resolver);
158         if (image == null && video == null) return null;
159 
160         Bitmap bitmap = null;
161         Media lastMedia;
162         // If there is only image or video, get its thumbnail. If both exist,
163         // get the thumbnail of the one that is newer.
164         if (image != null && (video == null || image.dateTaken >= video.dateTaken)) {
165             bitmap = Images.Thumbnails.getThumbnail(resolver, image.id,
166                     Images.Thumbnails.MINI_KIND, null);
167             lastMedia = image;
168         } else {
169             bitmap = Video.Thumbnails.getThumbnail(resolver, video.id,
170                     Video.Thumbnails.MINI_KIND, null);
171             lastMedia = video;
172         }
173 
174         // Ensure database and storage are in sync.
175         if (Util.isUriValid(lastMedia.uri, resolver)) {
176             return createThumbnail(lastMedia.uri, bitmap, lastMedia.orientation);
177         }
178         return null;
179     }
180 
181     private static class Media {
Media(long id, int orientation, long dateTaken, Uri uri)182         public Media(long id, int orientation, long dateTaken, Uri uri) {
183             this.id = id;
184             this.orientation = orientation;
185             this.dateTaken = dateTaken;
186             this.uri = uri;
187         }
188 
189         public final long id;
190         public final int orientation;
191         public final long dateTaken;
192         public final Uri uri;
193     }
194 
getLastImageThumbnail(ContentResolver resolver)195     public static Media getLastImageThumbnail(ContentResolver resolver) {
196         Uri baseUri = Images.Media.EXTERNAL_CONTENT_URI;
197 
198         Uri query = baseUri.buildUpon().appendQueryParameter("limit", "1").build();
199         String[] projection = new String[] {ImageColumns._ID, ImageColumns.ORIENTATION,
200                 ImageColumns.DATE_TAKEN};
201         String selection = ImageColumns.MIME_TYPE + "='image/jpeg' AND " +
202                 ImageColumns.BUCKET_ID + '=' + Storage.BUCKET_ID;
203         String order = ImageColumns.DATE_TAKEN + " DESC," + ImageColumns._ID + " DESC";
204 
205         Cursor cursor = null;
206         try {
207             cursor = resolver.query(query, projection, selection, null, order);
208             if (cursor != null && cursor.moveToFirst()) {
209                 long id = cursor.getLong(0);
210                 return new Media(id, cursor.getInt(1), cursor.getLong(2),
211                         ContentUris.withAppendedId(baseUri, id));
212             }
213         } finally {
214             if (cursor != null) {
215                 cursor.close();
216             }
217         }
218         return null;
219     }
220 
getLastVideoThumbnail(ContentResolver resolver)221     private static Media getLastVideoThumbnail(ContentResolver resolver) {
222         Uri baseUri = Video.Media.EXTERNAL_CONTENT_URI;
223 
224         Uri query = baseUri.buildUpon().appendQueryParameter("limit", "1").build();
225         String[] projection = new String[] {VideoColumns._ID, MediaColumns.DATA,
226                 VideoColumns.DATE_TAKEN};
227         String selection = VideoColumns.BUCKET_ID + '=' + Storage.BUCKET_ID;
228         String order = VideoColumns.DATE_TAKEN + " DESC," + VideoColumns._ID + " DESC";
229 
230         Cursor cursor = null;
231         try {
232             cursor = resolver.query(query, projection, selection, null, order);
233             if (cursor != null && cursor.moveToFirst()) {
234                 Log.d(TAG, "getLastVideoThumbnail: " + cursor.getString(1));
235                 long id = cursor.getLong(0);
236                 return new Media(id, 0, cursor.getLong(2),
237                         ContentUris.withAppendedId(baseUri, id));
238             }
239         } finally {
240             if (cursor != null) {
241                 cursor.close();
242             }
243         }
244         return null;
245     }
246 
createThumbnail(byte[] jpeg, int orientation, int inSampleSize, Uri uri)247     public static Thumbnail createThumbnail(byte[] jpeg, int orientation, int inSampleSize,
248             Uri uri) {
249         // Create the thumbnail.
250         BitmapFactory.Options options = new BitmapFactory.Options();
251         options.inSampleSize = inSampleSize;
252         Bitmap bitmap = BitmapFactory.decodeByteArray(jpeg, 0, jpeg.length, options);
253         return createThumbnail(uri, bitmap, orientation);
254     }
255 
createVideoThumbnail(FileDescriptor fd, int targetWidth)256     public static Bitmap createVideoThumbnail(FileDescriptor fd, int targetWidth) {
257         return createVideoThumbnail(null, fd, targetWidth);
258     }
259 
createVideoThumbnail(String filePath, int targetWidth)260     public static Bitmap createVideoThumbnail(String filePath, int targetWidth) {
261         return createVideoThumbnail(filePath, null, targetWidth);
262     }
263 
createVideoThumbnail(String filePath, FileDescriptor fd, int targetWidth)264     private static Bitmap createVideoThumbnail(String filePath, FileDescriptor fd, int targetWidth) {
265         Bitmap bitmap = null;
266         MediaMetadataRetriever retriever = new MediaMetadataRetriever();
267         try {
268             if (filePath != null) {
269                 retriever.setDataSource(filePath);
270             } else {
271                 retriever.setDataSource(fd);
272             }
273             bitmap = retriever.getFrameAtTime(-1);
274         } catch (IllegalArgumentException ex) {
275             // Assume this is a corrupt video file
276         } catch (RuntimeException ex) {
277             // Assume this is a corrupt video file.
278         } finally {
279             try {
280                 retriever.release();
281             } catch (RuntimeException ex) {
282                 // Ignore failures while cleaning up.
283             }
284         }
285         if (bitmap == null) return null;
286 
287         // Scale down the bitmap if it is bigger than we need.
288         int width = bitmap.getWidth();
289         int height = bitmap.getHeight();
290         if (width > targetWidth) {
291             float scale = (float) targetWidth / width;
292             int w = Math.round(scale * width);
293             int h = Math.round(scale * height);
294             bitmap = Bitmap.createScaledBitmap(bitmap, w, h, true);
295         }
296         return bitmap;
297     }
298 
createThumbnail(Uri uri, Bitmap bitmap, int orientation)299     private static Thumbnail createThumbnail(Uri uri, Bitmap bitmap, int orientation) {
300         if (bitmap == null) {
301             Log.e(TAG, "Failed to create thumbnail from null bitmap");
302             return null;
303         }
304         try {
305             return new Thumbnail(uri, bitmap, orientation);
306         } catch (IllegalArgumentException e) {
307             Log.e(TAG, "Failed to construct thumbnail", e);
308             return null;
309         }
310     }
311 }
312