Home | History | Annotate | Download | only in contacts
      1 /*
      2  * Copyright (C) 2011 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
      5  * use this file except in compliance with the License. You may obtain a copy of
      6  * 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, WITHOUT
     12  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
     13  * License for the specific language governing permissions and limitations under
     14  * the License
     15  */
     16 package com.android.providers.contacts;
     17 
     18 import android.content.ContentValues;
     19 import android.database.sqlite.SQLiteDatabase;
     20 import android.graphics.Bitmap;
     21 import android.provider.ContactsContract.PhotoFiles;
     22 import android.util.Log;
     23 
     24 import com.android.providers.contacts.ContactsDatabaseHelper.PhotoFilesColumns;
     25 import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
     26 import com.google.common.annotations.VisibleForTesting;
     27 
     28 import java.io.File;
     29 import java.io.FileOutputStream;
     30 import java.io.IOException;
     31 import java.util.HashMap;
     32 import java.util.HashSet;
     33 import java.util.Map;
     34 import java.util.Set;
     35 
     36 /**
     37  * Photo storage system that stores the files directly onto the hard disk
     38  * in the specified directory.
     39  */
     40 public class PhotoStore {
     41 
     42     private final String TAG = PhotoStore.class.getSimpleName();
     43 
     44     // Directory name under the root directory for photo storage.
     45     private final String DIRECTORY = "photos";
     46 
     47     /** Map of keys to entries in the directory. */
     48     private final Map<Long, Entry> mEntries;
     49 
     50     /** Total amount of space currently used by the photo store in bytes. */
     51     private long mTotalSize = 0;
     52 
     53     /** The file path for photo storage. */
     54     private final File mStorePath;
     55 
     56     /** The database helper. */
     57     private final ContactsDatabaseHelper mDatabaseHelper;
     58 
     59     /** The database to use for storing metadata for the photo files. */
     60     private SQLiteDatabase mDb;
     61 
     62     /**
     63      * Constructs an instance of the PhotoStore under the specified directory.
     64      * @param rootDirectory The root directory of the storage.
     65      * @param databaseHelper Helper class for obtaining a database instance.
     66      */
     67     public PhotoStore(File rootDirectory, ContactsDatabaseHelper databaseHelper) {
     68         mStorePath = new File(rootDirectory, DIRECTORY);
     69         if (!mStorePath.exists()) {
     70             if(!mStorePath.mkdirs()) {
     71                 throw new RuntimeException("Unable to create photo storage directory "
     72                         + mStorePath.getPath());
     73             }
     74         }
     75         mDatabaseHelper = databaseHelper;
     76         mEntries = new HashMap<Long, Entry>();
     77         initialize();
     78     }
     79 
     80     /**
     81      * Clears the photo storage. Deletes all files from disk.
     82      */
     83     public void clear() {
     84         File[] files = mStorePath.listFiles();
     85         if (files != null) {
     86             for (File file : files) {
     87                 cleanupFile(file);
     88             }
     89         }
     90         if (mDb == null) {
     91             mDb = mDatabaseHelper.getWritableDatabase();
     92         }
     93         mDb.delete(Tables.PHOTO_FILES, null, null);
     94         mEntries.clear();
     95         mTotalSize = 0;
     96     }
     97 
     98     @VisibleForTesting
     99     public long getTotalSize() {
    100         return mTotalSize;
    101     }
    102 
    103     /**
    104      * Returns the entry with the specified key if it exists, null otherwise.
    105      */
    106     public Entry get(long key) {
    107         return mEntries.get(key);
    108     }
    109 
    110     /**
    111      * Initializes the PhotoStore by scanning for all files currently in the
    112      * specified root directory.
    113      */
    114     public final void initialize() {
    115         File[] files = mStorePath.listFiles();
    116         if (files == null) {
    117             return;
    118         }
    119         for (File file : files) {
    120             try {
    121                 Entry entry = new Entry(file);
    122                 putEntry(entry.id, entry);
    123             } catch (NumberFormatException nfe) {
    124                 // Not a valid photo store entry - delete the file.
    125                 cleanupFile(file);
    126             }
    127         }
    128 
    129         // Get a reference to the database.
    130         mDb = mDatabaseHelper.getWritableDatabase();
    131     }
    132 
    133     /**
    134      * Cleans up the photo store such that only the keys in use still remain as
    135      * entries in the store (all other entries are deleted).
    136      *
    137      * If an entry in the keys in use does not exist in the photo store, that key
    138      * will be returned in the result set - the caller should take steps to clean
    139      * up those references, as the underlying photo entries do not exist.
    140      *
    141      * @param keysInUse The set of all keys that are in use in the photo store.
    142      * @return The set of the keys in use that refer to non-existent entries.
    143      */
    144     public Set<Long> cleanup(Set<Long> keysInUse) {
    145         Set<Long> keysToRemove = new HashSet<Long>();
    146         keysToRemove.addAll(mEntries.keySet());
    147         keysToRemove.removeAll(keysInUse);
    148         if (!keysToRemove.isEmpty()) {
    149             Log.d(TAG, "cleanup removing " + keysToRemove.size() + " entries");
    150             for (long key : keysToRemove) {
    151                 remove(key);
    152             }
    153         }
    154 
    155         Set<Long> missingKeys = new HashSet<Long>();
    156         missingKeys.addAll(keysInUse);
    157         missingKeys.removeAll(mEntries.keySet());
    158         return missingKeys;
    159     }
    160 
    161     /**
    162      * Inserts the photo in the given photo processor into the photo store.  If the display photo
    163      * is already thumbnail-sized or smaller, this will do nothing (and will return 0).
    164      * @param photoProcessor A photo processor containing the photo data to insert.
    165      * @return The photo file ID associated with the file, or 0 if the file could not be created or
    166      *     is thumbnail-sized or smaller.
    167      */
    168     public long insert(PhotoProcessor photoProcessor) {
    169         return insert(photoProcessor, false);
    170     }
    171 
    172     /**
    173      * Inserts the photo in the given photo processor into the photo store.  If the display photo
    174      * is already thumbnail-sized or smaller, this will do nothing (and will return 0) unless
    175      * allowSmallImageStorage is specified.
    176      * @param photoProcessor A photo processor containing the photo data to insert.
    177      * @param allowSmallImageStorage Whether thumbnail-sized or smaller photos should still be
    178      *     stored in the file store.
    179      * @return The photo file ID associated with the file, or 0 if the file could not be created or
    180      *     is thumbnail-sized or smaller and allowSmallImageStorage is false.
    181      */
    182     public long insert(PhotoProcessor photoProcessor, boolean allowSmallImageStorage) {
    183         Bitmap displayPhoto = photoProcessor.getDisplayPhoto();
    184         int width = displayPhoto.getWidth();
    185         int height = displayPhoto.getHeight();
    186         int thumbnailDim = photoProcessor.getMaxThumbnailPhotoDim();
    187         if (allowSmallImageStorage || width > thumbnailDim || height > thumbnailDim) {
    188             // Write the photo to a temp file, create the DB record for tracking it, and rename the
    189             // temp file to match.
    190             File file = null;
    191             try {
    192                 // Write the display photo to a temp file.
    193                 byte[] photoBytes = photoProcessor.getDisplayPhotoBytes();
    194                 file = File.createTempFile("img", null, mStorePath);
    195                 FileOutputStream fos = new FileOutputStream(file);
    196                 fos.write(photoBytes);
    197                 fos.close();
    198 
    199                 // Create the DB entry.
    200                 ContentValues values = new ContentValues();
    201                 values.put(PhotoFiles.HEIGHT, height);
    202                 values.put(PhotoFiles.WIDTH, width);
    203                 values.put(PhotoFiles.FILESIZE, photoBytes.length);
    204                 long id = mDb.insert(Tables.PHOTO_FILES, null, values);
    205                 if (id != 0) {
    206                     // Rename the temp file.
    207                     File target = getFileForPhotoFileId(id);
    208                     if (file.renameTo(target)) {
    209                         Entry entry = new Entry(target);
    210                         putEntry(entry.id, entry);
    211                         return id;
    212                     }
    213                 }
    214             } catch (IOException e) {
    215                 // Write failed - will delete the file below.
    216             }
    217 
    218             // If anything went wrong, clean up the file before returning.
    219             if (file != null) {
    220                 cleanupFile(file);
    221             }
    222         }
    223         return 0;
    224     }
    225 
    226     private void cleanupFile(File file) {
    227         boolean deleted = file.delete();
    228         if (!deleted) {
    229             Log.d("Could not clean up file %s", file.getAbsolutePath());
    230         }
    231     }
    232 
    233     /**
    234      * Removes the specified photo file from the store if it exists.
    235      */
    236     public void remove(long id) {
    237         cleanupFile(getFileForPhotoFileId(id));
    238         removeEntry(id);
    239     }
    240 
    241     /**
    242      * Returns a file object for the given photo file ID.
    243      */
    244     private File getFileForPhotoFileId(long id) {
    245         return new File(mStorePath, String.valueOf(id));
    246     }
    247 
    248     /**
    249      * Puts the entry with the specified photo file ID into the store.
    250      * @param id The photo file ID to identify the entry by.
    251      * @param entry The entry to store.
    252      */
    253     private void putEntry(long id, Entry entry) {
    254         if (!mEntries.containsKey(id)) {
    255             mTotalSize += entry.size;
    256         } else {
    257             Entry oldEntry = mEntries.get(id);
    258             mTotalSize += (entry.size - oldEntry.size);
    259         }
    260         mEntries.put(id, entry);
    261     }
    262 
    263     /**
    264      * Removes the entry identified by the given photo file ID from the store, removing
    265      * the associated photo file entry from the database.
    266      */
    267     private void removeEntry(long id) {
    268         Entry entry = mEntries.get(id);
    269         if (entry != null) {
    270             mTotalSize -= entry.size;
    271             mEntries.remove(id);
    272         }
    273         mDb.delete(ContactsDatabaseHelper.Tables.PHOTO_FILES, PhotoFilesColumns.CONCRETE_ID + "=?",
    274                 new String[]{String.valueOf(id)});
    275     }
    276 
    277     public static class Entry {
    278         /** The photo file ID that identifies the entry. */
    279         public final long id;
    280 
    281         /** The size of the data, in bytes. */
    282         public final long size;
    283 
    284         /** The path to the file. */
    285         public final String path;
    286 
    287         public Entry(File file) {
    288             id = Long.parseLong(file.getName());
    289             size = file.length();
    290             path = file.getAbsolutePath();
    291         }
    292     }
    293 }
    294