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