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