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