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