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