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.net.Uri;
20import android.os.Environment;
21import android.util.Log;
22
23import java.io.File;
24import java.io.IOException;
25import java.io.RandomAccessFile;
26import java.nio.ByteBuffer;
27import java.nio.channels.FileChannel;
28import java.nio.channels.FileLock;
29import java.util.Hashtable;
30
31/**
32 * This class handles the mini-thumb file. A mini-thumb file consists
33 * of blocks, indexed by id. Each block has BYTES_PER_MINTHUMB bytes in the
34 * following format:
35 *
36 * 1 byte status (0 = empty, 1 = mini-thumb available)
37 * 8 bytes magic (a magic number to match what's in the database)
38 * 4 bytes data length (LEN)
39 * LEN bytes jpeg data
40 * (the remaining bytes are unused)
41 *
42 * @hide This file is shared between MediaStore and MediaProvider and should remained internal use
43 *       only.
44 */
45public class MiniThumbFile {
46    private static final String TAG = "MiniThumbFile";
47    private static final int MINI_THUMB_DATA_FILE_VERSION = 4;
48    public static final int BYTES_PER_MINTHUMB = 10000;
49    private static final int HEADER_SIZE = 1 + 8 + 4;
50    private Uri mUri;
51    private RandomAccessFile mMiniThumbFile;
52    private FileChannel mChannel;
53    private ByteBuffer mBuffer;
54    private ByteBuffer mEmptyBuffer;
55    private static final Hashtable<String, MiniThumbFile> sThumbFiles =
56        new Hashtable<String, MiniThumbFile>();
57
58    /**
59     * We store different types of thumbnails in different files. To remain backward compatibility,
60     * we should hashcode of content://media/external/images/media remains the same.
61     */
62    public static synchronized void reset() {
63        for (MiniThumbFile file : sThumbFiles.values()) {
64            file.deactivate();
65        }
66        sThumbFiles.clear();
67    }
68
69    public static synchronized MiniThumbFile instance(Uri uri) {
70        String type = uri.getPathSegments().get(1);
71        MiniThumbFile file = sThumbFiles.get(type);
72        // Log.v(TAG, "get minithumbfile for type: "+type);
73        if (file == null) {
74            file = new MiniThumbFile(
75                    Uri.parse("content://media/external/" + type + "/media"));
76            sThumbFiles.put(type, file);
77        }
78
79        return file;
80    }
81
82    private String randomAccessFilePath(int version) {
83        String directoryName =
84                Environment.getExternalStorageDirectory().toString()
85                + "/DCIM/.thumbnails";
86        return directoryName + "/.thumbdata" + version + "-" + mUri.hashCode();
87    }
88
89    private void removeOldFile() {
90        String oldPath = randomAccessFilePath(MINI_THUMB_DATA_FILE_VERSION - 1);
91        File oldFile = new File(oldPath);
92        if (oldFile.exists()) {
93            try {
94                oldFile.delete();
95            } catch (SecurityException ex) {
96                // ignore
97            }
98        }
99    }
100
101    private RandomAccessFile miniThumbDataFile() {
102        if (mMiniThumbFile == null) {
103            removeOldFile();
104            String path = randomAccessFilePath(MINI_THUMB_DATA_FILE_VERSION);
105            File directory = new File(path).getParentFile();
106            if (!directory.isDirectory()) {
107                if (!directory.mkdirs()) {
108                    Log.e(TAG, "Unable to create .thumbnails directory "
109                            + directory.toString());
110                }
111            }
112            File f = new File(path);
113            try {
114                mMiniThumbFile = new RandomAccessFile(f, "rw");
115            } catch (IOException ex) {
116                // Open as read-only so we can at least read the existing
117                // thumbnails.
118                try {
119                    mMiniThumbFile = new RandomAccessFile(f, "r");
120                } catch (IOException ex2) {
121                    // ignore exception
122                }
123            }
124            if (mMiniThumbFile != null) {
125                mChannel = mMiniThumbFile.getChannel();
126            }
127        }
128        return mMiniThumbFile;
129    }
130
131    private MiniThumbFile(Uri uri) {
132        mUri = uri;
133        mBuffer = ByteBuffer.allocateDirect(BYTES_PER_MINTHUMB);
134        mEmptyBuffer = 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 eraseMiniThumb(long id) {
190        RandomAccessFile r = miniThumbDataFile();
191        if (r != null) {
192            long pos = id * BYTES_PER_MINTHUMB;
193            FileLock lock = null;
194            try {
195                mBuffer.clear();
196                mBuffer.limit(1 + 8);
197
198                lock = mChannel.lock(pos, BYTES_PER_MINTHUMB, false);
199                // check that we can read the following 9 bytes
200                // (1 for the "status" and 8 for the long)
201                if (mChannel.read(mBuffer, pos) == 9) {
202                    mBuffer.position(0);
203                    if (mBuffer.get() == 1) {
204                        long currentMagic = mBuffer.getLong();
205                        if (currentMagic == 0) {
206                            // there is no thumbnail stored here
207                            Log.i(TAG, "no thumbnail for id " + id);
208                            return;
209                        }
210                        // zero out the thumbnail slot
211                        // Log.v(TAG, "clearing slot " + id + ", magic " + currentMagic
212                        //         + " at offset " + pos);
213                        mChannel.write(mEmptyBuffer, pos);
214                    }
215                } else {
216                    // Log.v(TAG, "No slot");
217                }
218            } catch (IOException ex) {
219                Log.v(TAG, "Got exception checking file magic: ", ex);
220            } catch (RuntimeException ex) {
221                // Other NIO related exception like disk full, read only channel..etc
222                Log.e(TAG, "Got exception when reading magic, id = " + id +
223                        ", disk full or mount read-only? " + ex.getClass());
224            } finally {
225                try {
226                    if (lock != null) lock.release();
227                }
228                catch (IOException ex) {
229                    // ignore it.
230                }
231            }
232        } else {
233            // Log.v(TAG, "No data file");
234        }
235    }
236
237    public synchronized void saveMiniThumbToFile(byte[] data, long id, long magic)
238            throws IOException {
239        RandomAccessFile r = miniThumbDataFile();
240        if (r == null) return;
241
242        long pos = id * BYTES_PER_MINTHUMB;
243        FileLock lock = null;
244        try {
245            if (data != null) {
246                if (data.length > BYTES_PER_MINTHUMB - HEADER_SIZE) {
247                    // not enough space to store it.
248                    return;
249                }
250                mBuffer.clear();
251                mBuffer.put((byte) 1);
252                mBuffer.putLong(magic);
253                mBuffer.putInt(data.length);
254                mBuffer.put(data);
255                mBuffer.flip();
256
257                lock = mChannel.lock(pos, BYTES_PER_MINTHUMB, false);
258                mChannel.write(mBuffer, pos);
259            }
260        } catch (IOException ex) {
261            Log.e(TAG, "couldn't save mini thumbnail data for "
262                    + id + "; ", ex);
263            throw ex;
264        } catch (RuntimeException ex) {
265            // Other NIO related exception like disk full, read only channel..etc
266            Log.e(TAG, "couldn't save mini thumbnail data for "
267                    + id + "; disk full or mount read-only? " + ex.getClass());
268        } finally {
269            try {
270                if (lock != null) lock.release();
271            }
272            catch (IOException ex) {
273                // ignore it.
274            }
275        }
276    }
277
278    /**
279     * Gallery app can use this method to retrieve mini-thumbnail. Full size
280     * images share the same IDs with their corresponding thumbnails.
281     *
282     * @param id the ID of the image (same of full size image).
283     * @param data the buffer to store mini-thumbnail.
284     */
285    public synchronized byte [] getMiniThumbFromFile(long id, byte [] data) {
286        RandomAccessFile r = miniThumbDataFile();
287        if (r == null) return null;
288
289        long pos = id * BYTES_PER_MINTHUMB;
290        FileLock lock = null;
291        try {
292            mBuffer.clear();
293            lock = mChannel.lock(pos, BYTES_PER_MINTHUMB, true);
294            int size = mChannel.read(mBuffer, pos);
295            if (size > 1 + 8 + 4) { // flag, magic, length
296                mBuffer.position(0);
297                byte flag = mBuffer.get();
298                long magic = mBuffer.getLong();
299                int length = mBuffer.getInt();
300
301                if (size >= 1 + 8 + 4 + length && length != 0 && magic != 0 && flag == 1 &&
302                        data.length >= length) {
303                    mBuffer.get(data, 0, length);
304                    return data;
305                }
306            }
307        } catch (IOException ex) {
308            Log.w(TAG, "got exception when reading thumbnail id=" + id + ", exception: " + ex);
309        } catch (RuntimeException ex) {
310            // Other NIO related exception like disk full, read only channel..etc
311            Log.e(TAG, "Got exception when reading thumbnail, id = " + id +
312                    ", disk full or mount read-only? " + ex.getClass());
313        } finally {
314            try {
315                if (lock != null) lock.release();
316            }
317            catch (IOException ex) {
318                // ignore it.
319            }
320        }
321        return null;
322    }
323}
324