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