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 = 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      */
reset()62     public static synchronized void reset() {
63         for (MiniThumbFile file : sThumbFiles.values()) {
64             file.deactivate();
65         }
66         sThumbFiles.clear();
67     }
68 
instance(Uri uri)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 
randomAccessFilePath(int version)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 
removeOldFile()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 
miniThumbDataFile()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 
MiniThumbFile(Uri uri)131     private MiniThumbFile(Uri uri) {
132         mUri = uri;
133         mBuffer = ByteBuffer.allocateDirect(BYTES_PER_MINTHUMB);
134         mEmptyBuffer = ByteBuffer.allocateDirect(BYTES_PER_MINTHUMB);
135     }
136 
deactivate()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.
getMagic(long id)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 
eraseMiniThumb(long id)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 
saveMiniThumbToFile(byte[] data, long id, long magic)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      */
getMiniThumbFromFile(long id, byte [] data)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