MediaThumbRequest.java 9.15 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
/*
 * Copyright (C) 2009 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.providers.media;

19
import java.io.ByteArrayOutputStream;
20
import java.io.IOException;
21
import java.io.OutputStream;
22 23 24 25 26 27 28 29 30 31
import java.util.Comparator;
import java.util.Random;

import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.media.MiniThumbFile;
32
import android.media.ThumbnailUtils;
33
import android.net.Uri;
Ray Chen's avatar
Ray Chen committed
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 41 42 43 44 45 46 47 48 49 50 51
import android.provider.MediaStore.Images.ImageColumns;
import android.util.Log;

/**
 * Instances of this class are created and put in a queue to be executed sequentially to see if
 * it needs to (re)generate the thumbnails.
 */
class MediaThumbRequest {
    private static final String TAG = "MediaThumbRequest";
    static final int PRIORITY_LOW = 20;
    static final int PRIORITY_NORMAL = 10;
    static final int PRIORITY_HIGH = 5;
Ray Chen's avatar
Ray Chen committed
52 53
    static final int PRIORITY_CRITICAL = 0;
    static enum State {WAIT, DONE, CANCEL}
54 55 56 57 58 59 60
    private static final String[] THUMB_PROJECTION = new String[] {
        BaseColumns._ID // 0
    };

    ContentResolver mCr;
    String mPath;
    long mRequestTime = System.currentTimeMillis();
Ray Chen's avatar
Ray Chen committed
61
    int mCallingPid = Binder.getCallingPid();
62
    long mGroupId;
63 64
    int mPriority;
    Uri mUri;
65 66
    Uri mThumbUri;
    String mOrigColumnName;
67 68
    boolean mIsVideo;
    long mOrigId;
Ray Chen's avatar
Ray Chen committed
69
    State mState = State.WAIT;
70 71
    long mMagic;

72
    private static final Random sRandom = new Random();
73 74 75 76 77 78 79 80 81 82 83 84 85

    static Comparator<MediaThumbRequest> getComparator() {
        return new Comparator<MediaThumbRequest>() {
            public int compare(MediaThumbRequest r1, MediaThumbRequest r2) {
                if (r1.mPriority != r2.mPriority) {
                    return r1.mPriority < r2.mPriority ? -1 : 1;
                }
                return r1.mRequestTime == r2.mRequestTime ? 0 :
                        r1.mRequestTime < r2.mRequestTime ? -1 : 1;
            }
        };
    }

86
    MediaThumbRequest(ContentResolver cr, String path, Uri uri, int priority, long magic) {
87 88
        mCr = cr;
        mPath = path;
89
        mPriority = priority;
90
        mMagic = magic;
91 92 93
        mUri = uri;
        mIsVideo = "video".equals(uri.getPathSegments().get(1));
        mOrigId = ContentUris.parseId(uri);
94 95 96 97 98 99
        mThumbUri = mIsVideo
                ? Video.Thumbnails.EXTERNAL_CONTENT_URI
                : Images.Thumbnails.EXTERNAL_CONTENT_URI;
        mOrigColumnName = mIsVideo
                ? Video.Thumbnails.VIDEO_ID
                : Images.Thumbnails.IMAGE_ID;
100 101 102 103 104 105
        // Only requests from Thumbnail API has this group_id parameter. In other cases,
        // mGroupId will always be zero and can't be canceled due to pid mismatch.
        String groupIdParam = uri.getQueryParameter("group_id");
        if (groupIdParam != null) {
            mGroupId = Long.parseLong(groupIdParam);
        }
106 107
    }

Ray Chen's avatar
Ray Chen committed
108
    Uri updateDatabase(Bitmap thumbnail) {
109 110 111 112 113 114 115 116 117 118 119
        Cursor c = mCr.query(mThumbUri, THUMB_PROJECTION,
                mOrigColumnName+ " = " + mOrigId, null, null);
        if (c == null) return null;
        try {
            if (c.moveToFirst()) {
                return ContentUris.withAppendedId(mThumbUri, c.getLong(0));
            }
        } finally {
            if (c != null) c.close();
        }

Ray Chen's avatar
Ray Chen committed
120
        ContentValues values = new ContentValues(4);
121 122
        values.put(Images.Thumbnails.KIND, Images.Thumbnails.MINI_KIND);
        values.put(mOrigColumnName, mOrigId);
Ray Chen's avatar
Ray Chen committed
123 124
        values.put(Images.Thumbnails.WIDTH, thumbnail.getWidth());
        values.put(Images.Thumbnails.HEIGHT, thumbnail.getHeight());
125 126 127 128 129 130 131 132
        try {
            return mCr.insert(mThumbUri, values);
        } catch (Exception ex) {
            Log.w(TAG, ex);
            return null;
        }
    }

133 134 135 136 137 138 139 140 141 142 143 144 145 146 147
    /**
     * Check if the corresponding thumbnail and mini-thumb have been created
     * for the given uri. This method creates both of them if they do not
     * exist yet or have been changed since last check. After thumbnails are
     * created, MINI_KIND thumbnail is stored in JPEG file and MICRO_KIND
     * thumbnail is stored in a random access file (MiniThumbFile).
     *
     * @throws IOException
     */
    void execute() throws IOException {
        MiniThumbFile miniThumbFile = MiniThumbFile.instance(mUri);
        long magic = mMagic;
        if (magic != 0) {
            long fileMagic = miniThumbFile.getMagic(mOrigId);
            if (fileMagic == magic) {
148
                Cursor c = null;
149
                ParcelFileDescriptor pfd = null;
150 151
                // Clear calling identity as we may be handling an IPC.
                final long identity = Binder.clearCallingIdentity();
152
                try {
153 154
                    c = mCr.query(mThumbUri, THUMB_PROJECTION,
                            mOrigColumnName + " = " + mOrigId, null, null);
155 156
                    if (c != null && c.moveToFirst()) {
                        pfd = mCr.openFileDescriptor(
157
                                mThumbUri.buildUpon().appendPath(c.getString(0)).build(), "r");
158
                    }
159 160
                } catch (IOException ex) {
                    // MINI_THUMBNAIL not exists, ignore the exception and generate one.
161
                } finally {
162
                    Binder.restoreCallingIdentity(identity);
163 164 165 166 167 168
                    if (c != null) c.close();
                    if (pfd != null) {
                        pfd.close();
                        return;
                    }
                }
169 170 171 172 173 174 175 176 177 178
            }
        }

        // If we can't retrieve the thumbnail, first check if there is one
        // embedded in the EXIF data. If not, or it's not big enough,
        // decompress the full size image.
        Bitmap bitmap = null;

        if (mPath != null) {
            if (mIsVideo) {
179 180
                bitmap = ThumbnailUtils.createVideoThumbnail(mPath,
                        Video.Thumbnails.MINI_KIND);
181
            } else {
182 183 184 185 186 187 188 189
                bitmap = ThumbnailUtils.createImageThumbnail(mPath,
                        Images.Thumbnails.MINI_KIND);
            }
            if (bitmap == null) {
                Log.w(TAG, "Can't create mini thumbnail for " + mPath);
                return;
            }

Ray Chen's avatar
Ray Chen committed
190
            Uri uri = updateDatabase(bitmap);
191 192 193 194
            if (uri != null) {
                OutputStream thumbOut = mCr.openOutputStream(uri);
                bitmap.compress(Bitmap.CompressFormat.JPEG, 85, thumbOut);
                thumbOut.close();
195 196 197
            }
        }

198 199 200 201
        bitmap = ThumbnailUtils.extractThumbnail(bitmap,
                        ThumbnailUtils.TARGET_SIZE_MICRO_THUMBNAIL,
                        ThumbnailUtils.TARGET_SIZE_MICRO_THUMBNAIL,
                        ThumbnailUtils.OPTIONS_RECYCLE_INPUT);
202 203

        if (bitmap != null) {
204 205 206 207 208 209 210 211 212 213 214
            ByteArrayOutputStream miniOutStream = new ByteArrayOutputStream();
            bitmap.compress(Bitmap.CompressFormat.JPEG, 75, miniOutStream);
            bitmap.recycle();
            byte [] data = null;

            try {
                miniOutStream.close();
                data = miniOutStream.toByteArray();
            } catch (java.io.IOException ex) {
                Log.e(TAG, "got exception ex " + ex);
            }
215 216 217 218 219

            // We may consider retire this proprietary format, after all it's size is only
            // 128 x 128 at most, which is still reasonable to be stored in database.
            // Gallery application can use the MINI_THUMB_MAGIC value to determine if it's
            // time to query and fetch by using Cursor.getBlob
220
            if (data != null) {
221 222
                // make a new magic number since things are out of sync
                do {
223
                    magic = sRandom.nextLong();
224 225
                } while (magic == 0);

226 227 228 229
                miniThumbFile.saveMiniThumbToFile(data, mOrigId, magic);
                ContentValues values = new ContentValues();
                // both video/images table use the same column name "mini_thumb_magic"
                values.put(ImageColumns.MINI_THUMB_MAGIC, magic);
230 231 232 233 234 235
                try {
                    mCr.update(mUri, values, null, null);
                    mMagic = magic;
                } catch (java.lang.IllegalStateException ex) {
                    Log.e(TAG, "got exception while updating database " + ex);
                }
236
            }
237 238 239
        } else {
            Log.w(TAG, "can't create bitmap for thumbnail.");
        }
240
        miniThumbFile.deactivate();
241 242
    }
}