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