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 
17 package android.media;
18 
19 import android.net.Uri;
20 import android.os.Environment;
21 import android.util.Log;
22 
23 import java.io.File;
24 import java.io.IOException;
25 import java.io.RandomAccessFile;
26 import java.nio.ByteBuffer;
27 import java.nio.channels.FileChannel;
28 import java.nio.channels.FileLock;
29 import 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  */
45 public 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      */
reset()61     public static synchronized void reset() {
62         for (MiniThumbFile file : sThumbFiles.values()) {
63             file.deactivate();
64         }
65         sThumbFiles.clear();
66     }
67 
instance(Uri uri)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 
randomAccessFilePath(int version)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 
removeOldFile()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 
miniThumbDataFile()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 
MiniThumbFile(Uri uri)130     public MiniThumbFile(Uri uri) {
131         mUri = uri;
132         mBuffer = ByteBuffer.allocateDirect(BYTES_PER_MINTHUMB);
133     }
134 
deactivate()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.
getMagic(long id)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 
saveMiniThumbToFile(byte[] data, long id, long magic)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      */
getMiniThumbFromFile(long id, byte [] data)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