From e9061f9b34396bc966c5cca76e81720c26a283fc Mon Sep 17 00:00:00 2001 From: Isaac Devine Date: Sun, 2 Aug 2015 15:06:16 +1200 Subject: [PATCH 1/3] Implement getType() and use UriMatcher Support the getType() content provider method and use the UriMatcher instead to determine which table, mime type and whether it is a single row lookup instead of getPathSegment(). This is more flexible and allows the provider to stop trusting the uris provided by clients. --- .../simpleprovider/AbstractProvider.java | 93 +++++++++++++++++-- .../java/de/triplet/simpleprovider/Table.java | 1 + .../java/de/triplet/simpleprovider/Utils.java | 8 ++ .../simpleprovider/SimpleProviderTest.java | 12 +++ 4 files changed, 104 insertions(+), 10 deletions(-) diff --git a/simpleprovider/src/main/java/de/triplet/simpleprovider/AbstractProvider.java b/simpleprovider/src/main/java/de/triplet/simpleprovider/AbstractProvider.java index ec4e59b..238d203 100644 --- a/simpleprovider/src/main/java/de/triplet/simpleprovider/AbstractProvider.java +++ b/simpleprovider/src/main/java/de/triplet/simpleprovider/AbstractProvider.java @@ -8,6 +8,7 @@ import android.content.ContentValues; import android.content.Context; import android.content.OperationApplicationException; +import android.content.UriMatcher; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteException; @@ -23,9 +24,13 @@ public abstract class AbstractProvider extends ContentProvider { protected final String mLogTag; protected SQLiteDatabase mDatabase; + protected Object mMatcherSynchronizer; + protected UriMatcher mMatcher; + protected MatchDetail[] mMatchDetails; protected AbstractProvider() { mLogTag = getClass().getName(); + mMatcherSynchronizer = new Object(); } @Override @@ -54,6 +59,58 @@ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { return false; } + /** + * Initializes the matcher, based on the tables contained within the subclass, + * also guarantees to have initialized the mTableNames[] array as well. + */ + protected void initializeMatcher() { + // initialize the UriMatcher once, use a synchronized block + // here, so that subclasses can implement this by static initialization if + // they want. + synchronized (mMatcherSynchronizer) { + if(mMatcher != null) { + return; + } + + mMatcher = new UriMatcher(UriMatcher.NO_MATCH); + List details = new ArrayList<>(); + + String authority = getAuthority(); + + for (Class clazz : getClass().getClasses()) { + Table table = clazz.getAnnotation(Table.class); + if (table != null) { + String tableName = Utils.getTableName(clazz, table); + String mimeName = Utils.getMimeName(clazz, table); + + // Add the plural version + mMatcher.addURI(authority, tableName, details.size()); + details.add(new MatchDetail(tableName, "vnd.android.cursor.dir/vnd." + getAuthority() + "." + mimeName, false)); + + // Add the singular version + mMatcher.addURI(authority, tableName + "/#", details.size()); + details.add(new MatchDetail(tableName, "vnd.android.cursor.item/vnd." + getAuthority() + "." + mimeName, true)); + } + } + + // Populate the rest. + mMatchDetails = details.toArray(new MatchDetail[0]); + } + } + + private static class MatchDetail + { + public final String tableName; + public final String mimeType; + public final boolean forceIdColumn; + + public MatchDetail(String tableName, String mimeType, boolean forceIdColumn) { + this.tableName = tableName; + this.mimeType = mimeType; + this.forceIdColumn = forceIdColumn; + } + } + /** * Called when the database needs to be updated and after AbstractProvider has * done its own work. That is, after creating columns that have been added using the @@ -93,7 +150,15 @@ protected int getSchemaVersion() { @Override public String getType(Uri uri) { - return null; + initializeMatcher(); + + int match = mMatcher.match(uri); + + if(match == UriMatcher.NO_MATCH) { + return null; + } + + return mMatchDetails[match].mimeType; } @Override @@ -118,14 +183,19 @@ private ContentResolver getContentResolver() { } private SelectionBuilder buildBaseQuery(Uri uri) { - List pathSegments = uri.getPathSegments(); - if (pathSegments == null) { - return null; + initializeMatcher(); + + int match = mMatcher.match(uri); + + if(match == UriMatcher.NO_MATCH) { + throw new IllegalArgumentException("Unsupported content uri"); } - SelectionBuilder builder = new SelectionBuilder(pathSegments.get(0)); + MatchDetail detail = mMatchDetails[match]; + + SelectionBuilder builder = new SelectionBuilder(detail.tableName); - if (pathSegments.size() == 2) { + if(detail.forceIdColumn) { builder.whereEquals(BaseColumns._ID, uri.getLastPathSegment()); } @@ -134,12 +204,15 @@ private SelectionBuilder buildBaseQuery(Uri uri) { @Override public Uri insert(Uri uri, ContentValues values) { - List segments = uri.getPathSegments(); - if (segments == null || segments.size() != 1) { - return null; + initializeMatcher(); + + int match = mMatcher.match(uri); + + if(match == UriMatcher.NO_MATCH) { + throw new IllegalArgumentException("Unsupported content uri"); } - long rowId = mDatabase.insert(segments.get(0), null, values); + long rowId = mDatabase.insert(mMatchDetails[match].tableName, null, values); if (rowId > -1) { getContentResolver().notifyChange(uri, null); diff --git a/simpleprovider/src/main/java/de/triplet/simpleprovider/Table.java b/simpleprovider/src/main/java/de/triplet/simpleprovider/Table.java index 34fb284..1943a4d 100644 --- a/simpleprovider/src/main/java/de/triplet/simpleprovider/Table.java +++ b/simpleprovider/src/main/java/de/triplet/simpleprovider/Table.java @@ -13,4 +13,5 @@ int since() default 1; + String mimeSuffix() default ""; } diff --git a/simpleprovider/src/main/java/de/triplet/simpleprovider/Utils.java b/simpleprovider/src/main/java/de/triplet/simpleprovider/Utils.java index 5f5dc71..f20d19a 100644 --- a/simpleprovider/src/main/java/de/triplet/simpleprovider/Utils.java +++ b/simpleprovider/src/main/java/de/triplet/simpleprovider/Utils.java @@ -19,6 +19,14 @@ static String getTableName(Class clazz, Table table) { } } + static String getMimeName(Class clazz, Table table) { + String mimeName = table.mimeSuffix(); + if(TextUtils.isEmpty(mimeName)) { + return clazz.getSimpleName().toLowerCase(java.util.Locale.US); + } + return mimeName; + } + static String pluralize(String string) { string = string.toLowerCase(); diff --git a/simpleprovider/src/test/java/de/triplet/simpleprovider/SimpleProviderTest.java b/simpleprovider/src/test/java/de/triplet/simpleprovider/SimpleProviderTest.java index c0efe98..a7f2f37 100644 --- a/simpleprovider/src/test/java/de/triplet/simpleprovider/SimpleProviderTest.java +++ b/simpleprovider/src/test/java/de/triplet/simpleprovider/SimpleProviderTest.java @@ -1,6 +1,7 @@ package de.triplet.simpleprovider; import android.content.ContentResolver; +import android.content.ContentUris; import android.content.ContentValues; import android.database.Cursor; import android.net.Uri; @@ -84,4 +85,15 @@ public void testQueryById() { CONTENT_2, c.getString(c.getColumnIndex(TestProvider.Post.CONTENT))); assertFalse("There shouldn't be any more entries", c.moveToNext()); } + + @Test + public void testGetType() { + String actual = mContentResolver.getType(mPostsUri); + + assertEquals("vnd.android.cursor.dir/vnd." + TestProvider.AUTHORITY + ".post", actual); + + actual = mContentResolver.getType(ContentUris.withAppendedId(mPostsUri , 1)); + + assertEquals("vnd.android.cursor.item/vnd." + TestProvider.AUTHORITY + ".post", actual); + } } From 1054f8e38cf67cfe7c9d19557e3f862316c998f2 Mon Sep 17 00:00:00 2001 From: Isaac Devine Date: Mon, 10 Aug 2015 20:23:36 +1200 Subject: [PATCH 2/3] Fix non-final synchronized object warning --- .../main/java/de/triplet/simpleprovider/AbstractProvider.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/simpleprovider/src/main/java/de/triplet/simpleprovider/AbstractProvider.java b/simpleprovider/src/main/java/de/triplet/simpleprovider/AbstractProvider.java index 238d203..2e77011 100644 --- a/simpleprovider/src/main/java/de/triplet/simpleprovider/AbstractProvider.java +++ b/simpleprovider/src/main/java/de/triplet/simpleprovider/AbstractProvider.java @@ -24,7 +24,7 @@ public abstract class AbstractProvider extends ContentProvider { protected final String mLogTag; protected SQLiteDatabase mDatabase; - protected Object mMatcherSynchronizer; + protected final Object mMatcherSynchronizer; protected UriMatcher mMatcher; protected MatchDetail[] mMatchDetails; From 1bf2366689b12b75846acfb9761f30f0698b1849 Mon Sep 17 00:00:00 2001 From: Isaac Devine Date: Thu, 6 Aug 2015 20:42:38 +1200 Subject: [PATCH 3/3] Add Foreign Key and heirarchical url support Add support for foreign keys on android versions which support it, as well as adding support for heirarchical urls so that you can have paths like: /posts/1/comments which refers to all the comments for the post with id 1. --- .../simpleprovider/AbstractProvider.java | 83 ++++++++++++++++--- .../de/triplet/simpleprovider/ForeignKey.java | 12 +++ .../simpleprovider/SimpleSQLHelper.java | 20 ++++- .../java/de/triplet/simpleprovider/Utils.java | 38 +++++++-- .../simpleprovider/SimpleProviderTest.java | 70 +++++++++++++++- .../triplet/simpleprovider/TestProvider.java | 15 +++- 6 files changed, 216 insertions(+), 22 deletions(-) create mode 100644 simpleprovider/src/main/java/de/triplet/simpleprovider/ForeignKey.java diff --git a/simpleprovider/src/main/java/de/triplet/simpleprovider/AbstractProvider.java b/simpleprovider/src/main/java/de/triplet/simpleprovider/AbstractProvider.java index 2e77011..40d5713 100644 --- a/simpleprovider/src/main/java/de/triplet/simpleprovider/AbstractProvider.java +++ b/simpleprovider/src/main/java/de/triplet/simpleprovider/AbstractProvider.java @@ -16,6 +16,7 @@ import android.provider.BaseColumns; import android.util.Log; +import java.lang.reflect.Field; import java.util.ArrayList; import java.util.List; @@ -85,16 +86,53 @@ protected void initializeMatcher() { // Add the plural version mMatcher.addURI(authority, tableName, details.size()); - details.add(new MatchDetail(tableName, "vnd.android.cursor.dir/vnd." + getAuthority() + "." + mimeName, false)); + details.add(new MatchDetail(tableName, "vnd.android.cursor.dir/vnd." + getAuthority() + "." + mimeName, null)); // Add the singular version mMatcher.addURI(authority, tableName + "/#", details.size()); - details.add(new MatchDetail(tableName, "vnd.android.cursor.item/vnd." + getAuthority() + "." + mimeName, true)); + details.add(new MatchDetail(tableName, "vnd.android.cursor.item/vnd." + getAuthority() + "." + mimeName, new ForcedColumn(BaseColumns._ID, 1))); + + // Add the hierarchical versions + for (Field field : clazz.getFields()) { + + ForeignKey foreignKey = field.getAnnotation(ForeignKey.class); + + if(foreignKey == null) { + continue; + } + + Column column = field.getAnnotation(Column.class); + if(column == null) { + continue; + } + + Class parent = foreignKey.references(); + + Table parentTable = parent.getAnnotation(Table.class); + + if(parentTable == null) { + throw new IllegalArgumentException("Parent is not a table!"); + } + + String parentTableName = Utils.getTableName(parent, parentTable); + + String foreignKeyColumn = null; + + try { + foreignKeyColumn = field.get(null).toString(); + } catch (IllegalAccessException e) { + throw new IllegalArgumentException("Can't read the field name, needs to be static"); + } + + String nestedUri = parentTableName + "/#/" + tableName; + mMatcher.addURI(authority, nestedUri, details.size()); + details.add(new MatchDetail(tableName, "vnd.android.cursor.dir/vnd." + getAuthority() + "." + mimeName, new ForcedColumn(foreignKeyColumn, 1))); + } } } // Populate the rest. - mMatchDetails = details.toArray(new MatchDetail[0]); + mMatchDetails = details.toArray(new MatchDetail[details.size()]); } } @@ -102,12 +140,22 @@ private static class MatchDetail { public final String tableName; public final String mimeType; - public final boolean forceIdColumn; + public final ForcedColumn forcedColumn; - public MatchDetail(String tableName, String mimeType, boolean forceIdColumn) { + public MatchDetail(String tableName, String mimeType, ForcedColumn forcedColumn) { this.tableName = tableName; this.mimeType = mimeType; - this.forceIdColumn = forceIdColumn; + this.forcedColumn = forcedColumn; + } + } + + private static class ForcedColumn { + public final String columnName; + public final int pathSegment; + + private ForcedColumn(String columnName, int pathSegment) { + this.columnName = columnName; + this.pathSegment = pathSegment; } } @@ -195,8 +243,9 @@ private SelectionBuilder buildBaseQuery(Uri uri) { SelectionBuilder builder = new SelectionBuilder(detail.tableName); - if(detail.forceIdColumn) { - builder.whereEquals(BaseColumns._ID, uri.getLastPathSegment()); + List segments = uri.getPathSegments(); + if(detail.forcedColumn != null) { + builder.whereEquals(detail.forcedColumn.columnName, segments.get(detail.forcedColumn.pathSegment)); } return builder; @@ -212,12 +261,26 @@ public Uri insert(Uri uri, ContentValues values) { throw new IllegalArgumentException("Unsupported content uri"); } - long rowId = mDatabase.insert(mMatchDetails[match].tableName, null, values); + MatchDetail detail = mMatchDetails[match]; + + if(detail.forcedColumn != null) { + // most likely a foreign key, so let's add it to the values. + values.put(detail.forcedColumn.columnName, Long.parseLong(uri.getPathSegments().get(detail.forcedColumn.pathSegment))); + } + + long rowId = mDatabase.insert(detail.tableName, null, values); if (rowId > -1) { + Uri canonical = Uri.parse("content://" + getAuthority() + "/" + detail.tableName); + getContentResolver().notifyChange(uri, null); + // when these differ it means there are two logical collections that the + // content observers may be interested in. + if(!canonical.equals(uri)) { + getContentResolver().notifyChange(canonical, null); + } - return ContentUris.withAppendedId(uri, rowId); + return ContentUris.withAppendedId(canonical, rowId); } return null; diff --git a/simpleprovider/src/main/java/de/triplet/simpleprovider/ForeignKey.java b/simpleprovider/src/main/java/de/triplet/simpleprovider/ForeignKey.java new file mode 100644 index 0000000..41f3bcb --- /dev/null +++ b/simpleprovider/src/main/java/de/triplet/simpleprovider/ForeignKey.java @@ -0,0 +1,12 @@ +package de.triplet.simpleprovider; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface ForeignKey { + Class references(); +} diff --git a/simpleprovider/src/main/java/de/triplet/simpleprovider/SimpleSQLHelper.java b/simpleprovider/src/main/java/de/triplet/simpleprovider/SimpleSQLHelper.java index e428719..6dd5b9b 100644 --- a/simpleprovider/src/main/java/de/triplet/simpleprovider/SimpleSQLHelper.java +++ b/simpleprovider/src/main/java/de/triplet/simpleprovider/SimpleSQLHelper.java @@ -1,9 +1,11 @@ package de.triplet.simpleprovider; +import android.annotation.TargetApi; import android.content.Context; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteException; import android.database.sqlite.SQLiteOpenHelper; +import android.os.Build; import android.text.TextUtils; import android.util.Log; @@ -14,6 +16,8 @@ public class SimpleSQLHelper extends SQLiteOpenHelper { private Class mTableClass; + protected final static boolean mForeignKeysEnabled = Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN; + public SimpleSQLHelper(Context context, String fileName, int schemaVersion) { super(context, fileName, null, schemaVersion); } @@ -30,6 +34,15 @@ private Class getTableClass() { } } + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + @Override + public void onConfigure(SQLiteDatabase db) { + super.onConfigure(db); + // N.B. this will only be called on JELLY_BEAN and above, + // so the field will match exactly. + db.setForeignKeyConstraintsEnabled(mForeignKeysEnabled); + } + @Override public void onCreate(SQLiteDatabase db) { createTables(db); @@ -62,8 +75,10 @@ private void createTable(SQLiteDatabase db, String tableName, Class tableClas for (Field field : tableClass.getFields()) { Column column = field.getAnnotation(Column.class); if (column != null) { + ForeignKey foreignKey = field.getAnnotation(ForeignKey.class); + try { - columns.add(Utils.getColumnConstraint(field, column)); + columns.add(Utils.getColumnConstraint(field, column, mForeignKeysEnabled ? foreignKey : null)); } catch (Exception e) { Log.e("SimpleSQLHelper", "Error accessing " + field, e); } @@ -95,8 +110,9 @@ private void upgradeTable(SQLiteDatabase db, int oldVersion, int newVersion, Str if (column != null) { int since = column.since(); if (oldVersion < since && newVersion >= since) { + ForeignKey foreignKey = field.getAnnotation(ForeignKey.class); try { - db.execSQL("ALTER TABLE " + tableName + " ADD COLUMN " + Utils.getColumnConstraint(field, column) + ";"); + db.execSQL("ALTER TABLE " + tableName + " ADD COLUMN " + Utils.getColumnConstraint(field, column, mForeignKeysEnabled ? foreignKey : null) + ";"); } catch (Exception e) { Log.e("SimpleSQLHelper", "Error accessing " + field, e); } diff --git a/simpleprovider/src/main/java/de/triplet/simpleprovider/Utils.java b/simpleprovider/src/main/java/de/triplet/simpleprovider/Utils.java index f20d19a..0939b0f 100644 --- a/simpleprovider/src/main/java/de/triplet/simpleprovider/Utils.java +++ b/simpleprovider/src/main/java/de/triplet/simpleprovider/Utils.java @@ -1,5 +1,6 @@ package de.triplet.simpleprovider; +import android.provider.BaseColumns; import android.text.TextUtils; import java.lang.reflect.Field; @@ -47,11 +48,38 @@ static String pluralize(String string) { } } - static String getColumnConstraint(Field field, Column column) throws IllegalAccessException { - return field.get(null) + " " + column.value() - + (column.primaryKey() ? " PRIMARY KEY" : "") - + (column.notNull() ? " NOT NULL" : "") - + (column.unique() ? " UNIQUE" : ""); + static String getColumnConstraint(Field field, Column column, ForeignKey foreignKey) throws IllegalAccessException { + StringBuilder columnDefinition = new StringBuilder(); + columnDefinition.append(field.get(null)); + columnDefinition.append(" " + column.value()); + + if(column.primaryKey()) { + columnDefinition.append(" PRIMARY KEY"); + } + + if(column.notNull()) { + columnDefinition.append(" NOT NULL"); + } + + if(column.unique()) { + columnDefinition.append(" UNIQUE"); + } + + // According to SQLite documentation don't have to worry about the order of table creation + // when creating foreign key constraints, as they are only checked when DML statements + // are executed; see: https://www.sqlite.org/foreignkeys.html#fk_schemacommands + if(foreignKey != null) { + columnDefinition.append(" REFERENCES "); + Class parentClazz = foreignKey.references(); + Table parentTable = parentClazz.getAnnotation(Table.class); + columnDefinition.append(getTableName(parentClazz, parentTable)); + columnDefinition.append("(" + BaseColumns._ID + ")"); // always use _id as the parent key. + + // defer the constraints so that batch operations can be done unordered. + columnDefinition.append(" DEFERRABLE INITIALLY DEFERRED"); + } + + return columnDefinition.toString(); } } diff --git a/simpleprovider/src/test/java/de/triplet/simpleprovider/SimpleProviderTest.java b/simpleprovider/src/test/java/de/triplet/simpleprovider/SimpleProviderTest.java index a7f2f37..bce450c 100644 --- a/simpleprovider/src/test/java/de/triplet/simpleprovider/SimpleProviderTest.java +++ b/simpleprovider/src/test/java/de/triplet/simpleprovider/SimpleProviderTest.java @@ -25,6 +25,8 @@ public class SimpleProviderTest { private static final String CONTENT_1 = "This is some content we want to store in a post!"; private static final String CONTENT_2 = "Another post"; + private static final String COMMENT_RESPONSE_1 = "this is a comment!"; + private static final String COMMENT_RESPONSE_2 = "this is a second comment!"; TestProvider mProvider; ContentResolver mContentResolver; Uri mPostsUri; @@ -55,8 +57,7 @@ public void testInsert() { assertNotNull("Resulting cursor must not be null", c); assertEquals("Query should return one post", c.getCount(), 1); assertTrue(c.moveToFirst()); - assertEquals("Entry should have the correct content", - CONTENT_1, c.getString(c.getColumnIndex(TestProvider.Post.CONTENT))); + assertEquals("Entry should have the correct content", CONTENT_1, c.getString(c.getColumnIndex(TestProvider.Post.CONTENT))); } @Test @@ -81,11 +82,72 @@ public void testQueryById() { // Make sure the query has returned the correct entity assertNotNull("Resulting cursor must not be null", c); assertTrue("We should be able to get the first entry", c.moveToFirst()); - assertEquals("Entry should have the correct content", - CONTENT_2, c.getString(c.getColumnIndex(TestProvider.Post.CONTENT))); + assertEquals("Entry should have the correct content", CONTENT_2, c.getString(c.getColumnIndex(TestProvider.Post.CONTENT))); assertFalse("There shouldn't be any more entries", c.moveToNext()); } + @Test + public void testInsertByParentId() { + ContentValues values = new ContentValues(); + values.put(TestProvider.Post.CONTENT, "first post"); + + Uri firstPost = mContentResolver.insert(mPostsUri, values); + + ContentValues comment = new ContentValues(); + + comment.put(TestProvider.Comment.RESPONSE, COMMENT_RESPONSE_1); + + Uri commentUri = mContentResolver.insert(Uri.parse("content://" + TestProvider.AUTHORITY + "/posts/" + Long.toString(ContentUris.parseId(firstPost)) + "/comments"), comment); + + // Perform a "SELECT * FROM posts" query + Cursor c = mContentResolver.query(commentUri, new String[] { TestProvider.Comment.POST, TestProvider.Comment.RESPONSE}, null, null, null); + + // Make sure the query has returned (the) one element + assertNotNull("Resulting cursor must not be null", c); + assertEquals("Query should return one comment", c.getCount(), 1); + assertTrue(c.moveToFirst()); + assertEquals("Entry should have the correct response", COMMENT_RESPONSE_1, c.getString(c.getColumnIndex(TestProvider.Comment.RESPONSE))); + assertEquals("Entry should have the correct post_id", ContentUris.parseId(firstPost), c.getLong(c.getColumnIndex(TestProvider.Comment.POST))); + + } + + @Test + public void testQueryByParentId() { + ContentValues values = new ContentValues(); + values.put(TestProvider.Post.CONTENT, "first post"); + + Uri firstPost = mContentResolver.insert(mPostsUri, values); + + values.put(TestProvider.Post.CONTENT, "second"); + + Uri secondPost = mContentResolver.insert(mPostsUri, values); + + ContentValues firstComment = new ContentValues(); + firstComment.put(TestProvider.Comment.RESPONSE, COMMENT_RESPONSE_1); + firstComment.put(TestProvider.Comment.POST, ContentUris.parseId(firstPost)); + + Uri commentOne = mContentResolver.insert(Uri.parse("content://" + TestProvider.AUTHORITY + "/comments"), firstComment); + + ContentValues secondComment = new ContentValues(); + secondComment.put(TestProvider.Comment.RESPONSE, COMMENT_RESPONSE_2); + secondComment.put(TestProvider.Comment.POST, ContentUris.parseId(secondPost)); + + mContentResolver.insert(Uri.parse("content://" + TestProvider.AUTHORITY + "/comments"), secondComment); + + Uri firstPostComments = Uri.parse("content://" + TestProvider.AUTHORITY + "/posts/" + ContentUris.parseId(firstPost) + "/comments"); + + // Now get all the comments for the first post. + Cursor c = mContentResolver.query(firstPostComments, new String[] { TestProvider.Comment.ID, TestProvider.Comment.POST, TestProvider.Comment.RESPONSE}, null, null, null); + + // Make sure the query has returned (the) one element + assertNotNull("Resulting cursor must not be null", c); + assertEquals("Query should return one comment", c.getCount(), 1); + assertTrue(c.moveToFirst()); + assertEquals("Entry should have the correct id", ContentUris.parseId(commentOne), c.getLong(c.getColumnIndex(TestProvider.Comment.ID))); + assertEquals("Entry should have the correct response", COMMENT_RESPONSE_1, c.getString(c.getColumnIndex(TestProvider.Comment.RESPONSE))); + assertEquals("Entry should have the correct post_id", ContentUris.parseId(firstPost), c.getLong(c.getColumnIndex(TestProvider.Comment.POST))); + } + @Test public void testGetType() { String actual = mContentResolver.getType(mPostsUri); diff --git a/simpleprovider/src/test/java/de/triplet/simpleprovider/TestProvider.java b/simpleprovider/src/test/java/de/triplet/simpleprovider/TestProvider.java index 780f05e..099b5a7 100644 --- a/simpleprovider/src/test/java/de/triplet/simpleprovider/TestProvider.java +++ b/simpleprovider/src/test/java/de/triplet/simpleprovider/TestProvider.java @@ -12,10 +12,23 @@ protected String getAuthority() { @Table public class Post { - @Column(Column.FieldType.INTEGER) + @Column(value = Column.FieldType.INTEGER, primaryKey = true) public static final String ID = "_id"; @Column(Column.FieldType.TEXT) public static final String CONTENT = "content"; } + + @Table + public class Comment { + @Column(value = Column.FieldType.INTEGER, primaryKey = true) + public static final String ID = "_id"; + + @Column(Column.FieldType.TEXT) + public static final String RESPONSE = "response"; + + @Column(value = Column.FieldType.INTEGER, notNull = true) + @ForeignKey(references = Post.class) + public static final String POST = "post_id"; + } }