diff options
Diffstat (limited to 'src/com/android/photos/data/PhotoProvider.java')
-rw-r--r-- | src/com/android/photos/data/PhotoProvider.java | 536 |
1 files changed, 536 insertions, 0 deletions
diff --git a/src/com/android/photos/data/PhotoProvider.java b/src/com/android/photos/data/PhotoProvider.java new file mode 100644 index 000000000..d4310ca95 --- /dev/null +++ b/src/com/android/photos/data/PhotoProvider.java @@ -0,0 +1,536 @@ +/* + * 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.ContentResolver; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +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.media.ExifInterface; +import android.net.Uri; +import android.os.CancellationSignal; +import android.provider.BaseColumns; + +import com.android.gallery3d.common.ApiHelper; + +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 SQLiteContentProvider { + @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, boolean syncToNetwork); + } + + /** + * Contains columns that can be accessed via Accounts.CONTENT_URI + */ + public static interface Accounts extends BaseColumns { + /** + * Internal database table used for account information + */ + public static final String TABLE = "accounts"; + /** + * Content URI for account information + */ + public static final Uri CONTENT_URI = Uri.withAppendedPath(BASE_CONTENT_URI, TABLE); + /** + * User name for this account. + */ + public static final String ACCOUNT_NAME = "name"; + } + + /** + * Contains columns that can be accessed via Photos.CONTENT_URI. + */ + public static interface Photos extends BaseColumns { + /** + * The image_type query parameter required for requesting a specific + * size of image. + */ + public static final String MEDIA_SIZE_QUERY_PARAMETER = "media_size"; + + /** Internal database table used for basic photo information. */ + public static final String TABLE = "photos"; + /** Content URI for basic photo and video information. */ + public static final Uri CONTENT_URI = Uri.withAppendedPath(BASE_CONTENT_URI, TABLE); + + /** Long foreign key to Accounts._ID */ + public static final String ACCOUNT_ID = "account_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"; + /** The title of the photo. String value. */ + public static final String TITLE = "title"; + /** The date the photo entry was last updated. Long value. */ + public static final String DATE_MODIFIED = "date_modified"; + /** + * The rotation of the photo in degrees, if rotation has not already + * been applied. Integer value. + */ + public static final String ROTATION = "rotation"; + } + + /** + * 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 = "albums"; + /** Content URI for album information. */ + public static final Uri CONTENT_URI = Uri.withAppendedPath(BASE_CONTENT_URI, TABLE); + + /** Long foreign key to Accounts._ID */ + public static final String ACCOUNT_ID = "account_id"; + /** Parent directory or null if this is in the root. */ + public static final String PARENT_ID = "parent_id"; + /** The type of album. Non-null, if album is auto-generated. String value. */ + public static final String ALBUM_TYPE = "album_type"; + /** + * Column name for the visibility level of the album. Can be any of the + * VISIBILITY_* values. + */ + public static final String VISIBILITY = "visibility"; + /** The user-specified location associated with the album. String value. */ + public static final String LOCATION_STRING = "location_string"; + /** The title of the album. String value. */ + public static final String TITLE = "title"; + /** A short summary of the contents of the album. String value. */ + public static final String SUMMARY = "summary"; + /** The date the album was created. Long value */ + public static final String DATE_PUBLISHED = "date_published"; + /** The date the album entry was last updated. Long value. */ + public static final String DATE_MODIFIED = "date_modified"; + + // 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"; + + /** A short summary of the photo. String value. */ + public static final String KEY_SUMMARY = "summary"; + /** The date the photo was added. Long value. */ + public static final String KEY_PUBLISHED = "date_published"; + /** The date the photo was last updated. Long value. */ + public static final String KEY_DATE_UPDATED = "date_updated"; + /** The size of the photo is bytes. Integer value. */ + public static final String KEY_SIZE_IN_BTYES = "size"; + /** The latitude associated with the photo. Double value. */ + public static final String KEY_LATITUDE = "latitude"; + /** The longitude associated with the photo. Double value. */ + public static final String KEY_LONGITUDE = "longitude"; + + /** The make of the camera used. String value. */ + public static final String KEY_EXIF_MAKE = ExifInterface.TAG_MAKE; + /** The model of the camera used. String value. */ + public static final String KEY_EXIF_MODEL = ExifInterface.TAG_MODEL;; + /** The exposure time used. Float value. */ + public static final String KEY_EXIF_EXPOSURE = ExifInterface.TAG_EXPOSURE_TIME; + /** Whether the flash was used. Boolean value. */ + public static final String KEY_EXIF_FLASH = ExifInterface.TAG_FLASH; + /** The focal length used. Float value. */ + public static final String KEY_EXIF_FOCAL_LENGTH = ExifInterface.TAG_FOCAL_LENGTH; + /** The fstop value used. Float value. */ + public static final String KEY_EXIF_FSTOP = ExifInterface.TAG_APERTURE; + /** The ISO equivalent value used. Integer value. */ + public static final String KEY_EXIF_ISO = ExifInterface.TAG_ISO; + } + + // 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 = ")"; + protected static final String[] PROJECTION_COUNT = { + "COUNT(*)" + }; + + /** + * For selecting the mime-type for an image. + */ + private static final String[] PROJECTION_MIME_TYPE = { + Photos.MIME_TYPE, + }; + + protected static final String[] BASE_COLUMNS_ID = { + BaseColumns._ID, + }; + + protected ChangeNotification mNotifier = null; + 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_ACCOUNT = 7; + protected static final int MATCH_ACCOUNT_ID = 8; + + 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); + sUriMatcher.addURI(AUTHORITY, Accounts.TABLE, MATCH_ACCOUNT); + // match against Accounts._ID + sUriMatcher.addURI(AUTHORITY, Accounts.TABLE + "/#", MATCH_ACCOUNT_ID); + } + + @Override + public int deleteInTransaction(Uri uri, String selection, String[] selectionArgs, + boolean callerIsSyncAdapter) { + int match = matchUri(uri); + selection = addIdToSelection(match, selection); + selectionArgs = addIdToSelectionArgs(match, uri, selectionArgs); + return deleteCascade(uri, match, selection, selectionArgs); + } + + @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 insertInTransaction(Uri uri, ContentValues values, boolean callerIsSyncAdapter) { + int match = matchUri(uri); + validateMatchTable(match); + String table = getTableFromMatch(match, uri); + SQLiteDatabase db = getDatabaseHelper().getWritableDatabase(); + Uri insertedUri = null; + long id = db.insert(table, null, values); + if (id != -1) { + // uri already matches the table. + insertedUri = ContentUris.withAppendedId(uri, id); + postNotifyUri(insertedUri); + } + return insertedUri; + } + + @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) { + projection = replaceCount(projection); + int match = matchUri(uri); + selection = addIdToSelection(match, selection); + selectionArgs = addIdToSelectionArgs(match, uri, selectionArgs); + String table = getTableFromMatch(match, uri); + Cursor c = query(table, projection, selection, selectionArgs, sortOrder, cancellationSignal); + if (c != null) { + c.setNotificationUri(getContext().getContentResolver(), uri); + } + return c; + } + + @Override + public int updateInTransaction(Uri uri, ContentValues values, String selection, + String[] selectionArgs, boolean callerIsSyncAdapter) { + int match = matchUri(uri); + int rowsUpdated = 0; + SQLiteDatabase db = getDatabaseHelper().getWritableDatabase(); + 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); + } + postNotifyUri(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; + case MATCH_ACCOUNT: + case MATCH_ACCOUNT_ID: + table = Accounts.TABLE; + break; + default: + throw unknownUri(uri); + } + return table; + } + + @Override + public SQLiteOpenHelper getDatabaseHelper(Context context) { + return new PhotoDatabase(context, DB_NAME); + } + + private int modifyMetadata(SQLiteDatabase db, ContentValues values) { + int rowCount; + if (values.get(Metadata.VALUE) == null) { + String[] selectionArgs = { + values.getAsString(Metadata.PHOTO_ID), values.getAsString(Metadata.KEY), + }; + rowCount = db.delete(Metadata.TABLE, WHERE_METADATA_ID, selectionArgs); + } else { + long rowId = db.replace(Metadata.TABLE, null, values); + rowCount = (rowId == -1) ? 0 : 1; + } + return rowCount; + } + + private int matchUri(Uri uri) { + int match = sUriMatcher.match(uri); + if (match == UriMatcher.NO_MATCH) { + throw unknownUri(uri); + } + return match; + } + + @Override + protected void notifyChange(ContentResolver resolver, Uri uri, boolean syncToNetwork) { + if (mNotifier != null) { + mNotifier.notifyChange(uri, syncToNetwork); + } else { + super.notifyChange(resolver, uri, syncToNetwork); + } + } + + 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 String metadataSelectionFromPhotos(String where) { + return nestWhere(Metadata.PHOTO_ID, Photos.TABLE, where); + } + + protected static String photoSelectionFromAlbums(String where) { + return nestWhere(Photos.ALBUM_ID, Albums.TABLE, where); + } + + protected static String photoSelectionFromAccounts(String where) { + return nestWhere(Photos.ACCOUNT_ID, Accounts.TABLE, where); + } + + protected static String albumSelectionFromAccounts(String where) { + return nestWhere(Albums.ACCOUNT_ID, Accounts.TABLE, where); + } + + protected int deleteCascade(Uri uri, int match, String selection, String[] selectionArgs) { + switch (match) { + case MATCH_PHOTO: + case MATCH_PHOTO_ID: + deleteCascade(Metadata.CONTENT_URI, MATCH_METADATA, + metadataSelectionFromPhotos(selection), selectionArgs); + break; + case MATCH_ALBUM: + case MATCH_ALBUM_ID: + deleteCascade(Photos.CONTENT_URI, MATCH_PHOTO, + photoSelectionFromAlbums(selection), selectionArgs); + break; + case MATCH_ACCOUNT: + case MATCH_ACCOUNT_ID: + deleteCascade(Photos.CONTENT_URI, MATCH_PHOTO, + photoSelectionFromAccounts(selection), selectionArgs); + deleteCascade(Albums.CONTENT_URI, MATCH_ALBUM, + albumSelectionFromAccounts(selection), selectionArgs); + break; + } + SQLiteDatabase db = getDatabaseHelper().getWritableDatabase(); + String table = getTableFromMatch(match, uri); + int deleted = db.delete(table, selection, selectionArgs); + if (deleted > 0) { + postNotifyUri(uri); + } + return deleted; + } + + private static void validateMatchTable(int match) { + switch (match) { + case MATCH_PHOTO: + case MATCH_ALBUM: + case MATCH_METADATA: + case MATCH_ACCOUNT: + break; + default: + throw new IllegalArgumentException("Operation not allowed on an existing row."); + } + } + + protected Cursor query(String table, String[] columns, String selection, + String[] selectionArgs, String orderBy, CancellationSignal cancellationSignal) { + SQLiteDatabase db = getDatabaseHelper().getReadableDatabase(); + if (ApiHelper.HAS_CANCELLATION_SIGNAL) { + return db.query(false, table, columns, selection, selectionArgs, null, null, + orderBy, null, cancellationSignal); + } else { + return db.query(table, columns, selection, selectionArgs, null, null, orderBy); + } + } + + protected static String[] replaceCount(String[] projection) { + if (projection != null && projection.length == 1 + && BaseColumns._COUNT.equals(projection[0])) { + return PROJECTION_COUNT; + } + return projection; + } +} |