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        /**
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(*) 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(*) 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(*)"
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 {
257        sUriMatcher.addURI(AUTHORITY, Photos.TABLE, MATCH_PHOTO);
258        // match against Photos._ID
259        sUriMatcher.addURI(AUTHORITY, Photos.TABLE + "/#", MATCH_PHOTO_ID);
260        sUriMatcher.addURI(AUTHORITY, Albums.TABLE, MATCH_ALBUM);
261        // match against Albums._ID
262        sUriMatcher.addURI(AUTHORITY, Albums.TABLE + "/#", MATCH_ALBUM_ID);
263        sUriMatcher.addURI(AUTHORITY, Metadata.TABLE, MATCH_METADATA);
264        // match against metadata/<Metadata._ID>
265        sUriMatcher.addURI(AUTHORITY, Metadata.TABLE + "/#", MATCH_METADATA_ID);
266        sUriMatcher.addURI(AUTHORITY, Accounts.TABLE, MATCH_ACCOUNT);
267        // match against Accounts._ID
268        sUriMatcher.addURI(AUTHORITY, Accounts.TABLE + "/#", MATCH_ACCOUNT_ID);
269    }
270
271    @Override
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
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
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
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
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
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
346    public void setMockNotification(ChangeNotification notification) {
347        mNotifier = notification;
348    }
349
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
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
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
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
416    public SQLiteOpenHelper getDatabaseHelper(Context context) {
417        return new PhotoDatabase(context, DB_NAME);
418    }
419
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
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
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
451    protected static IllegalArgumentException unknownUri(Uri uri) {
452        return new IllegalArgumentException("Unknown Uri format: " + uri);
453    }
454
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
461    protected static String metadataSelectionFromPhotos(String where) {
462        return nestWhere(Metadata.PHOTO_ID, Photos.TABLE, where);
463    }
464
465    protected static String photoSelectionFromAlbums(String where) {
466        return nestWhere(Photos.ALBUM_ID, Albums.TABLE, where);
467    }
468
469    protected static String photoSelectionFromAccounts(String where) {
470        return nestWhere(Photos.ACCOUNT_ID, Accounts.TABLE, where);
471    }
472
473    protected static String albumSelectionFromAccounts(String where) {
474        return nestWhere(Albums.ACCOUNT_ID, Accounts.TABLE, where);
475    }
476
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
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
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
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