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