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.ArrayMap; 23import android.util.ArraySet; 24import android.util.Log; 25 26import com.android.providers.contacts.ContactsDatabaseHelper.PhotoFilesColumns; 27import com.android.providers.contacts.ContactsDatabaseHelper.Tables; 28 29import com.google.common.annotations.VisibleForTesting; 30 31import java.io.File; 32import java.io.FileOutputStream; 33import java.io.IOException; 34import java.util.Map; 35import java.util.Set; 36 37/** 38 * Photo storage system that stores the files directly onto the hard disk 39 * in the specified directory. 40 */ 41public 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