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.graphics.Bitmap; 20 import android.net.Uri; 21 import android.os.Environment; 22 import android.util.Log; 23 24 import java.io.File; 25 import java.io.IOException; 26 import java.io.RandomAccessFile; 27 import java.nio.ByteBuffer; 28 import java.nio.channels.FileChannel; 29 import java.nio.channels.FileLock; 30 import 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 */ 46 public 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