1 /*
2  * Copyright (C) 2013 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of 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,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.photos.data;
17 
18 import android.content.ContentResolver;
19 import android.content.ContentUris;
20 import android.content.ContentValues;
21 import android.content.Context;
22 import android.content.UriMatcher;
23 import android.database.Cursor;
24 import android.database.DatabaseUtils;
25 import android.database.sqlite.SQLiteDatabase;
26 import android.database.sqlite.SQLiteOpenHelper;
27 import android.database.sqlite.SQLiteQueryBuilder;
28 import android.media.ExifInterface;
29 import android.net.Uri;
30 import android.os.CancellationSignal;
31 import android.provider.BaseColumns;
32 
33 import com.android.gallery3d.common.ApiHelper;
34 
35 import java.util.List;
36 
37 /**
38  * A provider that gives access to photo and video information for media stored
39  * on the server. Only media that is or will be put on the server will be
40  * accessed by this provider. Use Photos.CONTENT_URI to query all photos and
41  * videos. Use Albums.CONTENT_URI to query all albums. Use Metadata.CONTENT_URI
42  * to query metadata about a photo or video, based on the ID of the media. Use
43  * ImageCache.THUMBNAIL_CONTENT_URI, ImageCache.PREVIEW_CONTENT_URI, or
44  * ImageCache.ORIGINAL_CONTENT_URI to query the path of the thumbnail, preview,
45  * or original-sized image respectfully. <br/>
46  * To add or update metadata, use the update function rather than insert. All
47  * values for the metadata must be in the ContentValues, even if they are also
48  * in the selection. The selection and selectionArgs are not used when updating
49  * metadata. If the metadata values are null, the row will be deleted.
50  */
51 public class PhotoProvider extends SQLiteContentProvider {
52     @SuppressWarnings("unused")
53     private static final String TAG = PhotoProvider.class.getSimpleName();
54 
55     protected static final String DB_NAME = "photo.db";
56     public static final String AUTHORITY = PhotoProviderAuthority.AUTHORITY;
57     static final Uri BASE_CONTENT_URI = new Uri.Builder().scheme("content").authority(AUTHORITY)
58             .build();
59 
60     // Used to allow mocking out the change notification because
61     // MockContextResolver disallows system-wide notification.
62     public static interface ChangeNotification {
notifyChange(Uri uri, boolean syncToNetwork)63         void notifyChange(Uri uri, boolean syncToNetwork);
64     }
65 
66     /**
67      * Contains columns that can be accessed via Accounts.CONTENT_URI
68      */
69     public static interface Accounts extends BaseColumns {
70         /**
71          * Internal database table used for account information
72          */
73         public static final String TABLE = "accounts";
74         /**
75          * Content URI for account information
76          */
77         public static final Uri CONTENT_URI = Uri.withAppendedPath(BASE_CONTENT_URI, TABLE);
78         /**
79          * User name for this account.
80          */
81         public static final String ACCOUNT_NAME = "name";
82     }
83 
84     /**
85      * Contains columns that can be accessed via Photos.CONTENT_URI.
86      */
87     public static interface Photos extends BaseColumns {
88         /**
89          * The image_type query parameter required for requesting a specific
90          * size of image.
91          */
92         public static final String MEDIA_SIZE_QUERY_PARAMETER = "media_size";
93 
94         /** Internal database table used for basic photo information. */
95         public static final String TABLE = "photos";
96         /** Content URI for basic photo and video information. */
97         public static final Uri CONTENT_URI = Uri.withAppendedPath(BASE_CONTENT_URI, TABLE);
98 
99         /** Long foreign key to Accounts._ID */
100         public static final String ACCOUNT_ID = "account_id";
101         /** Column name for the width of the original image. Integer value. */
102         public static final String WIDTH = "width";
103         /** Column name for the height of the original image. Integer value. */
104         public static final String HEIGHT = "height";
105         /**
106          * Column name for the date that the original image was taken. Long
107          * value indicating the milliseconds since epoch in the GMT time zone.
108          */
109         public static final String DATE_TAKEN = "date_taken";
110         /**
111          * Column name indicating the long value of the album id that this image
112          * resides in. Will be NULL if it it has not been uploaded to the
113          * server.
114          */
115         public static final String ALBUM_ID = "album_id";
116         /** The column name for the mime-type String. */
117         public static final String MIME_TYPE = "mime_type";
118         /** The title of the photo. String value. */
119         public static final String TITLE = "title";
120         /** The date the photo entry was last updated. Long value. */
121         public static final String DATE_MODIFIED = "date_modified";
122         /**
123          * The rotation of the photo in degrees, if rotation has not already
124          * been applied. Integer value.
125          */
126         public static final String ROTATION = "rotation";
127     }
128 
129     /**
130      * Contains columns and Uri for accessing album information.
131      */
132     public static interface Albums extends BaseColumns {
133         /** Internal database table used album information. */
134         public static final String TABLE = "albums";
135         /** Content URI for album information. */
136         public static final Uri CONTENT_URI = Uri.withAppendedPath(BASE_CONTENT_URI, TABLE);
137 
138         /** Long foreign key to Accounts._ID */
139         public static final String ACCOUNT_ID = "account_id";
140         /** Parent directory or null if this is in the root. */
141         public static final String PARENT_ID = "parent_id";
142         /** The type of album. Non-null, if album is auto-generated. String value. */
143         public static final String ALBUM_TYPE = "album_type";
144         /**
145          * Column name for the visibility level of the album. Can be any of the
146          * VISIBILITY_* values.
147          */
148         public static final String VISIBILITY = "visibility";
149         /** The user-specified location associated with the album. String value. */
150         public static final String LOCATION_STRING = "location_string";
151         /** The title of the album. String value. */
152         public static final String TITLE = "title";
153         /** A short summary of the contents of the album. String value. */
154         public static final String SUMMARY = "summary";
155         /** The date the album was created. Long value */
156         public static final String DATE_PUBLISHED = "date_published";
157         /** The date the album entry was last updated. Long value. */
158         public static final String DATE_MODIFIED = "date_modified";
159 
160         // Privacy values for Albums.VISIBILITY
161         public static final int VISIBILITY_PRIVATE = 1;
162         public static final int VISIBILITY_SHARED = 2;
163         public static final int VISIBILITY_PUBLIC = 3;
164     }
165 
166     /**
167      * Contains columns and Uri for accessing photo and video metadata
168      */
169     public static interface Metadata extends BaseColumns {
170         /** Internal database table used metadata information. */
171         public static final String TABLE = "metadata";
172         /** Content URI for photo and video metadata. */
173         public static final Uri CONTENT_URI = Uri.withAppendedPath(BASE_CONTENT_URI, TABLE);
174         /** Foreign key to photo_id. Long value. */
175         public static final String PHOTO_ID = "photo_id";
176         /** Metadata key. String value */
177         public static final String KEY = "key";
178         /**
179          * Metadata value. Type is based on key.
180          */
181         public static final String VALUE = "value";
182 
183         /** A short summary of the photo. String value. */
184         public static final String KEY_SUMMARY = "summary";
185         /** The date the photo was added. Long value. */
186         public static final String KEY_PUBLISHED = "date_published";
187         /** The date the photo was last updated. Long value. */
188         public static final String KEY_DATE_UPDATED = "date_updated";
189         /** The size of the photo is bytes. Integer value. */
190         public static final String KEY_SIZE_IN_BTYES = "size";
191         /** The latitude associated with the photo. Double value. */
192         public static final String KEY_LATITUDE = "latitude";
193         /** The longitude associated with the photo. Double value. */
194         public static final String KEY_LONGITUDE = "longitude";
195 
196         /** The make of the camera used. String value. */
197         public static final String KEY_EXIF_MAKE = ExifInterface.TAG_MAKE;
198         /** The model of the camera used. String value. */
199         public static final String KEY_EXIF_MODEL = ExifInterface.TAG_MODEL;;
200         /** The exposure time used. Float value. */
201         public static final String KEY_EXIF_EXPOSURE = ExifInterface.TAG_EXPOSURE_TIME;
202         /** Whether the flash was used. Boolean value. */
203         public static final String KEY_EXIF_FLASH = ExifInterface.TAG_FLASH;
204         /** The focal length used. Float value. */
205         public static final String KEY_EXIF_FOCAL_LENGTH = ExifInterface.TAG_FOCAL_LENGTH;
206         /** The fstop value used. Float value. */
207         public static final String KEY_EXIF_FSTOP = ExifInterface.TAG_APERTURE;
208         /** The ISO equivalent value used. Integer value. */
209         public static final String KEY_EXIF_ISO = ExifInterface.TAG_ISO;
210     }
211 
212     // SQL used within this class.
213     protected static final String WHERE_ID = BaseColumns._ID + " = ?";
214     protected static final String WHERE_METADATA_ID = Metadata.PHOTO_ID + " = ? AND "
215             + Metadata.KEY + " = ?";
216 
217     protected static final String SELECT_ALBUM_ID = "SELECT " + Albums._ID + " FROM "
218             + Albums.TABLE;
219     protected static final String SELECT_PHOTO_ID = "SELECT " + Photos._ID + " FROM "
220             + Photos.TABLE;
221     protected static final String SELECT_PHOTO_COUNT = "SELECT COUNT(_id) FROM " + Photos.TABLE;
222     protected static final String DELETE_PHOTOS = "DELETE FROM " + Photos.TABLE;
223     protected static final String DELETE_METADATA = "DELETE FROM " + Metadata.TABLE;
224     protected static final String SELECT_METADATA_COUNT = "SELECT COUNT(_id) FROM " + Metadata.TABLE;
225     protected static final String WHERE = " WHERE ";
226     protected static final String IN = " IN ";
227     protected static final String NESTED_SELECT_START = "(";
228     protected static final String NESTED_SELECT_END = ")";
229     protected static final String[] PROJECTION_COUNT = {
230         "COUNT(_id)"
231     };
232 
233     /**
234      * For selecting the mime-type for an image.
235      */
236     private static final String[] PROJECTION_MIME_TYPE = {
237         Photos.MIME_TYPE,
238     };
239 
240     protected static final String[] BASE_COLUMNS_ID = {
241         BaseColumns._ID,
242     };
243 
244     protected ChangeNotification mNotifier = null;
245     protected static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
246 
247     protected static final int MATCH_PHOTO = 1;
248     protected static final int MATCH_PHOTO_ID = 2;
249     protected static final int MATCH_ALBUM = 3;
250     protected static final int MATCH_ALBUM_ID = 4;
251     protected static final int MATCH_METADATA = 5;
252     protected static final int MATCH_METADATA_ID = 6;
253     protected static final int MATCH_ACCOUNT = 7;
254     protected static final int MATCH_ACCOUNT_ID = 8;
255 
256     static {
sUriMatcher.addURI(AUTHORITY, Photos.TABLE, MATCH_PHOTO)257         sUriMatcher.addURI(AUTHORITY, Photos.TABLE, MATCH_PHOTO);
258         // match against Photos._ID
sUriMatcher.addURI(AUTHORITY, Photos.TABLE + "/#", MATCH_PHOTO_ID)259         sUriMatcher.addURI(AUTHORITY, Photos.TABLE + "/#", MATCH_PHOTO_ID);
sUriMatcher.addURI(AUTHORITY, Albums.TABLE, MATCH_ALBUM)260         sUriMatcher.addURI(AUTHORITY, Albums.TABLE, MATCH_ALBUM);
261         // match against Albums._ID
sUriMatcher.addURI(AUTHORITY, Albums.TABLE + "/#", MATCH_ALBUM_ID)262         sUriMatcher.addURI(AUTHORITY, Albums.TABLE + "/#", MATCH_ALBUM_ID);
sUriMatcher.addURI(AUTHORITY, Metadata.TABLE, MATCH_METADATA)263         sUriMatcher.addURI(AUTHORITY, Metadata.TABLE, MATCH_METADATA);
264         // match against metadata/<Metadata._ID>
sUriMatcher.addURI(AUTHORITY, Metadata.TABLE + "/#", MATCH_METADATA_ID)265         sUriMatcher.addURI(AUTHORITY, Metadata.TABLE + "/#", MATCH_METADATA_ID);
sUriMatcher.addURI(AUTHORITY, Accounts.TABLE, MATCH_ACCOUNT)266         sUriMatcher.addURI(AUTHORITY, Accounts.TABLE, MATCH_ACCOUNT);
267         // match against Accounts._ID
sUriMatcher.addURI(AUTHORITY, Accounts.TABLE + "/#", MATCH_ACCOUNT_ID)268         sUriMatcher.addURI(AUTHORITY, Accounts.TABLE + "/#", MATCH_ACCOUNT_ID);
269     }
270 
271     @Override
deleteInTransaction(Uri uri, String selection, String[] selectionArgs, boolean callerIsSyncAdapter)272     public int deleteInTransaction(Uri uri, String selection, String[] selectionArgs,
273             boolean callerIsSyncAdapter) {
274         int match = matchUri(uri);
275         selection = addIdToSelection(match, selection);
276         selectionArgs = addIdToSelectionArgs(match, uri, selectionArgs);
277         return deleteCascade(uri, match, selection, selectionArgs);
278     }
279 
280     @Override
getType(Uri uri)281     public String getType(Uri uri) {
282         Cursor cursor = query(uri, PROJECTION_MIME_TYPE, null, null, null);
283         String mimeType = null;
284         if (cursor.moveToNext()) {
285             mimeType = cursor.getString(0);
286         }
287         cursor.close();
288         return mimeType;
289     }
290 
291     @Override
insertInTransaction(Uri uri, ContentValues values, boolean callerIsSyncAdapter)292     public Uri insertInTransaction(Uri uri, ContentValues values, boolean callerIsSyncAdapter) {
293         int match = matchUri(uri);
294         validateMatchTable(match);
295         String table = getTableFromMatch(match, uri);
296         SQLiteDatabase db = getDatabaseHelper().getWritableDatabase();
297         Uri insertedUri = null;
298         long id = db.insert(table, null, values);
299         if (id != -1) {
300             // uri already matches the table.
301             insertedUri = ContentUris.withAppendedId(uri, id);
302             postNotifyUri(insertedUri);
303         }
304         return insertedUri;
305     }
306 
307     @Override
query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)308     public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
309             String sortOrder) {
310         return query(uri, projection, selection, selectionArgs, sortOrder, null);
311     }
312 
313     @Override
query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder, CancellationSignal cancellationSignal)314     public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
315             String sortOrder, CancellationSignal cancellationSignal) {
316         projection = replaceCount(projection);
317         int match = matchUri(uri);
318         selection = addIdToSelection(match, selection);
319         selectionArgs = addIdToSelectionArgs(match, uri, selectionArgs);
320         String table = getTableFromMatch(match, uri);
321         Cursor c = query(table, projection, selection, selectionArgs, sortOrder, cancellationSignal);
322         if (c != null) {
323             c.setNotificationUri(getContext().getContentResolver(), uri);
324         }
325         return c;
326     }
327 
328     @Override
updateInTransaction(Uri uri, ContentValues values, String selection, String[] selectionArgs, boolean callerIsSyncAdapter)329     public int updateInTransaction(Uri uri, ContentValues values, String selection,
330             String[] selectionArgs, boolean callerIsSyncAdapter) {
331         int match = matchUri(uri);
332         int rowsUpdated = 0;
333         SQLiteDatabase db = getDatabaseHelper().getWritableDatabase();
334         if (match == MATCH_METADATA) {
335             rowsUpdated = modifyMetadata(db, values);
336         } else {
337             selection = addIdToSelection(match, selection);
338             selectionArgs = addIdToSelectionArgs(match, uri, selectionArgs);
339             String table = getTableFromMatch(match, uri);
340             rowsUpdated = db.update(table, values, selection, selectionArgs);
341         }
342         postNotifyUri(uri);
343         return rowsUpdated;
344     }
345 
setMockNotification(ChangeNotification notification)346     public void setMockNotification(ChangeNotification notification) {
347         mNotifier = notification;
348     }
349 
addIdToSelection(int match, String selection)350     protected static String addIdToSelection(int match, String selection) {
351         String where;
352         switch (match) {
353             case MATCH_PHOTO_ID:
354             case MATCH_ALBUM_ID:
355             case MATCH_METADATA_ID:
356                 where = WHERE_ID;
357                 break;
358             default:
359                 return selection;
360         }
361         return DatabaseUtils.concatenateWhere(selection, where);
362     }
363 
addIdToSelectionArgs(int match, Uri uri, String[] selectionArgs)364     protected static String[] addIdToSelectionArgs(int match, Uri uri, String[] selectionArgs) {
365         String[] whereArgs;
366         switch (match) {
367             case MATCH_PHOTO_ID:
368             case MATCH_ALBUM_ID:
369             case MATCH_METADATA_ID:
370                 whereArgs = new String[] {
371                     uri.getPathSegments().get(1),
372                 };
373                 break;
374             default:
375                 return selectionArgs;
376         }
377         return DatabaseUtils.appendSelectionArgs(selectionArgs, whereArgs);
378     }
379 
addMetadataKeysToSelectionArgs(String[] selectionArgs, Uri uri)380     protected static String[] addMetadataKeysToSelectionArgs(String[] selectionArgs, Uri uri) {
381         List<String> segments = uri.getPathSegments();
382         String[] additionalArgs = {
383                 segments.get(1),
384                 segments.get(2),
385         };
386 
387         return DatabaseUtils.appendSelectionArgs(selectionArgs, additionalArgs);
388     }
389 
getTableFromMatch(int match, Uri uri)390     protected static String getTableFromMatch(int match, Uri uri) {
391         String table;
392         switch (match) {
393             case MATCH_PHOTO:
394             case MATCH_PHOTO_ID:
395                 table = Photos.TABLE;
396                 break;
397             case MATCH_ALBUM:
398             case MATCH_ALBUM_ID:
399                 table = Albums.TABLE;
400                 break;
401             case MATCH_METADATA:
402             case MATCH_METADATA_ID:
403                 table = Metadata.TABLE;
404                 break;
405             case MATCH_ACCOUNT:
406             case MATCH_ACCOUNT_ID:
407                 table = Accounts.TABLE;
408                 break;
409             default:
410                 throw unknownUri(uri);
411         }
412         return table;
413     }
414 
415     @Override
getDatabaseHelper(Context context)416     public SQLiteOpenHelper getDatabaseHelper(Context context) {
417         return new PhotoDatabase(context, DB_NAME);
418     }
419 
modifyMetadata(SQLiteDatabase db, ContentValues values)420     private int modifyMetadata(SQLiteDatabase db, ContentValues values) {
421         int rowCount;
422         if (values.get(Metadata.VALUE) == null) {
423             String[] selectionArgs = {
424                     values.getAsString(Metadata.PHOTO_ID), values.getAsString(Metadata.KEY),
425             };
426             rowCount = db.delete(Metadata.TABLE, WHERE_METADATA_ID, selectionArgs);
427         } else {
428             long rowId = db.replace(Metadata.TABLE, null, values);
429             rowCount = (rowId == -1) ? 0 : 1;
430         }
431         return rowCount;
432     }
433 
matchUri(Uri uri)434     private int matchUri(Uri uri) {
435         int match = sUriMatcher.match(uri);
436         if (match == UriMatcher.NO_MATCH) {
437             throw unknownUri(uri);
438         }
439         return match;
440     }
441 
442     @Override
notifyChange(ContentResolver resolver, Uri uri, boolean syncToNetwork)443     protected void notifyChange(ContentResolver resolver, Uri uri, boolean syncToNetwork) {
444         if (mNotifier != null) {
445             mNotifier.notifyChange(uri, syncToNetwork);
446         } else {
447             super.notifyChange(resolver, uri, syncToNetwork);
448         }
449     }
450 
unknownUri(Uri uri)451     protected static IllegalArgumentException unknownUri(Uri uri) {
452         return new IllegalArgumentException("Unknown Uri format: " + uri);
453     }
454 
nestWhere(String matchColumn, String table, String nestedWhere)455     protected static String nestWhere(String matchColumn, String table, String nestedWhere) {
456         String query = SQLiteQueryBuilder.buildQueryString(false, table, BASE_COLUMNS_ID,
457                 nestedWhere, null, null, null, null);
458         return matchColumn + IN + NESTED_SELECT_START + query + NESTED_SELECT_END;
459     }
460 
metadataSelectionFromPhotos(String where)461     protected static String metadataSelectionFromPhotos(String where) {
462         return nestWhere(Metadata.PHOTO_ID, Photos.TABLE, where);
463     }
464 
photoSelectionFromAlbums(String where)465     protected static String photoSelectionFromAlbums(String where) {
466         return nestWhere(Photos.ALBUM_ID, Albums.TABLE, where);
467     }
468 
photoSelectionFromAccounts(String where)469     protected static String photoSelectionFromAccounts(String where) {
470         return nestWhere(Photos.ACCOUNT_ID, Accounts.TABLE, where);
471     }
472 
albumSelectionFromAccounts(String where)473     protected static String albumSelectionFromAccounts(String where) {
474         return nestWhere(Albums.ACCOUNT_ID, Accounts.TABLE, where);
475     }
476 
deleteCascade(Uri uri, int match, String selection, String[] selectionArgs)477     protected int deleteCascade(Uri uri, int match, String selection, String[] selectionArgs) {
478         switch (match) {
479             case MATCH_PHOTO:
480             case MATCH_PHOTO_ID:
481                 deleteCascade(Metadata.CONTENT_URI, MATCH_METADATA,
482                         metadataSelectionFromPhotos(selection), selectionArgs);
483                 break;
484             case MATCH_ALBUM:
485             case MATCH_ALBUM_ID:
486                 deleteCascade(Photos.CONTENT_URI, MATCH_PHOTO,
487                         photoSelectionFromAlbums(selection), selectionArgs);
488                 break;
489             case MATCH_ACCOUNT:
490             case MATCH_ACCOUNT_ID:
491                 deleteCascade(Photos.CONTENT_URI, MATCH_PHOTO,
492                         photoSelectionFromAccounts(selection), selectionArgs);
493                 deleteCascade(Albums.CONTENT_URI, MATCH_ALBUM,
494                         albumSelectionFromAccounts(selection), selectionArgs);
495                 break;
496         }
497         SQLiteDatabase db = getDatabaseHelper().getWritableDatabase();
498         String table = getTableFromMatch(match, uri);
499         int deleted = db.delete(table, selection, selectionArgs);
500         if (deleted > 0) {
501             postNotifyUri(uri);
502         }
503         return deleted;
504     }
505 
validateMatchTable(int match)506     private static void validateMatchTable(int match) {
507         switch (match) {
508             case MATCH_PHOTO:
509             case MATCH_ALBUM:
510             case MATCH_METADATA:
511             case MATCH_ACCOUNT:
512                 break;
513             default:
514                 throw new IllegalArgumentException("Operation not allowed on an existing row.");
515         }
516     }
517 
query(String table, String[] columns, String selection, String[] selectionArgs, String orderBy, CancellationSignal cancellationSignal)518     protected Cursor query(String table, String[] columns, String selection,
519             String[] selectionArgs, String orderBy, CancellationSignal cancellationSignal) {
520         SQLiteDatabase db = getDatabaseHelper().getReadableDatabase();
521         if (ApiHelper.HAS_CANCELLATION_SIGNAL) {
522             return db.query(false, table, columns, selection, selectionArgs, null, null,
523                     orderBy, null, cancellationSignal);
524         } else {
525             return db.query(table, columns, selection, selectionArgs, null, null, orderBy);
526         }
527     }
528 
replaceCount(String[] projection)529     protected static String[] replaceCount(String[] projection) {
530         if (projection != null && projection.length == 1
531                 && BaseColumns._COUNT.equals(projection[0])) {
532             return PROJECTION_COUNT;
533         }
534         return projection;
535     }
536 }
537