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 */ PhotoStore(File rootDirectory, ContactsDatabaseHelper databaseHelper)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 */ clear()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 getTotalSize()103 public long getTotalSize() { 104 return mTotalSize; 105 } 106 107 /** 108 * Returns the entry with the specified key if it exists, null otherwise. 109 */ get(long key)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 */ initialize()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 */ cleanup(Set<Long> keysInUse)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 */ insert(PhotoProcessor photoProcessor)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 */ insert(PhotoProcessor photoProcessor, boolean allowSmallImageStorage)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 cleanupFile(File file)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 */ remove(long id)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 */ getFileForPhotoFileId(long id)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 */ putEntry(long id, Entry entry)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 */ removeEntry(long id)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 Entry(File file)291 public Entry(File file) { 292 id = Long.parseLong(file.getName()); 293 size = file.length(); 294 path = file.getAbsolutePath(); 295 } 296 } 297 } 298