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 access.
57    private static Object sLock = new Object();
58
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
65    public Uri getUri() {
66        return mUri;
67    }
68
69    public Bitmap getBitmap() {
70        return mBitmap;
71    }
72
73    public void setFromFile(boolean fromFile) {
74        mFromFile = fromFile;
75    }
76
77    public boolean fromFile() {
78        return mFromFile;
79    }
80
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.
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.
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
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 {
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
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
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
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
256    public static Bitmap createVideoThumbnail(FileDescriptor fd, int targetWidth) {
257        return createVideoThumbnail(null, fd, targetWidth);
258    }
259
260    public static Bitmap createVideoThumbnail(String filePath, int targetWidth) {
261        return createVideoThumbnail(filePath, null, targetWidth);
262    }
263
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
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