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 = 3;
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 static final Hashtable<String, MiniThumbFile> sThumbFiles =
55        new Hashtable<String, MiniThumbFile>();
56
57    /**
58     * We store different types of thumbnails in different files. To remain backward compatibility,
59     * we should hashcode of content://media/external/images/media remains the same.
60     */
61    public static synchronized void reset() {
62        for (MiniThumbFile file : sThumbFiles.values()) {
63            file.deactivate();
64        }
65        sThumbFiles.clear();
66    }
67
68    public static synchronized MiniThumbFile instance(Uri uri) {
69        String type = uri.getPathSegments().get(1);
70        MiniThumbFile file = sThumbFiles.get(type);
71        // Log.v(TAG, "get minithumbfile for type: "+type);
72        if (file == null) {
73            file = new MiniThumbFile(
74                    Uri.parse("content://media/external/" + type + "/media"));
75            sThumbFiles.put(type, file);
76        }
77
78        return file;
79    }
80
81    private String randomAccessFilePath(int version) {
82        String directoryName =
83                Environment.getExternalStorageDirectory().toString()
84                + "/DCIM/.thumbnails";
85        return directoryName + "/.thumbdata" + version + "-" + mUri.hashCode();
86    }
87
88    private void removeOldFile() {
89        String oldPath = randomAccessFilePath(MINI_THUMB_DATA_FILE_VERSION - 1);
90        File oldFile = new File(oldPath);
91        if (oldFile.exists()) {
92            try {
93                oldFile.delete();
94            } catch (SecurityException ex) {
95                // ignore
96            }
97        }
98    }
99
100    private RandomAccessFile miniThumbDataFile() {
101        if (mMiniThumbFile == null) {
102            removeOldFile();
103            String path = randomAccessFilePath(MINI_THUMB_DATA_FILE_VERSION);
104            File directory = new File(path).getParentFile();
105            if (!directory.isDirectory()) {
106                if (!directory.mkdirs()) {
107                    Log.e(TAG, "Unable to create .thumbnails directory "
108                            + directory.toString());
109                }
110            }
111            File f = new File(path);
112            try {
113                mMiniThumbFile = new RandomAccessFile(f, "rw");
114            } catch (IOException ex) {
115                // Open as read-only so we can at least read the existing
116                // thumbnails.
117                try {
118                    mMiniThumbFile = new RandomAccessFile(f, "r");
119                } catch (IOException ex2) {
120                    // ignore exception
121                }
122            }
123            if (mMiniThumbFile != null) {
124                mChannel = mMiniThumbFile.getChannel();
125            }
126        }
127        return mMiniThumbFile;
128    }
129
130    public MiniThumbFile(Uri uri) {
131        mUri = uri;
132        mBuffer = ByteBuffer.allocateDirect(BYTES_PER_MINTHUMB);
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 synchronized 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                mBuffer.clear();
158                mBuffer.limit(1 + 8);
159
160                lock = mChannel.lock(pos, 1 + 8, true);
161                // check that we can read the following 9 bytes
162                // (1 for the "status" and 8 for the long)
163                if (mChannel.read(mBuffer, pos) == 9) {
164                    mBuffer.position(0);
165                    if (mBuffer.get() == 1) {
166                        return mBuffer.getLong();
167                    }
168                }
169            } catch (IOException ex) {
170                Log.v(TAG, "Got exception checking file magic: ", ex);
171            } catch (RuntimeException ex) {
172                // Other NIO related exception like disk full, read only channel..etc
173                Log.e(TAG, "Got exception when reading magic, id = " + id +
174                        ", disk full or mount read-only? " + ex.getClass());
175            } finally {
176                try {
177                    if (lock != null) lock.release();
178                }
179                catch (IOException ex) {
180                    // ignore it.
181                }
182            }
183        }
184        return 0;
185    }
186
187    public synchronized void saveMiniThumbToFile(byte[] data, long id, long magic)
188            throws IOException {
189        RandomAccessFile r = miniThumbDataFile();
190        if (r == null) return;
191
192        long pos = id * BYTES_PER_MINTHUMB;
193        FileLock lock = null;
194        try {
195            if (data != null) {
196                if (data.length > BYTES_PER_MINTHUMB - HEADER_SIZE) {
197                    // not enough space to store it.
198                    return;
199                }
200                mBuffer.clear();
201                mBuffer.put((byte) 1);
202                mBuffer.putLong(magic);
203                mBuffer.putInt(data.length);
204                mBuffer.put(data);
205                mBuffer.flip();
206
207                lock = mChannel.lock(pos, BYTES_PER_MINTHUMB, false);
208                mChannel.write(mBuffer, pos);
209            }
210        } catch (IOException ex) {
211            Log.e(TAG, "couldn't save mini thumbnail data for "
212                    + id + "; ", ex);
213            throw ex;
214        } catch (RuntimeException ex) {
215            // Other NIO related exception like disk full, read only channel..etc
216            Log.e(TAG, "couldn't save mini thumbnail data for "
217                    + id + "; disk full or mount read-only? " + ex.getClass());
218        } finally {
219            try {
220                if (lock != null) lock.release();
221            }
222            catch (IOException ex) {
223                // ignore it.
224            }
225        }
226    }
227
228    /**
229     * Gallery app can use this method to retrieve mini-thumbnail. Full size
230     * images share the same IDs with their corresponding thumbnails.
231     *
232     * @param id the ID of the image (same of full size image).
233     * @param data the buffer to store mini-thumbnail.
234     */
235    public synchronized byte [] getMiniThumbFromFile(long id, byte [] data) {
236        RandomAccessFile r = miniThumbDataFile();
237        if (r == null) return null;
238
239        long pos = id * BYTES_PER_MINTHUMB;
240        FileLock lock = null;
241        try {
242            mBuffer.clear();
243            lock = mChannel.lock(pos, BYTES_PER_MINTHUMB, true);
244            int size = mChannel.read(mBuffer, pos);
245            if (size > 1 + 8 + 4) { // flag, magic, length
246                mBuffer.position(0);
247                byte flag = mBuffer.get();
248                long magic = mBuffer.getLong();
249                int length = mBuffer.getInt();
250
251                if (size >= 1 + 8 + 4 + length && length != 0 && magic != 0 && flag == 1 &&
252                        data.length >= length) {
253                    mBuffer.get(data, 0, length);
254                    return data;
255                }
256            }
257        } catch (IOException ex) {
258            Log.w(TAG, "got exception when reading thumbnail id=" + id + ", exception: " + ex);
259        } catch (RuntimeException ex) {
260            // Other NIO related exception like disk full, read only channel..etc
261            Log.e(TAG, "Got exception when reading thumbnail, id = " + id +
262                    ", disk full or mount read-only? " + ex.getClass());
263        } finally {
264            try {
265                if (lock != null) lock.release();
266            }
267            catch (IOException ex) {
268                // ignore it.
269            }
270        }
271        return null;
272    }
273}
274