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 | 178b04f4cea6dcbe0f84f0f189e9280d4c0d1442 (patch) | |
tree | dcf7aafa07634ee30491f50de3ce3d811723ec7e | |
parent | aae069d8a006e4664414df56750b86844825b39c (diff) | |
download | android_packages_apps_Snap-178b04f4cea6dcbe0f84f0f189e9280d4c0d1442.tar.gz android_packages_apps_Snap-178b04f4cea6dcbe0f84f0f189e9280d4c0d1442.tar.bz2 android_packages_apps_Snap-178b04f4cea6dcbe0f84f0f189e9280d4c0d1442.zip |
Add initial implementation of PhotosProvider.
Change-Id: I98694cf54bd0fb549703a7184e1816e9590a05ff
-rw-r--r-- | src/com/android/photos/data/PhotoDatabase.java | 111 | ||||
-rw-r--r-- | src/com/android/photos/data/PhotoProvider.java | 514 | ||||
-rw-r--r-- | tests/src/com/android/photos/data/DataTestRunner.java | 36 | ||||
-rw-r--r-- | tests/src/com/android/photos/data/PhotoDatabaseTest.java | 187 | ||||
-rw-r--r-- | tests/src/com/android/photos/data/PhotoDatabaseUtils.java | 117 | ||||
-rw-r--r-- | tests/src/com/android/photos/data/PhotoProviderTest.java | 463 |
6 files changed, 1428 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); + } + } +} diff --git a/tests/src/com/android/photos/data/DataTestRunner.java b/tests/src/com/android/photos/data/DataTestRunner.java new file mode 100644 index 000000000..432258561 --- /dev/null +++ b/tests/src/com/android/photos/data/DataTestRunner.java @@ -0,0 +1,36 @@ +/* + * 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.test.InstrumentationTestRunner; +import android.test.InstrumentationTestSuite; + +import junit.framework.TestSuite; + +public class DataTestRunner extends InstrumentationTestRunner { + @Override + public TestSuite getAllTests() { + TestSuite suite = new InstrumentationTestSuite(this); + suite.addTestSuite(PhotoDatabaseTest.class); + suite.addTestSuite(PhotoProviderTest.class); + return suite; + } + + @Override + public ClassLoader getLoader() { + return DataTestRunner.class.getClassLoader(); + } +} diff --git a/tests/src/com/android/photos/data/PhotoDatabaseTest.java b/tests/src/com/android/photos/data/PhotoDatabaseTest.java new file mode 100644 index 000000000..48e79d4e1 --- /dev/null +++ b/tests/src/com/android/photos/data/PhotoDatabaseTest.java @@ -0,0 +1,187 @@ +/* + * 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.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.test.InstrumentationTestCase; + +import com.android.photos.data.PhotoProvider.Albums; +import com.android.photos.data.PhotoProvider.Metadata; +import com.android.photos.data.PhotoProvider.Photos; + +import java.io.File; +import java.io.IOException; + +public class PhotoDatabaseTest extends InstrumentationTestCase { + + private PhotoDatabase mDBHelper; + + @Override + protected void setUp() { + Context context = getInstrumentation().getTargetContext(); + mDBHelper = new PhotoDatabase(context); + } + + @Override + protected void tearDown() { + mDBHelper.close(); + } + + public void testCreateDatabase() throws IOException { + Context context = getInstrumentation().getTargetContext(); + File dbFile = context.getDatabasePath(PhotoDatabase.DB_NAME); + if (dbFile.exists()) { + dbFile.delete(); + } + SQLiteDatabase db = getReadableDB(); + db.beginTransaction(); + db.endTransaction(); + assertTrue(dbFile.exists()); + dbFile.delete(); + } + + public void testTables() { + validateTable(Metadata.TABLE, PhotoDatabaseUtils.PROJECTION_METADATA); + validateTable(Albums.TABLE, PhotoDatabaseUtils.PROJECTION_ALBUMS); + validateTable(Photos.TABLE, PhotoDatabaseUtils.PROJECTION_PHOTOS); + } + + public void testAlbumsConstraints() { + SQLiteDatabase db = getWriteableDB(); + db.beginTransaction(); + try { + // Test NOT NULL constraint on name + assertFalse(PhotoDatabaseUtils + .insertAlbum(db, null, null, Albums.VISIBILITY_PRIVATE, null)); + + // test NOT NULL constraint on privacy + assertFalse(PhotoDatabaseUtils.insertAlbum(db, null, "hello", null, null)); + + // Normal insert + assertTrue(PhotoDatabaseUtils.insertAlbum(db, null, "hello", Albums.VISIBILITY_PRIVATE, + 100L)); + + // Test server id uniqueness + assertFalse(PhotoDatabaseUtils.insertAlbum(db, null, "world", Albums.VISIBILITY_PRIVATE, + 100L)); + + // Different server id allowed + assertTrue(PhotoDatabaseUtils.insertAlbum(db, null, "world", Albums.VISIBILITY_PRIVATE, + 101L)); + + // Allow null server id + assertTrue(PhotoDatabaseUtils.insertAlbum(db, null, "hello world", + Albums.VISIBILITY_PRIVATE, null)); + + long albumId = PhotoDatabaseUtils.queryAlbumIdFromServerId(db, 100); + + // Assign a valid child + assertTrue(PhotoDatabaseUtils.insertAlbum(db, albumId, "hello", Albums.VISIBILITY_PRIVATE, + null)); + + long otherAlbumId = PhotoDatabaseUtils.queryAlbumIdFromServerId(db, 101); + assertNotSame(albumId, otherAlbumId); + + // This is a valid child of another album. + assertTrue(PhotoDatabaseUtils.insertAlbum(db, otherAlbumId, "hello", + Albums.VISIBILITY_PRIVATE, null)); + + // This isn't allowed due to uniqueness constraint (parent_id/name) + assertFalse(PhotoDatabaseUtils.insertAlbum(db, otherAlbumId, "hello", + Albums.VISIBILITY_PRIVATE, null)); + } finally { + db.endTransaction(); + } + } + + public void testPhotosConstraints() { + SQLiteDatabase db = getWriteableDB(); + db.beginTransaction(); + try { + int width = 100; + int height = 100; + long dateTaken = System.currentTimeMillis(); + String mimeType = "test/test"; + + // Test NOT NULL mime-type + assertFalse(PhotoDatabaseUtils.insertPhoto(db, null, width, height, dateTaken, null, + null)); + + // Test NOT NULL width + assertFalse(PhotoDatabaseUtils.insertPhoto(db, null, null, height, dateTaken, null, + mimeType)); + + // Test NOT NULL height + assertFalse(PhotoDatabaseUtils.insertPhoto(db, null, width, null, dateTaken, null, + mimeType)); + + // Test NOT NULL dateTaken + assertFalse(PhotoDatabaseUtils.insertPhoto(db, null, width, height, null, null, + mimeType)); + + // Test normal insert + assertTrue(PhotoDatabaseUtils.insertPhoto(db, null, width, height, dateTaken, null, + mimeType)); + } finally { + db.endTransaction(); + } + } + + public void testMetadataConstraints() { + SQLiteDatabase db = getWriteableDB(); + db.beginTransaction(); + try { + final String mimeType = "test/test"; + long photoServerId = 100; + PhotoDatabaseUtils.insertPhoto(db, photoServerId, 100, 100, 100L, null, mimeType); + long photoId = PhotoDatabaseUtils.queryPhotoIdFromServerId(db, photoServerId); + + // Test NOT NULL PHOTO_ID constraint. + assertFalse(PhotoDatabaseUtils.insertMetadata(db, null, "foo", "bar")); + + // Normal insert. + assertTrue(PhotoDatabaseUtils.insertMetadata(db, photoId, "foo", "bar")); + + // Test uniqueness constraint. + assertFalse(PhotoDatabaseUtils.insertMetadata(db, photoId, "foo", "baz")); + } finally { + db.endTransaction(); + } + } + + private SQLiteDatabase getReadableDB() { + return mDBHelper.getReadableDatabase(); + } + + private SQLiteDatabase getWriteableDB() { + return mDBHelper.getWritableDatabase(); + } + + private void validateTable(String table, String[] projection) { + SQLiteDatabase db = getReadableDB(); + Cursor cursor = db.query(table, projection, null, null, null, null, null); + assertNotNull(cursor); + assertEquals(cursor.getCount(), 0); + assertEquals(cursor.getColumnCount(), projection.length); + for (int i = 0; i < projection.length; i++) { + assertEquals(cursor.getColumnName(i), projection[i]); + } + } + + +} diff --git a/tests/src/com/android/photos/data/PhotoDatabaseUtils.java b/tests/src/com/android/photos/data/PhotoDatabaseUtils.java new file mode 100644 index 000000000..6fd73e1b3 --- /dev/null +++ b/tests/src/com/android/photos/data/PhotoDatabaseUtils.java @@ -0,0 +1,117 @@ +/* + * 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.ContentValues; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; + +import com.android.photos.data.PhotoProvider.Albums; +import com.android.photos.data.PhotoProvider.Metadata; +import com.android.photos.data.PhotoProvider.Photos; + +import junit.framework.AssertionFailedError; + +public class PhotoDatabaseUtils { + public static String[] PROJECTION_ALBUMS = { + Albums._ID, + Albums.PARENT_ID, + Albums.VISIBILITY, + Albums.NAME, + Albums.SERVER_ID, + }; + + public static String[] PROJECTION_METADATA = { + Metadata.PHOTO_ID, + Metadata.KEY, + Metadata.VALUE, + }; + + public static String[] PROJECTION_PHOTOS = { + Photos._ID, + Photos.SERVER_ID, + Photos.WIDTH, + Photos.HEIGHT, + Photos.DATE_TAKEN, + Photos.ALBUM_ID, + Photos.MIME_TYPE, + }; + + private static String SELECTION_ALBUM_SERVER_ID = Albums.SERVER_ID + " = ?"; + private static String SELECTION_PHOTO_SERVER_ID = Photos.SERVER_ID + " = ?"; + + public static long queryAlbumIdFromServerId(SQLiteDatabase db, long serverId) { + return queryId(db, Albums.TABLE, PROJECTION_ALBUMS, SELECTION_ALBUM_SERVER_ID, serverId); + } + + public static long queryPhotoIdFromServerId(SQLiteDatabase db, long serverId) { + return queryId(db, Photos.TABLE, PROJECTION_PHOTOS, SELECTION_PHOTO_SERVER_ID, serverId); + } + + public static long queryId(SQLiteDatabase db, String table, String[] projection, + String selection, Object parameter) { + String paramString = parameter == null ? null : parameter.toString(); + String[] selectionArgs = { + paramString, + }; + Cursor cursor = db.query(table, projection, selection, selectionArgs, null, null, null); + try { + if (cursor.getCount() != 1 || !cursor.moveToNext()) { + throw new AssertionFailedError("Couldn't find item in table"); + } + long id = cursor.getLong(0); + return id; + } finally { + cursor.close(); + } + } + + public static boolean insertPhoto(SQLiteDatabase db, Long serverId, Integer width, + Integer height, Long dateTaken, Long albumId, String mimeType) { + ContentValues values = new ContentValues(); + values.put(Photos.SERVER_ID, serverId); + values.put(Photos.WIDTH, width); + values.put(Photos.HEIGHT, height); + values.put(Photos.DATE_TAKEN, dateTaken); + values.put(Photos.ALBUM_ID, albumId); + values.put(Photos.MIME_TYPE, mimeType); + return db.insert(Photos.TABLE, null, values) != -1; + } + + public static boolean insertAlbum(SQLiteDatabase db, Long parentId, String name, + Integer privacy, Long serverId) { + ContentValues values = new ContentValues(); + values.put(Albums.PARENT_ID, parentId); + values.put(Albums.NAME, name); + values.put(Albums.VISIBILITY, privacy); + values.put(Albums.SERVER_ID, serverId); + return db.insert(Albums.TABLE, null, values) != -1; + } + + public static boolean insertMetadata(SQLiteDatabase db, Long photosId, String key, String value) { + ContentValues values = new ContentValues(); + values.put(Metadata.PHOTO_ID, photosId); + values.put(Metadata.KEY, key); + values.put(Metadata.VALUE, value); + return db.insert(Metadata.TABLE, null, values) != -1; + } + + public static void deleteAllContent(SQLiteDatabase db) { + db.delete(Metadata.TABLE, null, null); + db.delete(Photos.TABLE, null, null); + db.delete(Albums.TABLE, null, null); + } +} diff --git a/tests/src/com/android/photos/data/PhotoProviderTest.java b/tests/src/com/android/photos/data/PhotoProviderTest.java new file mode 100644 index 000000000..ad913b0bd --- /dev/null +++ b/tests/src/com/android/photos/data/PhotoProviderTest.java @@ -0,0 +1,463 @@ +/* + * 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.database.ContentObserver; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.net.Uri; +import android.os.Handler; +import android.os.Looper; +import android.provider.BaseColumns; +import android.test.InstrumentationTestCase; + +import com.android.photos.data.PhotoProvider.Albums; +import com.android.photos.data.PhotoProvider.Metadata; +import com.android.photos.data.PhotoProvider.Photos; + +public class PhotoProviderTest extends InstrumentationTestCase { + @SuppressWarnings("unused") + private static final String TAG = PhotoProviderTest.class.getSimpleName(); + + private static final String MIME_TYPE = "test/test"; + private static final String ALBUM_NAME = "My Album"; + private static final long ALBUM_SERVER_ID = 100; + private static final long PHOTO_SERVER_ID = 50; + private static final String META_KEY = "mykey"; + private static final String META_VALUE = "myvalue"; + + private static final Uri NO_TABLE_URI = PhotoProvider.BASE_CONTENT_URI; + private static final Uri BAD_TABLE_URI = Uri.withAppendedPath(PhotoProvider.BASE_CONTENT_URI, + "bad_table"); + + private static final String WHERE_METADATA_PHOTOS_ID = Metadata.PHOTO_ID + " = ?"; + private static final String WHERE_METADATA = Metadata.PHOTO_ID + " = ? AND " + Metadata.KEY + + " = ?"; + + private static final long WAIT_FOR_CHANGE_MILLIS = 200; + + private long mAlbumId; + private long mPhotoId; + private long mMetadataId; + + private PhotoDatabase mDBHelper; + private ContentResolver mResolver; + + private static class WatchContentObserverThread extends Thread { + private WatchContentObserver mObserver; + private Looper mLooper; + + @Override + public void run() { + Looper.prepare(); + mLooper = Looper.myLooper(); + WatchContentObserver observer = new WatchContentObserver(); + synchronized (this) { + mObserver = observer; + this.notifyAll(); + } + Looper.loop(); + } + + public void waitForObserver() throws InterruptedException { + synchronized (this) { + while (mObserver == null) { + this.wait(); + } + } + } + + public WatchContentObserver getObserver() { + return mObserver; + } + + public void stopLooper() { + mLooper.quit(); + } + }; + + private static class WatchContentObserver extends ContentObserver { + private boolean mOnChangeReceived = false; + private Uri mUri = null; + + public WatchContentObserver() { + super(new Handler()); + } + + @Override + public synchronized void onChange(boolean selfChange, Uri uri) { + mOnChangeReceived = true; + mUri = uri; + notifyAll(); + } + + @Override + public synchronized void onChange(boolean selfChange) { + mOnChangeReceived = true; + notifyAll(); + } + + public boolean waitForNotification() { + synchronized (this) { + if (!mOnChangeReceived) { + try { + wait(WAIT_FOR_CHANGE_MILLIS); + } catch (InterruptedException e) { + } + } + } + return mOnChangeReceived; + } + }; + + @Override + protected void setUp() { + Context context = getInstrumentation().getTargetContext(); + mDBHelper = new PhotoDatabase(context); + mResolver = context.getContentResolver(); + SQLiteDatabase db = mDBHelper.getWritableDatabase(); + db.beginTransaction(); + try { + PhotoDatabaseUtils.insertAlbum(db, null, ALBUM_NAME, Albums.VISIBILITY_PRIVATE, + ALBUM_SERVER_ID); + mAlbumId = PhotoDatabaseUtils.queryAlbumIdFromServerId(db, ALBUM_SERVER_ID); + PhotoDatabaseUtils.insertPhoto(db, PHOTO_SERVER_ID, 100, 100, + System.currentTimeMillis(), mAlbumId, MIME_TYPE); + mPhotoId = PhotoDatabaseUtils.queryPhotoIdFromServerId(db, PHOTO_SERVER_ID); + PhotoDatabaseUtils.insertMetadata(db, mPhotoId, META_KEY, META_VALUE); + String[] projection = { + BaseColumns._ID, + }; + Cursor cursor = db.query(Metadata.TABLE, projection, null, null, null, null, null); + cursor.moveToNext(); + mMetadataId = cursor.getLong(0); + cursor.close(); + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } + + @Override + protected void tearDown() { + SQLiteDatabase db = mDBHelper.getWritableDatabase(); + db.beginTransaction(); + try { + PhotoDatabaseUtils.deleteAllContent(db); + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + mDBHelper.close(); + mDBHelper = null; + } + + public void testDelete() { + try { + mResolver.delete(NO_TABLE_URI, null, null); + fail("Exeption should be thrown when no table given"); + } catch (Exception e) { + // expected exception + } + try { + mResolver.delete(BAD_TABLE_URI, null, null); + fail("Exeption should be thrown when deleting from a table that doesn't exist"); + } catch (Exception e) { + // expected exception + } + + String[] selectionArgs = { + String.valueOf(mPhotoId) + }; + // Delete some metadata + assertEquals(1, + mResolver.delete(Metadata.CONTENT_URI, WHERE_METADATA_PHOTOS_ID, selectionArgs)); + Uri photoUri = ContentUris.withAppendedId(Photos.CONTENT_URI, mPhotoId); + assertEquals(1, mResolver.delete(photoUri, null, null)); + Uri albumUri = ContentUris.withAppendedId(Albums.CONTENT_URI, mAlbumId); + assertEquals(1, mResolver.delete(albumUri, null, null)); + // now delete something that isn't there + assertEquals(0, mResolver.delete(photoUri, null, null)); + } + + public void testDeleteMetadataId() { + Uri metadataUri = ContentUris.withAppendedId(Metadata.CONTENT_URI, mMetadataId); + assertEquals(1, mResolver.delete(metadataUri, null, null)); + Cursor cursor = mResolver.query(Metadata.CONTENT_URI, null, null, null, null); + assertEquals(0, cursor.getCount()); + cursor.close(); + } + // Delete the album and ensure that the photos referring to the album are + // deleted. + public void testDeleteAlbumCascade() { + WatchContentObserverThread observerThread = createObserverThread(); + WatchContentObserver observer = observerThread.getObserver(); + mResolver.registerContentObserver(Photos.CONTENT_URI, true, observer); + try { + Uri albumUri = ContentUris.withAppendedId(Albums.CONTENT_URI, mAlbumId); + mResolver.delete(albumUri, null, null); + assertTrue(observer.waitForNotification()); + assertEquals(observer.mUri, Photos.CONTENT_URI); + Cursor cursor = mResolver.query(Photos.CONTENT_URI, + PhotoDatabaseUtils.PROJECTION_PHOTOS, null, null, null); + assertEquals(0, cursor.getCount()); + cursor.close(); + } finally { + mResolver.unregisterContentObserver(observer); + observerThread.stopLooper(); + } + } + + // Delete the album and ensure that the metadata referring to photos in that + // album are deleted. + public void testDeleteAlbumCascade2() { + WatchContentObserverThread observerThread = createObserverThread(); + WatchContentObserver observer = observerThread.getObserver(); + mResolver.registerContentObserver(Metadata.CONTENT_URI, true, observer); + try { + Uri albumUri = ContentUris.withAppendedId(Albums.CONTENT_URI, mAlbumId); + mResolver.delete(albumUri, null, null); + assertTrue(observer.waitForNotification()); + assertEquals(observer.mUri, Metadata.CONTENT_URI); + Cursor cursor = mResolver.query(Metadata.CONTENT_URI, + PhotoDatabaseUtils.PROJECTION_METADATA, null, null, null); + assertEquals(0, cursor.getCount()); + cursor.close(); + } finally { + mResolver.unregisterContentObserver(observer); + observerThread.stopLooper(); + } + } + + // Delete all albums and ensure that photos in any album are deleted. + public void testDeleteAlbumCascade3() { + WatchContentObserverThread observerThread = createObserverThread(); + WatchContentObserver observer = observerThread.getObserver(); + mResolver.registerContentObserver(Photos.CONTENT_URI, true, observer); + try { + mResolver.delete(Albums.CONTENT_URI, null, null); + assertTrue(observer.waitForNotification()); + assertEquals(observer.mUri, Photos.CONTENT_URI); + Cursor cursor = mResolver.query(Photos.CONTENT_URI, + PhotoDatabaseUtils.PROJECTION_PHOTOS, null, null, null); + assertEquals(0, cursor.getCount()); + cursor.close(); + } finally { + mResolver.unregisterContentObserver(observer); + observerThread.stopLooper(); + } + } + + // Delete a photo and ensure that the metadata for that photo are deleted. + public void testDeletePhotoCascade() { + WatchContentObserverThread observerThread = createObserverThread(); + WatchContentObserver observer = observerThread.getObserver(); + mResolver.registerContentObserver(Metadata.CONTENT_URI, true, observer); + try { + Uri albumUri = ContentUris.withAppendedId(Photos.CONTENT_URI, mPhotoId); + mResolver.delete(albumUri, null, null); + assertTrue(observer.waitForNotification()); + assertEquals(observer.mUri, Metadata.CONTENT_URI); + Cursor cursor = mResolver.query(Metadata.CONTENT_URI, + PhotoDatabaseUtils.PROJECTION_METADATA, null, null, null); + assertEquals(0, cursor.getCount()); + cursor.close(); + } finally { + mResolver.unregisterContentObserver(observer); + observerThread.stopLooper(); + } + } + + public void testGetType() { + // We don't return types for albums + assertNull(mResolver.getType(Albums.CONTENT_URI)); + + Uri noImage = ContentUris.withAppendedId(Photos.CONTENT_URI, mPhotoId + 1); + assertNull(mResolver.getType(noImage)); + + Uri image = ContentUris.withAppendedId(Photos.CONTENT_URI, mPhotoId); + assertEquals(MIME_TYPE, mResolver.getType(image)); + } + + public void testInsert() { + ContentValues values = new ContentValues(); + values.put(Albums.NAME, "don't add me"); + values.put(Albums.VISIBILITY, Albums.VISIBILITY_PRIVATE); + assertNull(mResolver.insert(Albums.CONTENT_URI, values)); + } + + public void testUpdate() { + ContentValues values = new ContentValues(); + // Normal update -- use an album. + values.put(Albums.NAME, "foo"); + Uri albumUri = ContentUris.withAppendedId(Albums.CONTENT_URI, mAlbumId); + assertEquals(1, mResolver.update(albumUri, values, null, null)); + String[] projection = { + Albums.NAME, + }; + Cursor cursor = mResolver.query(albumUri, projection, null, null, null); + assertEquals(1, cursor.getCount()); + assertTrue(cursor.moveToNext()); + assertEquals("foo", cursor.getString(0)); + cursor.close(); + + // Update a row that doesn't exist. + Uri noAlbumUri = ContentUris.withAppendedId(Albums.CONTENT_URI, mAlbumId + 1); + values.put(Albums.NAME, "bar"); + assertEquals(0, mResolver.update(noAlbumUri, values, null, null)); + + // Update a metadata value that exists. + ContentValues metadata = new ContentValues(); + metadata.put(Metadata.PHOTO_ID, mPhotoId); + metadata.put(Metadata.KEY, META_KEY); + metadata.put(Metadata.VALUE, "new value"); + assertEquals(1, mResolver.update(Metadata.CONTENT_URI, metadata, null, null)); + + projection = new String[] { + Metadata.VALUE, + }; + + String[] selectionArgs = { + String.valueOf(mPhotoId), META_KEY, + }; + + cursor = mResolver.query(Metadata.CONTENT_URI, projection, WHERE_METADATA, selectionArgs, + null); + assertEquals(1, cursor.getCount()); + assertTrue(cursor.moveToNext()); + assertEquals("new value", cursor.getString(0)); + cursor.close(); + + // Update a metadata value that doesn't exist. + metadata.put(Metadata.KEY, "other stuff"); + assertEquals(1, mResolver.update(Metadata.CONTENT_URI, metadata, null, null)); + + selectionArgs[1] = "other stuff"; + cursor = mResolver.query(Metadata.CONTENT_URI, projection, WHERE_METADATA, selectionArgs, + null); + assertEquals(1, cursor.getCount()); + assertTrue(cursor.moveToNext()); + assertEquals("new value", cursor.getString(0)); + cursor.close(); + + // Remove a metadata value using update. + metadata.putNull(Metadata.VALUE); + assertEquals(1, mResolver.update(Metadata.CONTENT_URI, metadata, null, null)); + cursor = mResolver.query(Metadata.CONTENT_URI, projection, WHERE_METADATA, selectionArgs, + null); + assertEquals(0, cursor.getCount()); + cursor.close(); + } + + public void testQuery() { + // Query a photo that exists. + Cursor cursor = mResolver.query(Photos.CONTENT_URI, PhotoDatabaseUtils.PROJECTION_PHOTOS, + null, null, null); + assertNotNull(cursor); + assertEquals(1, cursor.getCount()); + assertTrue(cursor.moveToNext()); + assertEquals(mPhotoId, cursor.getLong(0)); + cursor.close(); + + // Query a photo that doesn't exist. + Uri noPhotoUri = ContentUris.withAppendedId(Photos.CONTENT_URI, mPhotoId + 1); + cursor = mResolver.query(noPhotoUri, PhotoDatabaseUtils.PROJECTION_PHOTOS, null, null, + null); + assertNotNull(cursor); + assertEquals(0, cursor.getCount()); + cursor.close(); + + // Query a photo that exists using selection arguments. + String[] selectionArgs = { + String.valueOf(mPhotoId), + }; + + cursor = mResolver.query(Photos.CONTENT_URI, PhotoDatabaseUtils.PROJECTION_PHOTOS, + Photos._ID + " = ?", selectionArgs, null); + assertNotNull(cursor); + assertEquals(1, cursor.getCount()); + assertTrue(cursor.moveToNext()); + assertEquals(mPhotoId, cursor.getLong(0)); + cursor.close(); + } + + public void testUpdatePhotoNotification() { + WatchContentObserverThread observerThread = createObserverThread(); + WatchContentObserver observer = observerThread.getObserver(); + mResolver.registerContentObserver(Photos.CONTENT_URI, true, observer); + try { + Uri photoUri = ContentUris.withAppendedId(Photos.CONTENT_URI, mPhotoId); + ContentValues values = new ContentValues(); + values.put(Photos.MIME_TYPE, "not-a/mime-type"); + mResolver.update(photoUri, values, null, null); + assertTrue(observer.waitForNotification()); + assertEquals(observer.mUri, photoUri); + } finally { + mResolver.unregisterContentObserver(observer); + observerThread.stopLooper(); + } + } + + public void testUpdateMetadataNotification() { + WatchContentObserverThread observerThread = createObserverThread(); + WatchContentObserver observer = observerThread.getObserver(); + mResolver.registerContentObserver(Metadata.CONTENT_URI, true, observer); + try { + ContentValues values = new ContentValues(); + values.put(Metadata.PHOTO_ID, mPhotoId); + values.put(Metadata.KEY, META_KEY); + values.put(Metadata.VALUE, "hello world"); + mResolver.update(Metadata.CONTENT_URI, values, null, null); + assertTrue(observer.waitForNotification()); + assertEquals(observer.mUri, Metadata.CONTENT_URI); + } finally { + mResolver.unregisterContentObserver(observer); + observerThread.stopLooper(); + } + } + + public void testDeletePhotoNotification() { + WatchContentObserverThread observerThread = createObserverThread(); + WatchContentObserver observer = observerThread.getObserver(); + mResolver.registerContentObserver(Photos.CONTENT_URI, true, observer); + try { + Uri photoUri = ContentUris.withAppendedId(Photos.CONTENT_URI, mPhotoId); + mResolver.delete(photoUri, null, null); + assertTrue(observer.waitForNotification()); + assertEquals(observer.mUri, photoUri); + } finally { + mResolver.unregisterContentObserver(observer); + observerThread.stopLooper(); + } + } + + private WatchContentObserverThread createObserverThread() { + WatchContentObserverThread thread = new WatchContentObserverThread(); + thread.start(); + try { + thread.waitForObserver(); + return thread; + } catch (InterruptedException e) { + thread.stopLooper(); + fail("Interruption while waiting for observer being created."); + return null; + } + } +} |