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
17package com.android.providers.media;
18
19import java.io.ByteArrayOutputStream;
20import java.io.IOException;
21import java.io.OutputStream;
22import java.util.Comparator;
23import java.util.Random;
24
25import android.content.ContentResolver;
26import android.content.ContentUris;
27import android.content.ContentValues;
28import android.database.Cursor;
29import android.graphics.Bitmap;
30import android.graphics.BitmapFactory;
31import android.media.MiniThumbFile;
32import android.media.ThumbnailUtils;
33import android.net.Uri;
34import android.os.Binder;
35import android.os.ParcelFileDescriptor;
36import android.provider.BaseColumns;
37import android.provider.MediaStore.Images;
38import android.provider.MediaStore.Video;
39import android.provider.MediaStore.MediaColumns;
40import android.provider.MediaStore.Images.ImageColumns;
41import 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 */
47class 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
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                try {
151                    c = mCr.query(mThumbUri, THUMB_PROJECTION,
152                            mOrigColumnName + " = " + mOrigId, null, null);
153                    if (c != null && c.moveToFirst()) {
154                        pfd = mCr.openFileDescriptor(
155                                mThumbUri.buildUpon().appendPath(c.getString(0)).build(), "r");
156                    }
157                } catch (IOException ex) {
158                    // MINI_THUMBNAIL not exists, ignore the exception and generate one.
159                } finally {
160                    if (c != null) c.close();
161                    if (pfd != null) {
162                        pfd.close();
163                        return;
164                    }
165                }
166            }
167        }
168
169        // If we can't retrieve the thumbnail, first check if there is one
170        // embedded in the EXIF data. If not, or it's not big enough,
171        // decompress the full size image.
172        Bitmap bitmap = null;
173
174        if (mPath != null) {
175            if (mIsVideo) {
176                bitmap = ThumbnailUtils.createVideoThumbnail(mPath,
177                        Video.Thumbnails.MINI_KIND);
178            } else {
179                bitmap = ThumbnailUtils.createImageThumbnail(mPath,
180                        Images.Thumbnails.MINI_KIND);
181            }
182            if (bitmap == null) {
183                Log.w(TAG, "Can't create mini thumbnail for " + mPath);
184                return;
185            }
186
187            Uri uri = updateDatabase(bitmap);
188            if (uri != null) {
189                OutputStream thumbOut = mCr.openOutputStream(uri);
190                bitmap.compress(Bitmap.CompressFormat.JPEG, 85, thumbOut);
191                thumbOut.close();
192            }
193        }
194
195        bitmap = ThumbnailUtils.extractThumbnail(bitmap,
196                        ThumbnailUtils.TARGET_SIZE_MICRO_THUMBNAIL,
197                        ThumbnailUtils.TARGET_SIZE_MICRO_THUMBNAIL,
198                        ThumbnailUtils.OPTIONS_RECYCLE_INPUT);
199
200        if (bitmap != null) {
201            ByteArrayOutputStream miniOutStream = new ByteArrayOutputStream();
202            bitmap.compress(Bitmap.CompressFormat.JPEG, 75, miniOutStream);
203            bitmap.recycle();
204            byte [] data = null;
205
206            try {
207                miniOutStream.close();
208                data = miniOutStream.toByteArray();
209            } catch (java.io.IOException ex) {
210                Log.e(TAG, "got exception ex " + ex);
211            }
212
213            // We may consider retire this proprietary format, after all it's size is only
214            // 128 x 128 at most, which is still reasonable to be stored in database.
215            // Gallery application can use the MINI_THUMB_MAGIC value to determine if it's
216            // time to query and fetch by using Cursor.getBlob
217            if (data != null) {
218                // make a new magic number since things are out of sync
219                do {
220                    magic = sRandom.nextLong();
221                } while (magic == 0);
222
223                miniThumbFile.saveMiniThumbToFile(data, mOrigId, magic);
224                ContentValues values = new ContentValues();
225                // both video/images table use the same column name "mini_thumb_magic"
226                values.put(ImageColumns.MINI_THUMB_MAGIC, magic);
227                mCr.update(mUri, values, null, null);
228            }
229        } else {
230            Log.w(TAG, "can't create bitmap for thumbnail.");
231        }
232    }
233}
234