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.net.Uri;
21import android.os.Environment;
22import android.util.Log;
23
24import java.io.File;
25import java.io.IOException;
26import java.io.RandomAccessFile;
27import java.nio.ByteBuffer;
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    private static final String TAG = "MiniThumbFile";
48    private static final int MINI_THUMB_DATA_FILE_VERSION = 3;
49    public static final int BYTES_PER_MINTHUMB = 10000;
50    private static final int HEADER_SIZE = 1 + 8 + 4;
51    private Uri mUri;
52    private RandomAccessFile mMiniThumbFile;
53    private FileChannel mChannel;
54    private ByteBuffer mBuffer;
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    public MiniThumbFile(Uri uri) {
132        mUri = uri;
133        mBuffer = ByteBuffer.allocateDirect(BYTES_PER_MINTHUMB);
134    }
135
136    public synchronized void deactivate() {
137        if (mMiniThumbFile != null) {
138            try {
139                mMiniThumbFile.close();
140                mMiniThumbFile = null;
141            } catch (IOException ex) {
142                // ignore exception
143            }
144        }
145    }
146
147    // Get the magic number for the specified id in the mini-thumb file.
148    // Returns 0 if the magic is not available.
149    public synchronized long getMagic(long id) {
150        // check the mini thumb file for the right data.  Right is
151        // defined as having the right magic number at the offset
152        // reserved for this "id".
153        RandomAccessFile r = miniThumbDataFile();
154        if (r != null) {
155            long pos = id * BYTES_PER_MINTHUMB;
156            FileLock lock = null;
157            try {
158                mBuffer.clear();
159                mBuffer.limit(1 + 8);
160
161                lock = mChannel.lock(pos, 1 + 8, true);
162                // check that we can read the following 9 bytes
163                // (1 for the "status" and 8 for the long)
164                if (mChannel.read(mBuffer, pos) == 9) {
165                    mBuffer.position(0);
166                    if (mBuffer.get() == 1) {
167                        return mBuffer.getLong();
168                    }
169                }
170            } catch (IOException ex) {
171                Log.v(TAG, "Got exception checking file magic: ", ex);
172            } catch (RuntimeException ex) {
173                // Other NIO related exception like disk full, read only channel..etc
174                Log.e(TAG, "Got exception when reading magic, id = " + id +
175                        ", disk full or mount read-only? " + ex.getClass());
176            } finally {
177                try {
178                    if (lock != null) lock.release();
179                }
180                catch (IOException ex) {
181                    // ignore it.
182                }
183            }
184        }
185        return 0;
186    }
187
188    public synchronized void saveMiniThumbToFile(byte[] data, long id, long magic)
189            throws IOException {
190        RandomAccessFile r = miniThumbDataFile();
191        if (r == null) return;
192
193        long pos = id * BYTES_PER_MINTHUMB;
194        FileLock lock = null;
195        try {
196            if (data != null) {
197                if (data.length > BYTES_PER_MINTHUMB - HEADER_SIZE) {
198                    // not enough space to store it.
199                    return;
200                }
201                mBuffer.clear();
202                mBuffer.put((byte) 1);
203                mBuffer.putLong(magic);
204                mBuffer.putInt(data.length);
205                mBuffer.put(data);
206                mBuffer.flip();
207
208                lock = mChannel.lock(pos, BYTES_PER_MINTHUMB, false);
209                mChannel.write(mBuffer, pos);
210            }
211        } catch (IOException ex) {
212            Log.e(TAG, "couldn't save mini thumbnail data for "
213                    + id + "; ", ex);
214            throw ex;
215        } catch (RuntimeException ex) {
216            // Other NIO related exception like disk full, read only channel..etc
217            Log.e(TAG, "couldn't save mini thumbnail data for "
218                    + id + "; disk full or mount read-only? " + ex.getClass());
219        } finally {
220            try {
221                if (lock != null) lock.release();
222            }
223            catch (IOException ex) {
224                // ignore it.
225            }
226        }
227    }
228
229    /**
230     * Gallery app can use this method to retrieve mini-thumbnail. Full size
231     * images share the same IDs with their corresponding thumbnails.
232     *
233     * @param id the ID of the image (same of full size image).
234     * @param data the buffer to store mini-thumbnail.
235     */
236    public synchronized byte [] getMiniThumbFromFile(long id, byte [] data) {
237        RandomAccessFile r = miniThumbDataFile();
238        if (r == null) return null;
239
240        long pos = id * BYTES_PER_MINTHUMB;
241        FileLock lock = null;
242        try {
243            mBuffer.clear();
244            lock = mChannel.lock(pos, BYTES_PER_MINTHUMB, true);
245            int size = mChannel.read(mBuffer, pos);
246            if (size > 1 + 8 + 4) { // flag, magic, length
247                mBuffer.position(0);
248                byte flag = mBuffer.get();
249                long magic = mBuffer.getLong();
250                int length = mBuffer.getInt();
251
252                if (size >= 1 + 8 + 4 + length && 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