summaryrefslogtreecommitdiffstats
path: root/src/com/android/photos/data/PhotoProvider.java
blob: 27afb586d318e9219262269345f502a2cd4c1b50 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
/*
 * Copyright (C) 2013 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.android.photos.data;

import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.os.CancellationSignal;
import android.provider.BaseColumns;

import java.util.ArrayList;
import java.util.List;

/**
 * A provider that gives access to photo and video information for media stored
 * on the server. Only media that is or will be put on the server will be
 * accessed by this provider. Use Photos.CONTENT_URI to query all photos and
 * videos. Use Albums.CONTENT_URI to query all albums. Use Metadata.CONTENT_URI
 * to query metadata about a photo or video, based on the ID of the media. Use
 * ImageCache.THUMBNAIL_CONTENT_URI, ImageCache.PREVIEW_CONTENT_URI, or
 * ImageCache.ORIGINAL_CONTENT_URI to query the path of the thumbnail, preview,
 * or original-sized image respectfully. <br/>
 * To add or update metadata, use the update function rather than insert. All
 * values for the metadata must be in the ContentValues, even if they are also
 * in the selection. The selection and selectionArgs are not used when updating
 * metadata. If the metadata values are null, the row will be deleted.
 */
public class PhotoProvider extends ContentProvider {
    @SuppressWarnings("unused")
    private static final String TAG = PhotoProvider.class.getSimpleName();

    protected static final String DB_NAME = "photo.db";
    public static final String AUTHORITY = PhotoProviderAuthority.AUTHORITY;
    static final Uri BASE_CONTENT_URI = new Uri.Builder().scheme("content").authority(AUTHORITY)
            .build();

    // Used to allow mocking out the change notification because
    // MockContextResolver disallows system-wide notification.
    public static interface ChangeNotification {
        void notifyChange(Uri uri);
    }

    /**
     * Contains columns that can be accessed via PHOTOS_CONTENT_URI.
     */
    public static interface Photos extends BaseColumns {
        /**
         * Internal database table used for basic photo information.
         */
        public static final String TABLE = "photo";
        /**
         * Content URI for basic photo and video information.
         */
        public static final Uri CONTENT_URI = Uri.withAppendedPath(BASE_CONTENT_URI, TABLE);
        /**
         * Identifier used on the server. Long value.
         */
        public static final String SERVER_ID = "server_id";
        /**
         * Column name for the width of the original image. Integer value.
         */
        public static final String WIDTH = "width";
        /**
         * Column name for the height of the original image. Integer value.
         */
        public static final String HEIGHT = "height";
        /**
         * Column name for the date that the original image was taken. Long
         * value indicating the milliseconds since epoch in the GMT time zone.
         */
        public static final String DATE_TAKEN = "date_taken";
        /**
         * Column name indicating the long value of the album id that this image
         * resides in. Will be NULL if it it has not been uploaded to the
         * server.
         */
        public static final String ALBUM_ID = "album_id";
        /**
         * The column name for the mime-type String.
         */
        public static final String MIME_TYPE = "mime_type";
    }

    /**
     * Contains columns and Uri for accessing album information.
     */
    public static interface Albums extends BaseColumns {
        /**
         * Internal database table used album information.
         */
        public static final String TABLE = "album";
        /**
         * Content URI for album information.
         */
        public static final Uri CONTENT_URI = Uri.withAppendedPath(BASE_CONTENT_URI, TABLE);
        /**
         * Parent directory or null if this is in the root.
         */
        public static final String PARENT_ID = "parent";
        /**
         * Column name for the name of the album. String value.
         */
        public static final String NAME = "name";
        /**
         * Column name for the visibility level of the album. Can be any of the
         * VISIBILITY_* values.
         */
        public static final String VISIBILITY = "visibility";
        /**
         * Column name for the server identifier for this album. NULL if the
         * server doesn't have this album yet.
         */
        public static final String SERVER_ID = "server_id";

        // Privacy values for Albums.VISIBILITY
        public static final int VISIBILITY_PRIVATE = 1;
        public static final int VISIBILITY_SHARED = 2;
        public static final int VISIBILITY_PUBLIC = 3;
    }

    /**
     * Contains columns and Uri for accessing photo and video metadata
     */
    public static interface Metadata extends BaseColumns {
        /**
         * Internal database table used metadata information.
         */
        public static final String TABLE = "metadata";
        /**
         * Content URI for photo and video metadata.
         */
        public static final Uri CONTENT_URI = Uri.withAppendedPath(BASE_CONTENT_URI, TABLE);
        /**
         * Foreign key to photo_id. Long value.
         */
        public static final String PHOTO_ID = "photo_id";
        /**
         * Metadata key. String value
         */
        public static final String KEY = "key";
        /**
         * Metadata value. Type is based on key.
         */
        public static final String VALUE = "value";
    }

    /**
     * Contains columns and Uri for maintaining the image cache.
     */
    public static interface ImageCache extends BaseColumns {
        /**
         * Internal database table used for the image cache
         */
        public static final String TABLE = "image_cache";

        /**
         * The image_type query parameter required for accessing a specific
         * image
         */
        public static final String IMAGE_TYPE_QUERY_PARAMETER = "image_type";

        // ImageCache.IMAGE_TYPE values
        public static final int IMAGE_TYPE_THUMBNAIL = 1;
        public static final int IMAGE_TYPE_PREVIEW = 2;
        public static final int IMAGE_TYPE_ORIGINAL = 3;

        /**
         * Content URI for retrieving image paths. The
         * IMAGE_TYPE_QUERY_PARAMETER must be used in queries.
         */
        public static final Uri CONTENT_URI = Uri.withAppendedPath(BASE_CONTENT_URI, TABLE);

        /**
         * Foreign key to the photos._id. Long value.
         */
        public static final String PHOTO_ID = "photo_id";
        /**
         * One of IMAGE_TYPE_* values.
         */
        public static final String IMAGE_TYPE = "image_type";
        /**
         * The String path to the image.
         */
        public static final String PATH = "path";
    };

    // SQL used within this class.
    protected static final String WHERE_ID = BaseColumns._ID + " = ?";
    protected static final String WHERE_METADATA_ID = Metadata.PHOTO_ID + " = ? AND "
            + Metadata.KEY + " = ?";

    protected static final String SELECT_ALBUM_ID = "SELECT " + Albums._ID + " FROM "
            + Albums.TABLE;
    protected static final String SELECT_PHOTO_ID = "SELECT " + Photos._ID + " FROM "
            + Photos.TABLE;
    protected static final String SELECT_PHOTO_COUNT = "SELECT COUNT(*) FROM " + Photos.TABLE;
    protected static final String DELETE_PHOTOS = "DELETE FROM " + Photos.TABLE;
    protected static final String DELETE_METADATA = "DELETE FROM " + Metadata.TABLE;
    protected static final String SELECT_METADATA_COUNT = "SELECT COUNT(*) FROM " + Metadata.TABLE;
    protected static final String WHERE = " WHERE ";
    protected static final String IN = " IN ";
    protected static final String NESTED_SELECT_START = "(";
    protected static final String NESTED_SELECT_END = ")";

    /**
     * For selecting the mime-type for an image.
     */
    private static final String[] PROJECTION_MIME_TYPE = {
        Photos.MIME_TYPE,
    };

    private static final String[] BASE_COLUMNS_ID = {
        BaseColumns._ID,
    };

    protected ChangeNotification mNotifier = null;
    private SQLiteOpenHelper mOpenHelper;
    protected static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);

    protected static final int MATCH_PHOTO = 1;
    protected static final int MATCH_PHOTO_ID = 2;
    protected static final int MATCH_ALBUM = 3;
    protected static final int MATCH_ALBUM_ID = 4;
    protected static final int MATCH_METADATA = 5;
    protected static final int MATCH_METADATA_ID = 6;
    protected static final int MATCH_IMAGE = 7;

    static {
        sUriMatcher.addURI(AUTHORITY, Photos.TABLE, MATCH_PHOTO);
        // match against Photos._ID
        sUriMatcher.addURI(AUTHORITY, Photos.TABLE + "/#", MATCH_PHOTO_ID);
        sUriMatcher.addURI(AUTHORITY, Albums.TABLE, MATCH_ALBUM);
        // match against Albums._ID
        sUriMatcher.addURI(AUTHORITY, Albums.TABLE + "/#", MATCH_ALBUM_ID);
        sUriMatcher.addURI(AUTHORITY, Metadata.TABLE, MATCH_METADATA);
        // match against metadata/<Metadata._ID>
        sUriMatcher.addURI(AUTHORITY, Metadata.TABLE + "/#", MATCH_METADATA_ID);
        // match against image_cache/<ImageCache.PHOTO_ID>
        sUriMatcher.addURI(AUTHORITY, ImageCache.TABLE + "/#", MATCH_IMAGE);
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        int match = matchUri(uri);
        if (match == MATCH_IMAGE) {
            throw new IllegalArgumentException("Cannot delete from image cache");
        }
        selection = addIdToSelection(match, selection);
        selectionArgs = addIdToSelectionArgs(match, uri, selectionArgs);
        List<Uri> changeUris = new ArrayList<Uri>();
        int deleted = 0;
        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
        db.beginTransaction();
        try {
            deleted = deleteCascade(db, match, selection, selectionArgs, changeUris, uri);
            db.setTransactionSuccessful();
        } finally {
            db.endTransaction();
        }
        for (Uri changeUri : changeUris) {
            notifyChanges(changeUri);
        }
        return deleted;
    }

    @Override
    public String getType(Uri uri) {
        Cursor cursor = query(uri, PROJECTION_MIME_TYPE, null, null, null);
        String mimeType = null;
        if (cursor.moveToNext()) {
            mimeType = cursor.getString(0);
        }
        cursor.close();
        return mimeType;
    }

    @Override
    public Uri insert(Uri uri, ContentValues values) {
        // Cannot insert into this ContentProvider
        return null;
    }

    @Override
    public boolean onCreate() {
        mOpenHelper = createDatabaseHelper();
        return true;
    }

    @Override
    public void shutdown() {
        getDatabaseHelper().close();
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
            String sortOrder) {
        return query(uri, projection, selection, selectionArgs, sortOrder, null);
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
            String sortOrder, CancellationSignal cancellationSignal) {
        int match = matchUri(uri);
        selection = addIdToSelection(match, selection);
        selectionArgs = addIdToSelectionArgs(match, uri, selectionArgs);
        String table = getTableFromMatch(match, uri);
        SQLiteDatabase db = mOpenHelper.getReadableDatabase();
        return db.query(false, table, projection, selection, selectionArgs, null, null, sortOrder,
                null, cancellationSignal);
    }

    @Override
    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
        int match = matchUri(uri);
        int rowsUpdated = 0;
        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
        db.beginTransaction();
        try {
            if (match == MATCH_METADATA) {
                rowsUpdated = modifyMetadata(db, values);
            } else {
                selection = addIdToSelection(match, selection);
                selectionArgs = addIdToSelectionArgs(match, uri, selectionArgs);
                String table = getTableFromMatch(match, uri);
                rowsUpdated = db.update(table, values, selection, selectionArgs);
            }
            db.setTransactionSuccessful();
        } finally {
            db.endTransaction();
        }
        notifyChanges(uri);
        return rowsUpdated;
    }

    public void setMockNotification(ChangeNotification notification) {
        mNotifier = notification;
    }

    protected static String addIdToSelection(int match, String selection) {
        String where;
        switch (match) {
            case MATCH_PHOTO_ID:
            case MATCH_ALBUM_ID:
            case MATCH_METADATA_ID:
                where = WHERE_ID;
                break;
            default:
                return selection;
        }
        return DatabaseUtils.concatenateWhere(selection, where);
    }

    protected static String[] addIdToSelectionArgs(int match, Uri uri, String[] selectionArgs) {
        String[] whereArgs;
        switch (match) {
            case MATCH_PHOTO_ID:
            case MATCH_ALBUM_ID:
            case MATCH_METADATA_ID:
                whereArgs = new String[] {
                    uri.getPathSegments().get(1),
                };
                break;
            default:
                return selectionArgs;
        }
        return DatabaseUtils.appendSelectionArgs(selectionArgs, whereArgs);
    }

    protected static String[] addMetadataKeysToSelectionArgs(String[] selectionArgs, Uri uri) {
        List<String> segments = uri.getPathSegments();
        String[] additionalArgs = {
                segments.get(1),
                segments.get(2),
        };

        return DatabaseUtils.appendSelectionArgs(selectionArgs, additionalArgs);
    }

    protected static String getTableFromMatch(int match, Uri uri) {
        String table;
        switch (match) {
            case MATCH_PHOTO:
            case MATCH_PHOTO_ID:
                table = Photos.TABLE;
                break;
            case MATCH_ALBUM:
            case MATCH_ALBUM_ID:
                table = Albums.TABLE;
                break;
            case MATCH_METADATA:
            case MATCH_METADATA_ID:
                table = Metadata.TABLE;
                break;
            default:
                throw unknownUri(uri);
        }
        return table;
    }

    protected final SQLiteOpenHelper getDatabaseHelper() {
        return mOpenHelper;
    }

    protected SQLiteOpenHelper createDatabaseHelper() {
        return new PhotoDatabase(getContext(), DB_NAME);
    }

    private int modifyMetadata(SQLiteDatabase db, ContentValues values) {
        String[] selectionArgs = {
            values.getAsString(Metadata.PHOTO_ID),
            values.getAsString(Metadata.KEY),
        };
        int rowCount;
        if (values.get(Metadata.VALUE) == null) {
            rowCount = db.delete(Metadata.TABLE, WHERE_METADATA_ID, selectionArgs);
        } else {
            rowCount = (int) DatabaseUtils.queryNumEntries(db, Metadata.TABLE, WHERE_METADATA_ID,
                    selectionArgs);
            if (rowCount > 0) {
                db.update(Metadata.TABLE, values, WHERE_METADATA_ID, selectionArgs);
            } else {
                db.insert(Metadata.TABLE, null, values);
                rowCount = 1;
            }
        }
        return rowCount;
    }

    private int matchUri(Uri uri) {
        int match = sUriMatcher.match(uri);
        if (match == UriMatcher.NO_MATCH) {
            throw unknownUri(uri);
        }
        return match;
    }

    protected void notifyChanges(Uri uri) {
        if (mNotifier != null) {
            mNotifier.notifyChange(uri);
        } else {
            getContext().getContentResolver().notifyChange(uri, null, false);
        }
    }

    protected static IllegalArgumentException unknownUri(Uri uri) {
        return new IllegalArgumentException("Unknown Uri format: " + uri);
    }

    protected static String nestWhere(String matchColumn, String table, String nestedWhere) {
        String query = SQLiteQueryBuilder.buildQueryString(false, table, BASE_COLUMNS_ID,
                nestedWhere, null, null, null, null);
        return matchColumn + IN + NESTED_SELECT_START + query + NESTED_SELECT_END;
    }

    protected static int deleteCascade(SQLiteDatabase db, int match, String selection,
            String[] selectionArgs, List<Uri> changeUris, Uri uri) {
        switch (match) {
            case MATCH_PHOTO:
            case MATCH_PHOTO_ID: {
                deleteCascadeMetadata(db, selection, selectionArgs, changeUris);
                break;
            }
            case MATCH_ALBUM:
            case MATCH_ALBUM_ID: {
                deleteCascadePhotos(db, selection, selectionArgs, changeUris);
                break;
            }
        }
        String table = getTableFromMatch(match, uri);
        int deleted = db.delete(table, selection, selectionArgs);
        if (deleted > 0) {
            changeUris.add(uri);
        }
        return deleted;
    }

    private static void deleteCascadePhotos(SQLiteDatabase db, String albumSelect,
            String[] selectArgs, List<Uri> changeUris) {
        String photoWhere = nestWhere(Photos.ALBUM_ID, Albums.TABLE, albumSelect);
        deleteCascadeMetadata(db, photoWhere, selectArgs, changeUris);
        int deleted = db.delete(Photos.TABLE, photoWhere, selectArgs);
        if (deleted > 0) {
            changeUris.add(Photos.CONTENT_URI);
        }
    }

    private static void deleteCascadeMetadata(SQLiteDatabase db, String photosSelect,
            String[] selectArgs, List<Uri> changeUris) {
        String metadataWhere = nestWhere(Metadata.PHOTO_ID, Photos.TABLE, photosSelect);
        int deleted = db.delete(Metadata.TABLE, metadataWhere, selectArgs);
        if (deleted > 0) {
            changeUris.add(Metadata.CONTENT_URI);
        }
    }
}