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