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.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      */
     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     private MiniThumbFile(Uri uri) {
    132         mUri = uri;
    133         mBuffer = ByteBuffer.allocateDirect(BYTES_PER_MINTHUMB);
    134         mEmptyBuffer = ByteBuffer.allocateDirect(BYTES_PER_MINTHUMB);
    135     }
    136 
    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.
    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 
    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 
    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      */
    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