/* * Copyright (C) 2011 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License */ package com.android.providers.contacts; import com.android.providers.contacts.ContactsDatabaseHelper.PhotoFilesColumns; import com.android.providers.contacts.ContactsDatabaseHelper.Tables; import com.google.common.annotations.VisibleForTesting; import android.content.ContentValues; import android.database.sqlite.SQLiteDatabase; import android.graphics.Bitmap; import android.provider.ContactsContract.PhotoFiles; import android.util.Log; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; /** * Photo storage system that stores the files directly onto the hard disk * in the specified directory. */ public class PhotoStore { private final String TAG = PhotoStore.class.getSimpleName(); // Directory name under the root directory for photo storage. private final String DIRECTORY = "photos"; /** Map of keys to entries in the directory. */ private final Map<Long, Entry> mEntries; /** Total amount of space currently used by the photo store in bytes. */ private long mTotalSize = 0; /** The file path for photo storage. */ private final File mStorePath; /** The database helper. */ private final ContactsDatabaseHelper mDatabaseHelper; /** The database to use for storing metadata for the photo files. */ private SQLiteDatabase mDb; /** * Constructs an instance of the PhotoStore under the specified directory. * @param rootDirectory The root directory of the storage. * @param databaseHelper Helper class for obtaining a database instance. */ public PhotoStore(File rootDirectory, ContactsDatabaseHelper databaseHelper) { mStorePath = new File(rootDirectory, DIRECTORY); if (!mStorePath.exists()) { if(!mStorePath.mkdirs()) { throw new RuntimeException("Unable to create photo storage directory " + mStorePath.getPath()); } } mDatabaseHelper = databaseHelper; mEntries = new HashMap<Long, Entry>(); initialize(); } /** * Clears the photo storage. Deletes all files from disk. */ public void clear() { File[] files = mStorePath.listFiles(); if (files != null) { for (File file : files) { cleanupFile(file); } } if (mDb == null) { mDb = mDatabaseHelper.getWritableDatabase(); } mDb.delete(Tables.PHOTO_FILES, null, null); mEntries.clear(); mTotalSize = 0; } @VisibleForTesting public long getTotalSize() { return mTotalSize; } /** * Returns the entry with the specified key if it exists, null otherwise. */ public Entry get(long key) { return mEntries.get(key); } /** * Initializes the PhotoStore by scanning for all files currently in the * specified root directory. */ public final void initialize() { File[] files = mStorePath.listFiles(); if (files == null) { return; } for (File file : files) { try { Entry entry = new Entry(file); putEntry(entry.id, entry); } catch (NumberFormatException nfe) { // Not a valid photo store entry - delete the file. cleanupFile(file); } } // Get a reference to the database. mDb = mDatabaseHelper.getWritableDatabase(); } /** * Cleans up the photo store such that only the keys in use still remain as * entries in the store (all other entries are deleted). * * If an entry in the keys in use does not exist in the photo store, that key * will be returned in the result set - the caller should take steps to clean * up those references, as the underlying photo entries do not exist. * * @param keysInUse The set of all keys that are in use in the photo store. * @return The set of the keys in use that refer to non-existent entries. */ public Set<Long> cleanup(Set<Long> keysInUse) { Set<Long> keysToRemove = new HashSet<Long>(); keysToRemove.addAll(mEntries.keySet()); keysToRemove.removeAll(keysInUse); if (!keysToRemove.isEmpty()) { Log.d(TAG, "cleanup removing " + keysToRemove.size() + " entries"); for (long key : keysToRemove) { remove(key); } } Set<Long> missingKeys = new HashSet<Long>(); missingKeys.addAll(keysInUse); missingKeys.removeAll(mEntries.keySet()); return missingKeys; } /** * Inserts the photo in the given photo processor into the photo store. If the display photo * is already thumbnail-sized or smaller, this will do nothing (and will return 0). * @param photoProcessor A photo processor containing the photo data to insert. * @return The photo file ID associated with the file, or 0 if the file could not be created or * is thumbnail-sized or smaller. */ public long insert(PhotoProcessor photoProcessor) { return insert(photoProcessor, false); } /** * Inserts the photo in the given photo processor into the photo store. If the display photo * is already thumbnail-sized or smaller, this will do nothing (and will return 0) unless * allowSmallImageStorage is specified. * @param photoProcessor A photo processor containing the photo data to insert. * @param allowSmallImageStorage Whether thumbnail-sized or smaller photos should still be * stored in the file store. * @return The photo file ID associated with the file, or 0 if the file could not be created or * is thumbnail-sized or smaller and allowSmallImageStorage is false. */ public long insert(PhotoProcessor photoProcessor, boolean allowSmallImageStorage) { Bitmap displayPhoto = photoProcessor.getDisplayPhoto(); int width = displayPhoto.getWidth(); int height = displayPhoto.getHeight(); int thumbnailDim = photoProcessor.getMaxThumbnailPhotoDim(); if (allowSmallImageStorage || width > thumbnailDim || height > thumbnailDim) { // Write the photo to a temp file, create the DB record for tracking it, and rename the // temp file to match. File file = null; try { // Write the display photo to a temp file. byte[] photoBytes = photoProcessor.getDisplayPhotoBytes(); file = File.createTempFile("img", null, mStorePath); FileOutputStream fos = new FileOutputStream(file); fos.write(photoProcessor.getDisplayPhotoBytes()); fos.close(); // Create the DB entry. ContentValues values = new ContentValues(); values.put(PhotoFiles.HEIGHT, height); values.put(PhotoFiles.WIDTH, width); values.put(PhotoFiles.FILESIZE, photoBytes.length); long id = mDb.insert(Tables.PHOTO_FILES, null, values); if (id != 0) { // Rename the temp file. File target = getFileForPhotoFileId(id); if (file.renameTo(target)) { Entry entry = new Entry(target); putEntry(entry.id, entry); return id; } } } catch (IOException e) { // Write failed - will delete the file below. } // If anything went wrong, clean up the file before returning. if (file != null) { cleanupFile(file); } } return 0; } private void cleanupFile(File file) { boolean deleted = file.delete(); if (!deleted) { Log.d("Could not clean up file %s", file.getAbsolutePath()); } } /** * Removes the specified photo file from the store if it exists. */ public void remove(long id) { cleanupFile(getFileForPhotoFileId(id)); removeEntry(id); } /** * Returns a file object for the given photo file ID. */ private File getFileForPhotoFileId(long id) { return new File(mStorePath, String.valueOf(id)); } /** * Puts the entry with the specified photo file ID into the store. * @param id The photo file ID to identify the entry by. * @param entry The entry to store. */ private void putEntry(long id, Entry entry) { if (!mEntries.containsKey(id)) { mTotalSize += entry.size; } else { Entry oldEntry = mEntries.get(id); mTotalSize += (entry.size - oldEntry.size); } mEntries.put(id, entry); } /** * Removes the entry identified by the given photo file ID from the store, removing * the associated photo file entry from the database. */ private void removeEntry(long id) { Entry entry = mEntries.get(id); if (entry != null) { mTotalSize -= entry.size; mEntries.remove(id); } mDb.delete(ContactsDatabaseHelper.Tables.PHOTO_FILES, PhotoFilesColumns.CONCRETE_ID + "=?", new String[]{String.valueOf(id)}); } public static class Entry { /** The photo file ID that identifies the entry. */ public final long id; /** The size of the data, in bytes. */ public final long size; /** The path to the file. */ public final String path; public Entry(File file) { id = Long.parseLong(file.getName()); size = file.length(); path = file.getAbsolutePath(); } } }