diff options
author | George Mount <mount@google.com> | 2013-02-20 17:01:54 -0800 |
---|---|---|
committer | George Mount <mount@google.com> | 2013-02-26 12:49:13 -0800 |
commit | c8419b4e5e3302f2efc7ea629891041a14219aa7 (patch) | |
tree | cb364c89207944dfcaa453ddae73671ba3dcde05 /src/com/android/photos | |
parent | db4cedf1feb3899840963b2648dc3e2b4d0a1d6f (diff) | |
download | android_packages_apps_Gallery2-c8419b4e5e3302f2efc7ea629891041a14219aa7.tar.gz android_packages_apps_Gallery2-c8419b4e5e3302f2efc7ea629891041a14219aa7.tar.bz2 android_packages_apps_Gallery2-c8419b4e5e3302f2efc7ea629891041a14219aa7.zip |
Add initial implementation of PhotosProvider.
Change-Id: I98694cf54bd0fb549703a7184e1816e9590a05ff
Diffstat (limited to 'src/com/android/photos')
-rw-r--r-- | src/com/android/photos/data/PhotoDatabase.java | 111 | ||||
-rw-r--r-- | src/com/android/photos/data/PhotoProvider.java | 514 |
2 files changed, 625 insertions, 0 deletions
diff --git a/src/com/android/photos/data/PhotoDatabase.java b/src/com/android/photos/data/PhotoDatabase.java new file mode 100644 index 000000000..64a857fcc --- /dev/null +++ b/src/com/android/photos/data/PhotoDatabase.java @@ -0,0 +1,111 @@ +/* + * 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.Context; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; + +import com.android.photos.data.PhotoProvider.Albums; +import com.android.photos.data.PhotoProvider.Metadata; +import com.android.photos.data.PhotoProvider.Photos; + +/** + * Used in PhotoProvider to create and access the database containing + * information about photo and video information stored on the server. + */ +public class PhotoDatabase extends SQLiteOpenHelper { + @SuppressWarnings("unused") + private static final String TAG = PhotoDatabase.class.getSimpleName(); + static final String DB_NAME = "photo.db"; + static final int DB_VERSION = 1; + + private static final String SQL_CREATE_TABLE = "CREATE TABLE "; + + private static final String[][] CREATE_PHOTO = { + { Photos._ID, "INTEGER PRIMARY KEY AUTOINCREMENT" }, + { Photos.SERVER_ID, "INTEGER UNIQUE" }, + { Photos.WIDTH, "INTEGER NOT NULL" }, + { Photos.HEIGHT, "INTEGER NOT NULL" }, + { Photos.DATE_TAKEN, "INTEGER NOT NULL" }, + // Photos.ALBUM_ID is a foreign key to Albums._ID + { Photos.ALBUM_ID, "INTEGER" }, + { Photos.MIME_TYPE, "TEXT NOT NULL" }, + }; + + private static final String[][] CREATE_ALBUM = { + { Albums._ID, "INTEGER PRIMARY KEY AUTOINCREMENT" }, + // Albums.PARENT_ID is a foriegn key to Albums._ID + { Albums.PARENT_ID, "INTEGER" }, + { Albums.NAME, "Text NOT NULL" }, + { Albums.VISIBILITY, "INTEGER NOT NULL" }, + { Albums.SERVER_ID, "INTEGER UNIQUE" }, + createUniqueConstraint(Albums.PARENT_ID, Albums.NAME), + }; + + private static final String[][] CREATE_METADATA = { + { Metadata._ID, "INTEGER PRIMARY KEY AUTOINCREMENT" }, + // Metadata.PHOTO_ID is a foreign key to Photos._ID + { Metadata.PHOTO_ID, "INTEGER NOT NULL" }, + { Metadata.KEY, "TEXT NOT NULL" }, + { Metadata.VALUE, "TEXT NOT NULL" }, + createUniqueConstraint(Metadata.PHOTO_ID, Metadata.KEY), + }; + + @Override + public void onCreate(SQLiteDatabase db) { + createTable(db, Albums.TABLE, CREATE_ALBUM); + createTable(db, Photos.TABLE, CREATE_PHOTO); + createTable(db, Metadata.TABLE, CREATE_METADATA); + } + + public PhotoDatabase(Context context) { + super(context, DB_NAME, null, DB_VERSION); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + } + + protected static void createTable(SQLiteDatabase db, String table, String[][] columns) { + StringBuilder create = new StringBuilder(SQL_CREATE_TABLE); + create.append(table).append('('); + boolean first = true; + for (String[] column : columns) { + if (!first) { + create.append(','); + } + first = false; + for (String val: column) { + create.append(val).append(' '); + } + } + create.append(')'); + db.beginTransaction(); + try { + db.execSQL(create.toString()); + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } + + protected static String[] createUniqueConstraint(String column1, String column2) { + return new String[] { + "UNIQUE(", column1, ",", column2, ")" + }; + } +} diff --git a/src/com/android/photos/data/PhotoProvider.java b/src/com/android/photos/data/PhotoProvider.java new file mode 100644 index 000000000..eefa37349 --- /dev/null +++ b/src/com/android/photos/data/PhotoProvider.java @@ -0,0 +1,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.ContentResolver; +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.SQLiteStatement; +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(); + static final String AUTHORITY = "com.android.gallery3d.photoprovider"; + static final Uri BASE_CONTENT_URI = new Uri.Builder().scheme("content").authority(AUTHORITY) + .build(); + + /** + * 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 = "photos_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 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 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; + } + + 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()); + } + + 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) { + ContentResolver resolver = getContext().getContentResolver(); + resolver.notifyChange(uri, null, false); + } + + protected static IllegalArgumentException unknownUri(Uri uri) { + return new IllegalArgumentException("Unknown Uri format: " + uri); + } + + protected static String nestSql(String base, String columnMatch, String nested) { + StringBuilder sql = new StringBuilder(base); + sql.append(WHERE); + sql.append(columnMatch); + sql.append(IN); + sql.append(NESTED_SELECT_START); + sql.append(nested); + sql.append(NESTED_SELECT_END); + return sql.toString(); + } + + protected static String addWhere(String base, String where) { + if (where == null || where.isEmpty()) { + return base; + } + return base + WHERE + where; + } + + 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: { + String selectPhotoIdsSql = addWhere(SELECT_PHOTO_ID, selection); + deleteCascadeMetadata(db, selectPhotoIdsSql, selectionArgs, changeUris); + break; + } + case MATCH_ALBUM: + case MATCH_ALBUM_ID: { + String selectAlbumIdSql = addWhere(SELECT_ALBUM_ID, selection); + deleteCascadePhotos(db, selectAlbumIdSql, selectionArgs, changeUris); + break; + } + } + String table = getTableFromMatch(match, uri); + changeUris.add(uri); + return db.delete(table, selection, selectionArgs); + } + + protected static void execSql(SQLiteDatabase db, String sql, String[] args) { + if (args == null) { + db.execSQL(sql); + } else { + db.execSQL(sql, args); + } + } + + private static void deleteCascadePhotos(SQLiteDatabase db, String albumSelect, + String[] selectArgs, List<Uri> changeUris) { + String selectPhotoIdSql = nestSql(SELECT_PHOTO_ID, Photos.ALBUM_ID, albumSelect); + deleteCascadeMetadata(db, selectPhotoIdSql, selectArgs, changeUris); + String deletePhotoSql = nestSql(DELETE_PHOTOS, Photos.ALBUM_ID, albumSelect); + SQLiteStatement statement = db.compileStatement(deletePhotoSql); + statement.bindAllArgsAsStrings(selectArgs); + int deleted = statement.executeUpdateDelete(); + if (deleted > 0) { + changeUris.add(Photos.CONTENT_URI); + } + } + + private static void deleteCascadeMetadata(SQLiteDatabase db, String photosSelect, + String[] selectArgs, List<Uri> changeUris) { + String deleteMetadataSql = nestSql(DELETE_METADATA, Metadata.PHOTO_ID, photosSelect); + SQLiteStatement statement = db.compileStatement(deleteMetadataSql); + statement.bindAllArgsAsStrings(selectArgs); + int deleted = statement.executeUpdateDelete(); + if (deleted > 0) { + changeUris.add(Metadata.CONTENT_URI); + } + } +} |