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