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