• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.ArrayMap;
23 import android.util.ArraySet;
24 import android.util.Log;
25 
26 import com.android.providers.contacts.ContactsDatabaseHelper.PhotoFilesColumns;
27 import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
28 
29 import com.google.common.annotations.VisibleForTesting;
30 
31 import java.io.File;
32 import java.io.FileOutputStream;
33 import java.io.IOException;
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 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      */
PhotoStore(File rootDirectory, ContactsDatabaseHelper databaseHelper)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      */
clear()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
getTotalSize()104     public long getTotalSize() {
105         return mTotalSize;
106     }
107 
108     /**
109      * Returns the entry with the specified key if it exists, null otherwise.
110      */
get(long key)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      */
initialize()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      */
cleanup(Set<Long> keysInUse)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      */
insert(PhotoProcessor photoProcessor)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      */
insert(PhotoProcessor photoProcessor, boolean allowSmallImageStorage)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 
cleanupFile(File file)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      */
remove(long id)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      */
getFileForPhotoFileId(long id)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      */
putEntry(long id, Entry entry)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      */
removeEntry(long id)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 
Entry(File file)292         public Entry(File file) {
293             id = Long.parseLong(file.getName());
294             size = file.length();
295             path = file.getAbsolutePath();
296         }
297     }
298 }
299