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