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