summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/com/android/photos/data/PhotoDatabase.java111
-rw-r--r--src/com/android/photos/data/PhotoProvider.java514
-rw-r--r--tests/src/com/android/photos/data/DataTestRunner.java36
-rw-r--r--tests/src/com/android/photos/data/PhotoDatabaseTest.java187
-rw-r--r--tests/src/com/android/photos/data/PhotoDatabaseUtils.java117
-rw-r--r--tests/src/com/android/photos/data/PhotoProviderTest.java463
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 0000000..64a857f
--- /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 0000000..eefa373
--- /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 0000000..4322585
--- /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 0000000..48e79d4
--- /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 0000000..6fd73e1
--- /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 0000000..ad913b0
--- /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;
+ }
+ }
+}