MiniThumbFile.java revision ef093cd6c4ab4d3c8a1c8be5ed7147d5f06d7027
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 android.media;
18
19import android.graphics.Bitmap;
20import android.media.ThumbnailUtil;
21import android.net.Uri;
22import android.os.Environment;
23import android.util.Log;
24
25import java.io.File;
26import java.io.IOException;
27import java.io.RandomAccessFile;
28import java.nio.ByteBuffer;
29import java.nio.channels.FileChannel;
30import java.nio.channels.FileLock;
31import java.util.Hashtable;
32
33/**
34 * This class handles the mini-thumb file. A mini-thumb file consists
35 * of blocks, indexed by id. Each block has BYTES_PER_MINTHUMB bytes in the
36 * following format:
37 *
38 * 1 byte status (0 = empty, 1 = mini-thumb available)
39 * 8 bytes magic (a magic number to match what's in the database)
40 * 4 bytes data length (LEN)
41 * LEN bytes jpeg data
42 * (the remaining bytes are unused)
43 *
44 * @hide This file is shared between MediaStore and MediaProvider and should remained internal use
45 *       only.
46 */
47public class MiniThumbFile {
48    private static final String TAG = "MiniThumbFile";
49    private static final int MINI_THUMB_DATA_FILE_VERSION = 3;
50    public static final int BYTES_PER_MINTHUMB = 10000;
51    private static final int HEADER_SIZE = 1 + 8 + 4;
52    private Uri mUri;
53    private RandomAccessFile mMiniThumbFile;
54    private FileChannel mChannel;
55    private ByteBuffer mBuffer;
56    private static Hashtable<String, MiniThumbFile> sThumbFiles =
57        new Hashtable<String, MiniThumbFile>();
58
59    /**
60     * We store different types of thumbnails in different files. To remain backward compatibility,
61     * we should hashcode of content://media/external/images/media remains the same.
62     */
63    public static synchronized void reset() {
64        for (MiniThumbFile file : sThumbFiles.values()) {
65            file.deactivate();
66        }
67        sThumbFiles.clear();
68    }
69
70    public static synchronized MiniThumbFile instance(Uri uri) {
71        String type = uri.getPathSegments().get(1);
72        MiniThumbFile file = sThumbFiles.get(type);
73        // Log.v(TAG, "get minithumbfile for type: "+type);
74        if (file == null) {
75            file = new MiniThumbFile(
76                    Uri.parse("content://media/external/" + type + "/media"));
77            sThumbFiles.put(type, file);
78        }
79
80        return file;
81    }
82
83    private String randomAccessFilePath(int version) {
84        String directoryName =
85                Environment.getExternalStorageDirectory().toString()
86                + "/DCIM/.thumbnails";
87        return directoryName + "/.thumbdata" + version + "-" + mUri.hashCode();
88    }
89
90    private void removeOldFile() {
91        String oldPath = randomAccessFilePath(MINI_THUMB_DATA_FILE_VERSION - 1);
92        File oldFile = new File(oldPath);
93        if (oldFile.exists()) {
94            try {
95                oldFile.delete();
96            } catch (SecurityException ex) {
97                // ignore
98            }
99        }
100    }
101
102    private RandomAccessFile miniThumbDataFile() {
103        if (mMiniThumbFile == null) {
104            removeOldFile();
105            String path = randomAccessFilePath(MINI_THUMB_DATA_FILE_VERSION);
106            File directory = new File(path).getParentFile();
107            if (!directory.isDirectory()) {
108                if (!directory.mkdirs()) {
109                    Log.e(TAG, "Unable to create .thumbnails directory "
110                            + directory.toString());
111                }
112            }
113            File f = new File(path);
114            try {
115                mMiniThumbFile = new RandomAccessFile(f, "rw");
116            } catch (IOException ex) {
117                // Open as read-only so we can at least read the existing
118                // thumbnails.
119                try {
120                    mMiniThumbFile = new RandomAccessFile(f, "r");
121                } catch (IOException ex2) {
122                    // ignore exception
123                }
124            }
125            if (mMiniThumbFile != null) {
126                mChannel = mMiniThumbFile.getChannel();
127            }
128        }
129        return mMiniThumbFile;
130    }
131
132    public MiniThumbFile(Uri uri) {
133        mUri = uri;
134        mBuffer = ByteBuffer.allocateDirect(BYTES_PER_MINTHUMB);
135    }
136
137    public synchronized void deactivate() {
138        if (mMiniThumbFile != null) {
139            try {
140                mMiniThumbFile.close();
141                mMiniThumbFile = null;
142            } catch (IOException ex) {
143                // ignore exception
144            }
145        }
146    }
147
148    // Get the magic number for the specified id in the mini-thumb file.
149    // Returns 0 if the magic is not available.
150    public synchronized long getMagic(long id) {
151        // check the mini thumb file for the right data.  Right is
152        // defined as having the right magic number at the offset
153        // reserved for this "id".
154        RandomAccessFile r = miniThumbDataFile();
155        if (r != null) {
156            long pos = id * BYTES_PER_MINTHUMB;
157            FileLock lock = null;
158            try {
159                mBuffer.clear();
160                mBuffer.limit(1 + 8);
161
162                lock = mChannel.lock(pos, 1 + 8, true);
163                // check that we can read the following 9 bytes
164                // (1 for the "status" and 8 for the long)
165                if (mChannel.read(mBuffer, pos) == 9) {
166                    mBuffer.position(0);
167                    if (mBuffer.get() == 1) {
168                        return mBuffer.getLong();
169                    }
170                }
171            } catch (IOException ex) {
172                Log.v(TAG, "Got exception checking file magic: ", ex);
173            } catch (RuntimeException ex) {
174                // Other NIO related exception like disk full, read only channel..etc
175                Log.e(TAG, "Got exception when reading magic, id = " + id +
176                        ", disk full or mount read-only? " + ex.getClass());
177            } finally {
178                try {
179                    if (lock != null) lock.release();
180                }
181                catch (IOException ex) {
182                    // ignore it.
183                }
184            }
185        }
186        return 0;
187    }
188
189    public synchronized void saveMiniThumbToFile(byte[] data, long id, long magic)
190            throws IOException {
191        RandomAccessFile r = miniThumbDataFile();
192        if (r == null) return;
193
194        long pos = id * BYTES_PER_MINTHUMB;
195        FileLock lock = null;
196        try {
197            if (data != null) {
198                if (data.length > BYTES_PER_MINTHUMB - HEADER_SIZE) {
199                    // not enough space to store it.
200                    return;
201                }
202                mBuffer.clear();
203                mBuffer.put((byte) 1);
204                mBuffer.putLong(magic);
205                mBuffer.putInt(data.length);
206                mBuffer.put(data);
207                mBuffer.flip();
208
209                lock = mChannel.lock(pos, BYTES_PER_MINTHUMB, false);
210                mChannel.write(mBuffer, pos);
211            }
212        } catch (IOException ex) {
213            Log.e(TAG, "couldn't save mini thumbnail data for "
214                    + id + "; ", ex);
215            throw ex;
216        } catch (RuntimeException ex) {
217            // Other NIO related exception like disk full, read only channel..etc
218            Log.e(TAG, "couldn't save mini thumbnail data for "
219                    + id + "; disk full or mount read-only? " + ex.getClass());
220        } finally {
221            try {
222                if (lock != null) lock.release();
223            }
224            catch (IOException ex) {
225                // ignore it.
226            }
227        }
228    }
229
230    /**
231     * Gallery app can use this method to retrieve mini-thumbnail. Full size
232     * images share the same IDs with their corresponding thumbnails.
233     *
234     * @param id the ID of the image (same of full size image).
235     * @param data the buffer to store mini-thumbnail.
236     */
237    public synchronized byte [] getMiniThumbFromFile(long id, byte [] data) {
238        RandomAccessFile r = miniThumbDataFile();
239        if (r == null) return null;
240
241        long pos = id * BYTES_PER_MINTHUMB;
242        FileLock lock = null;
243        try {
244            mBuffer.clear();
245            lock = mChannel.lock(pos, BYTES_PER_MINTHUMB, true);
246            int size = mChannel.read(mBuffer, pos);
247            if (size > 1 + 8 + 4) { // flag, magic, length
248                mBuffer.position(0);
249                byte flag = mBuffer.get();
250                long magic = mBuffer.getLong();
251                int length = mBuffer.getInt();
252
253                if (size >= 1 + 8 + 4 + length && data.length >= length) {
254                    mBuffer.get(data, 0, length);
255                    return data;
256                }
257            }
258        } catch (IOException ex) {
259            Log.w(TAG, "got exception when reading thumbnail id=" + id + ", exception: " + ex);
260        } catch (RuntimeException ex) {
261            // Other NIO related exception like disk full, read only channel..etc
262            Log.e(TAG, "Got exception when reading thumbnail, id = " + id +
263                    ", disk full or mount read-only? " + ex.getClass());
264        } finally {
265            try {
266                if (lock != null) lock.release();
267            }
268            catch (IOException ex) {
269                // ignore it.
270            }
271        }
272        return null;
273    }
274}
275