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