Home | History | Annotate | Download | only in media
      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