Add more tests

Test: ./run-all-tests.sh

Change-Id: Ib7db8e429a6da41363cfdb430e8ab3b0cda408dd
diff --git a/src/com/android/providers/contacts/ContactsDatabaseHelper.java b/src/com/android/providers/contacts/ContactsDatabaseHelper.java
index 8d0d06a..f0c53de 100644
--- a/src/com/android/providers/contacts/ContactsDatabaseHelper.java
+++ b/src/com/android/providers/contacts/ContactsDatabaseHelper.java
@@ -4952,16 +4952,26 @@
      * Ensure (a piece of) SQL is valid and doesn't contain disallowed tokens.
      */
     public void validateSql(String callerPackage, String sqlPiece) {
-        runSqlValidation(callerPackage, () -> getSqlChecker().ensureNoInvalidTokens(sqlPiece));
+        // TODO Replace the Runnable with a lambda -- which would crash right now due to an art bug?
+        runSqlValidation(callerPackage, new Runnable() {
+            @Override
+            public void run() {
+                ContactsDatabaseHelper.this.getSqlChecker().ensureNoInvalidTokens(sqlPiece);
+            }
+        });
     }
 
     /**
      * Ensure all keys in {@code values} are valid. (i.e. they're all single token.)
      */
     public void validateContentValues(String callerPackage, ContentValues values) {
-        runSqlValidation(callerPackage, () -> {
-            for (String key : values.keySet()) {
-                getSqlChecker().ensureSingleTokenOnly(key);
+        // TODO Replace the Runnable with a lambda -- which would crash right now due to an art bug?
+        runSqlValidation(callerPackage, new Runnable() {
+            @Override
+            public void run() {
+                for (String key : values.keySet()) {
+                    ContactsDatabaseHelper.this.getSqlChecker().ensureSingleTokenOnly(key);
+                }
             }
         });
    }
@@ -4970,17 +4980,20 @@
      * Ensure all column names in {@code projection} are valid. (i.e. they're all single token.)
      */
     public void validateProjection(String callerPackage, String[] projection) {
+        // TODO Replace the Runnable with a lambda -- which would crash right now due to an art bug?
         if (projection != null) {
-            runSqlValidation(callerPackage, () -> {
-                for (String column : projection) {
-                    getSqlChecker().ensureSingleTokenOnly(column);
+            runSqlValidation(callerPackage, new Runnable() {
+                @Override
+                public void run() {
+                    for (String column : projection) {
+                        ContactsDatabaseHelper.this.getSqlChecker().ensureSingleTokenOnly(column);
+                    }
                 }
             });
         }
     }
 
     private void runSqlValidation(String callerPackage, Runnable r) {
-        // STOPSHIP Allow certain system apps to access it
         try {
             r.run();
         } catch (InvalidSqlException e) {
diff --git a/src/com/android/providers/contacts/ContactsProvider2.java b/src/com/android/providers/contacts/ContactsProvider2.java
index 8399441..bbbde37 100644
--- a/src/com/android/providers/contacts/ContactsProvider2.java
+++ b/src/com/android/providers/contacts/ContactsProvider2.java
@@ -8893,6 +8893,9 @@
             }
 
             case PROFILE_AS_VCARD: {
+                if (!mode.equals("r")) {
+                    throw new IllegalArgumentException("Write is not supported.");
+                }
                 // When opening a contact as file, we pass back contents as a
                 // vCard-encoded stream. We build into a local buffer first,
                 // then pipe into MemoryFile once the exact size is known.
@@ -8902,6 +8905,9 @@
             }
 
             case CONTACTS_AS_VCARD: {
+                if (!mode.equals("r")) {
+                    throw new IllegalArgumentException("Write is not supported.");
+                }
                 // When opening a contact as file, we pass back contents as a
                 // vCard-encoded stream. We build into a local buffer first,
                 // then pipe into MemoryFile once the exact size is known.
@@ -8911,6 +8917,9 @@
             }
 
             case CONTACTS_AS_MULTI_VCARD: {
+                if (!mode.equals("r")) {
+                    throw new IllegalArgumentException("Write is not supported.");
+                }
                 final String lookupKeys = uri.getPathSegments().get(2);
                 final String[] lookupKeyList = lookupKeys.split(":");
                 final StringBuilder inBuilder = new StringBuilder();
@@ -8956,7 +8965,8 @@
 
             default:
                 throw new FileNotFoundException(
-                        mDbHelper.get().exceptionMessage("File does not exist", uri));
+                        mDbHelper.get().exceptionMessage(
+                                "Stream I/O not supported on this URI.", uri));
         }
     }
 
diff --git a/tests2/src/com/android/providers/contacts/tests2/AllUriTest.java b/tests2/src/com/android/providers/contacts/tests2/AllUriTest.java
index a38a4a6..f7747cc 100644
--- a/tests2/src/com/android/providers/contacts/tests2/AllUriTest.java
+++ b/tests2/src/com/android/providers/contacts/tests2/AllUriTest.java
@@ -15,20 +15,46 @@
  */
 package com.android.providers.contacts.tests2;
 
+import android.content.ContentResolver;
+import android.content.ContentValues;
 import android.database.Cursor;
 import android.net.Uri;
 import android.os.CancellationSignal;
 import android.provider.ContactsContract;
 import android.provider.ContactsContract.Contacts;
 import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.SyncState;
 import android.test.AndroidTestCase;
 import android.test.suitebuilder.annotation.LargeTest;
 import android.util.Log;
 
 import junit.framework.AssertionFailedError;
 
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+import java.io.OutputStream;
 import java.util.ArrayList;
 
+/*
+ * TODO The following operations would fail, not because they're not supported, but because of
+ * missing parameters.  Fix them.
+insert for 'content://com.android.contacts/contacts' failed: Aggregate contacts are created automatically
+insert for 'content://com.android.contacts/raw_contacts/1/data' failed: mimetype is required
+update for 'content://com.android.contacts/raw_contacts/1/stream_items/1' failed: Empty values
+insert for 'content://com.android.contacts/data' failed: raw_contact_id is required
+insert for 'content://com.android.contacts/settings' failed: Must specify both or neither of ACCOUNT_NAME and ACCOUNT_TYPE; URI: content://com.android.contacts/settings?account_type=1, calling user: com.android.providers.contacts.tests2, calling package:com.android.providers.contacts.tests2
+insert for 'content://com.android.contacts/status_updates' failed: PROTOCOL and IM_HANDLE are required
+insert for 'content://com.android.contacts/profile' failed: The profile contact is created automatically
+insert for 'content://com.android.contacts/profile/data' failed: raw_contact_id is required
+insert for 'content://com.android.contacts/profile/raw_contacts/1/data' failed: mimetype is required
+insert for 'content://com.android.contacts/profile/status_updates' failed: PROTOCOL and IM_HANDLE are required
+
+
+openInputStream for 'content://com.android.contacts/contacts/as_multi_vcard/XXX' failed: Caught Exception: Invalid lookup id: XXX
+openInputStream for 'content://com.android.contacts/directory_file_enterprise/XXX?directory=0' failed: Caught Exception: java.lang.IllegalArgumentException: Directory is not a remote directory: content://com.android.contacts/directory_file_enterprise/XXX?directory=0
+openOutputStream for 'content://com.android.contacts/directory_file_enterprise/XXX?directory=0' failed: Caught Exception: java.lang.IllegalArgumentException: Directory is not a remote directory: content://com.android.contacts/directory_file_enterprise/XXX?directory=0
+*/
+
 /**
  * TODO Add test for delete/update/insert too.
  * TODO Copy it to CTS
@@ -42,67 +68,74 @@
     // The following markers are planned, but not implemented and the definition below is not all
     // correct yet.
     // "d" : supports delete.
-    // "u" : supports delete.
+    // "u" : supports update.
     // "i" : supports insert.
     // "r" : supports read.
     // "w" : supports write.
     // "s" : has x_times_contacted and x_last_time_contacted.
     // "t" : has x_times_used and x_last_time_used.
     private static final String[][] URIs = {
-            {"content://com.android.contacts/contacts", "s"},
-            {"content://com.android.contacts/contacts/1", "s"},
+            {"content://com.android.contacts/contacts", "sud"},
+            {"content://com.android.contacts/contacts/1", "sud"},
             {"content://com.android.contacts/contacts/1/data", "t"},
             {"content://com.android.contacts/contacts/1/entities", "t"},
             {"content://com.android.contacts/contacts/1/suggestions"},
             {"content://com.android.contacts/contacts/1/suggestions/XXX"},
-            {"content://com.android.contacts/contacts/1/photo"},
-            {"content://com.android.contacts/contacts/1/display_photo", "-rw"},
-            {"content://com.android.contacts/contacts_corp/1/photo", "-rw"},
-            {"content://com.android.contacts/contacts_corp/1/display_photo", "-rw"},
-            {"content://com.android.contacts/contacts/1/stream_items"},
+            {"content://com.android.contacts/contacts/1/photo", "r"},
+            {"content://com.android.contacts/contacts/1/display_photo", "-r"},
+            {"content://com.android.contacts/contacts_corp/1/photo", "-r"},
+            {"content://com.android.contacts/contacts_corp/1/display_photo", "-r"},
+
             {"content://com.android.contacts/contacts/filter", "s"},
             {"content://com.android.contacts/contacts/filter/XXX", "s"},
-            {"content://com.android.contacts/contacts/lookup/nlookup", "s"},
+
+            {"content://com.android.contacts/contacts/lookup/nlookup", "sud"},
             {"content://com.android.contacts/contacts/lookup/nlookup/data", "t"},
-            {"content://com.android.contacts/contacts/lookup/nlookup/photo", "t"},
-            {"content://com.android.contacts/contacts/lookup/nlookup/1", "s"},
+            {"content://com.android.contacts/contacts/lookup/nlookup/photo", "tr"},
+
+            {"content://com.android.contacts/contacts/lookup/nlookup/1", "sud"},
             {"content://com.android.contacts/contacts/lookup/nlookup/1/data"},
-            {"content://com.android.contacts/contacts/lookup/nlookup/1/photo"},
-            {"content://com.android.contacts/contacts/lookup/nlookup/display_photo", "-rw"},
-            {"content://com.android.contacts/contacts/lookup/nlookup/1/display_photo", "-rw"},
+            {"content://com.android.contacts/contacts/lookup/nlookup/1/photo", "r"},
+            {"content://com.android.contacts/contacts/lookup/nlookup/display_photo", "-r"},
+            {"content://com.android.contacts/contacts/lookup/nlookup/1/display_photo", "-r"},
             {"content://com.android.contacts/contacts/lookup/nlookup/entities"},
             {"content://com.android.contacts/contacts/lookup/nlookup/1/entities"},
-            {"content://com.android.contacts/contacts/lookup/nlookup/stream_items"},
-            {"content://com.android.contacts/contacts/lookup/nlookup/1/stream_items"},
-            {"content://com.android.contacts/contacts/as_vcard/nlookup"},
+
+            {"content://com.android.contacts/contacts/as_vcard/nlookup", "r"},
             {"content://com.android.contacts/contacts/as_multi_vcard/XXX"},
+
             {"content://com.android.contacts/contacts/strequent/", "s"},
             {"content://com.android.contacts/contacts/strequent/filter/XXX", "s"},
+
             {"content://com.android.contacts/contacts/group/XXX"},
+
             {"content://com.android.contacts/contacts/frequent", "s"},
             {"content://com.android.contacts/contacts/delete_usage", "-d"},
             {"content://com.android.contacts/contacts/filter_enterprise?directory=0", "s"},
             {"content://com.android.contacts/contacts/filter_enterprise/XXX?directory=0", "s"},
-            {"content://com.android.contacts/raw_contacts", "s"},
-            {"content://com.android.contacts/raw_contacts/1", "s"},
-            {"content://com.android.contacts/raw_contacts/1/data", "t"},
+
+            {"content://com.android.contacts/raw_contacts", "siud"},
+            {"content://com.android.contacts/raw_contacts/1", "sud"},
+            {"content://com.android.contacts/raw_contacts/1/data", "tu"},
             {"content://com.android.contacts/raw_contacts/1/display_photo", "-rw"},
             {"content://com.android.contacts/raw_contacts/1/entity"},
-            {"content://com.android.contacts/raw_contacts/1/stream_items"},
-            {"content://com.android.contacts/raw_contacts/1/stream_items/1"},
+
             {"content://com.android.contacts/raw_contact_entities"},
             {"content://com.android.contacts/raw_contact_entities_corp", "!"},
-            {"content://com.android.contacts/data", "t"},
-            {"content://com.android.contacts/data/1", "t"},
+
+            {"content://com.android.contacts/data", "tud"},
+            {"content://com.android.contacts/data/1", "tudr"},
             {"content://com.android.contacts/data/phones", "t"},
             {"content://com.android.contacts/data_enterprise/phones", "!"},
-            {"content://com.android.contacts/data/phones/1", "t"},
+            {"content://com.android.contacts/data/phones/1", "tud"},
             {"content://com.android.contacts/data/phones/filter", "t"},
             {"content://com.android.contacts/data/phones/filter/XXX", "t"},
+
             {"content://com.android.contacts/data/phones/filter_enterprise?directory=0", "t"},
             {"content://com.android.contacts/data/phones/filter_enterprise/XXX?directory=0", "t"},
+
             {"content://com.android.contacts/data/emails", "t"},
-            {"content://com.android.contacts/data/emails/1", "t"},
+            {"content://com.android.contacts/data/emails/1", "tud"},
             {"content://com.android.contacts/data/emails/lookup", "t"},
             {"content://com.android.contacts/data/emails/lookup/XXX", "t"},
             {"content://com.android.contacts/data/emails/filter", "t"},
@@ -112,10 +145,10 @@
             {"content://com.android.contacts/data/emails/lookup_enterprise", "t"},
             {"content://com.android.contacts/data/emails/lookup_enterprise/XXX", "t"},
             {"content://com.android.contacts/data/postals", "t"},
-            {"content://com.android.contacts/data/postals/1", "t"},
-            {"content://com.android.contacts/data/usagefeedback/XXX", "-u"},
+            {"content://com.android.contacts/data/postals/1", "tud"},
+            {"content://com.android.contacts/data/usagefeedback/1,2,3", "-u"},
             {"content://com.android.contacts/data/callables/", "t"},
-            {"content://com.android.contacts/data/callables/1", "t"},
+            {"content://com.android.contacts/data/callables/1", "tud"},
             {"content://com.android.contacts/data/callables/filter", "t"},
             {"content://com.android.contacts/data/callables/filter/XXX", "t"},
             {"content://com.android.contacts/data/callables/filter_enterprise?directory=0", "t"},
@@ -124,95 +157,54 @@
             {"content://com.android.contacts/data/contactables/", "t"},
             {"content://com.android.contacts/data/contactables/filter", "t"},
             {"content://com.android.contacts/data/contactables/filter/XXX", "t"},
-            {"content://com.android.contacts/groups"},
-            {"content://com.android.contacts/groups/1"},
+
+            {"content://com.android.contacts/groups", "iud"},
+            {"content://com.android.contacts/groups/1", "ud"},
             {"content://com.android.contacts/groups_summary"},
-            {"content://com.android.contacts/syncstate"},
-            {"content://com.android.contacts/syncstate/1", "-du"},
-            {"content://com.android.contacts/profile/syncstate"},
+            {"content://com.android.contacts/syncstate", "iud"},
+            {"content://com.android.contacts/syncstate/1", "-ud"},
+            {"content://com.android.contacts/profile/syncstate", "iud"},
             {"content://com.android.contacts/phone_lookup/XXX"},
             {"content://com.android.contacts/phone_lookup_enterprise/XXX"},
-            {"content://com.android.contacts/aggregation_exceptions"},
-            {"content://com.android.contacts/settings"},
-            {"content://com.android.contacts/status_updates"},
+            {"content://com.android.contacts/aggregation_exceptions", "u"},
+            {"content://com.android.contacts/settings", "ud"},
+            {"content://com.android.contacts/status_updates", "ud"},
             {"content://com.android.contacts/status_updates/1"},
             {"content://com.android.contacts/search_suggest_query"},
             {"content://com.android.contacts/search_suggest_query/XXX"},
             {"content://com.android.contacts/search_suggest_shortcut/XXX"},
             {"content://com.android.contacts/provider_status"},
-            {"content://com.android.contacts/directories"},
+            {"content://com.android.contacts/directories", "u"},
             {"content://com.android.contacts/directories/1"},
             {"content://com.android.contacts/directories_enterprise"},
             {"content://com.android.contacts/directories_enterprise/1"},
             {"content://com.android.contacts/complete_name"},
-            {"content://com.android.contacts/profile", "s"},
+            {"content://com.android.contacts/profile", "su"},
             {"content://com.android.contacts/profile/entities", "s"},
-            {"content://com.android.contacts/profile/data", "t"},
-            {"content://com.android.contacts/profile/data/1", "t"},
+            {"content://com.android.contacts/profile/data", "tud"},
+            {"content://com.android.contacts/profile/data/1", "td"},
             {"content://com.android.contacts/profile/photo", "t"},
-            {"content://com.android.contacts/profile/display_photo", "-rw"},
-            {"content://com.android.contacts/profile/as_vcard"},
-            {"content://com.android.contacts/profile/raw_contacts", "s"},
-            {"content://com.android.contacts/profile/raw_contacts/1", "s"},
-            {"content://com.android.contacts/profile/raw_contacts/1/data", "t"},
+            {"content://com.android.contacts/profile/display_photo", "-r"},
+            {"content://com.android.contacts/profile/as_vcard", "r"},
+            {"content://com.android.contacts/profile/raw_contacts", "siud"},
+
+            // Note this should have supported update... Too late to add.
+            {"content://com.android.contacts/profile/raw_contacts/1", "sd"},
+            {"content://com.android.contacts/profile/raw_contacts/1/data", "tu"},
             {"content://com.android.contacts/profile/raw_contacts/1/entity"},
-            {"content://com.android.contacts/profile/status_updates"},
+            {"content://com.android.contacts/profile/status_updates", "ud"},
             {"content://com.android.contacts/profile/raw_contact_entities"},
-            {"content://com.android.contacts/stream_items"},
-            {"content://com.android.contacts/stream_items/photo"},
-            {"content://com.android.contacts/stream_items/1"},
-            {"content://com.android.contacts/stream_items/1/photo"},
-            {"content://com.android.contacts/stream_items/1/photo/1"},
-            {"content://com.android.contacts/stream_items_limit"},
-            {"content://com.android.contacts/display_photo/1", "-rw"},
+            {"content://com.android.contacts/display_photo/1", "-r"},
             {"content://com.android.contacts/photo_dimensions"},
             {"content://com.android.contacts/deleted_contacts"},
             {"content://com.android.contacts/deleted_contacts/1"},
-            {"content://com.android.contacts/directory_file_enterprise/XXX", "-rw"},
-
-            {"content://contacts/extensions"},
-            {"content://contacts/extensions/1"},
-            {"content://contacts/groups"},
-            {"content://contacts/groups/1"},
-            {"content://contacts/groups/name/XXX/members"},
-            {"content://contacts/groups/system_id/XXX/members"},
-            {"content://contacts/groupmembership"},
-            {"content://contacts/groupmembership/1"},
-            {"content://contacts/people", "s"},
-            {"content://contacts/people/filter/XXX"},
-            {"content://contacts/people/1"},
-            {"content://contacts/people/1/extensions"},
-            {"content://contacts/people/1/extensions/1"},
-            {"content://contacts/people/1/phones"},
-            {"content://contacts/people/1/phones/1"},
-            {"content://contacts/people/1/photo"},
-            {"content://contacts/people/1/contact_methods"},
-            {"content://contacts/people/1/contact_methods/1"},
-            {"content://contacts/people/1/organizations"},
-            {"content://contacts/people/1/organizations/1"},
-            {"content://contacts/people/1/groupmembership"},
-            {"content://contacts/people/1/groupmembership/1"},
-            {"content://contacts/people/1/update_contact_time", "-u"},
-            {"content://contacts/deleted_people", "-"},
-            {"content://contacts/deleted_groups", "-"},
-            {"content://contacts/phones"},
-            {"content://contacts/phones/filter/XXX"},
-            {"content://contacts/phones/1"},
-            {"content://contacts/photos"},
-            {"content://contacts/photos/1"},
-            {"content://contacts/contact_methods"},
-            {"content://contacts/contact_methods/email"},
-            {"content://contacts/contact_methods/1"},
-            {"content://contacts/organizations"},
-            {"content://contacts/organizations/1"},
-            {"content://contacts/search_suggest_query"},
-            {"content://contacts/search_suggest_query/XXX"},
-            {"content://contacts/search_suggest_shortcut/XXX"},
-            {"content://contacts/settings"},
+            {"content://com.android.contacts/directory_file_enterprise/XXX?directory=0", "-"},
     };
 
     private static final String[] ARG1 = {"-1"};
 
+    private ContentResolver mResolver;
+
     private ArrayList<String> mFailures;
 
     @Override
@@ -220,6 +212,7 @@
         super.setUp();
 
         mFailures = new ArrayList<>();
+        mResolver = getContext().getContentResolver();
     }
 
     @Override
@@ -231,11 +224,14 @@
         super.tearDown();
     }
 
-    private void addFailure(String message) {
-        mFailures.add(message);
-        Log.e(TAG, "Failed: " + message);
-        if (mFailures.size() > 100) {
-            // Too many failures...
+    private void addFailure(String message, Throwable th) {
+        Log.e(TAG, "Failed: " + message, th);
+
+        final int MAX = 100;
+        if (mFailures.size() == MAX) {
+            mFailures.add("Too many failures.");
+        } else if (mFailures.size() > MAX) {
+            // Too many failures already...
         } else {
             mFailures.add(message);
         }
@@ -264,6 +260,10 @@
         mFailures = null;
     }
 
+    private static Uri getUri(String[] path) {
+        return Uri.parse(path[0]);
+    }
+
     private static boolean supportsQuery(String[] path) {
         if (path.length == 1) {
             return true; // supports query by default.
@@ -271,12 +271,28 @@
         return !(path[1].contains("-") || path[1].contains("!"));
     }
 
-    private static Uri getUri(String[] path) {
-        return Uri.parse(path[0]);
+    private static boolean supportsInsert(String[] path) {
+        return (path.length) >= 2 && path[1].contains("i");
+    }
+
+    private static boolean supportsUpdate(String[] path) {
+        return (path.length) >= 2 && path[1].contains("u");
+    }
+
+    private static boolean supportsDelete(String[] path) {
+        return (path.length) >= 2 && path[1].contains("d");
+    }
+
+    private static boolean supportsRead(String[] path) {
+        return (path.length) >= 2 && path[1].contains("r");
+    }
+
+    private static boolean supportsWrite(String[] path) {
+        return (path.length) >= 2 && path[1].contains("w");
     }
 
     private String[] getColumns(Uri uri) {
-        try (Cursor c = getContext().getContentResolver().query(uri,
+        try (Cursor c = mResolver.query(uri,
                 null, // projection
                 "1=2", // selection
                 null, // selection args
@@ -290,32 +306,47 @@
             String[] projection, String selection,
             String[] selectionArgs, String sortOrder) {
         try {
-            try (Cursor c = getContext().getContentResolver().query(uri, projection, selection,
+            try (Cursor c = mResolver.query(uri, projection, selection,
                     selectionArgs, sortOrder)) {
                 c.moveToFirst();
             }
         } catch (Throwable th) {
-            addFailure("Query failed: URI=" + uri + " Message=" + th.getMessage());
+            addFailure("Query failed: URI=" + uri + " Message=" + th.getMessage(), th);
         }
         try {
             // With CancellationSignal.
-            try (Cursor c = getContext().getContentResolver().query(uri, projection, selection,
+            try (Cursor c = mResolver.query(uri, projection, selection,
                     selectionArgs, sortOrder, new CancellationSignal())) {
                 c.moveToFirst();
             }
         } catch (Throwable th) {
-            addFailure("Query with cancel failed: URI=" + uri + " Message=" + th.getMessage());
+            addFailure("Query with cancel failed: URI=" + uri + " Message=" + th.getMessage(), th);
         }
         try {
             // With limit.
-            try (Cursor c = getContext().getContentResolver().query(
+            try (Cursor c = mResolver.query(
                     uri.buildUpon().appendQueryParameter(
                             ContactsContract.LIMIT_PARAM_KEY, "0").build(),
                     projection, selection, selectionArgs, sortOrder)) {
                 c.moveToFirst();
             }
         } catch (Throwable th) {
-            addFailure("Query with limit failed: URI=" + uri + " Message=" + th.getMessage());
+            addFailure("Query with limit failed: URI=" + uri + " Message=" + th.getMessage(), th);
+        }
+
+        try {
+            // With account.
+            try (Cursor c = mResolver.query(
+                    uri.buildUpon()
+                            .appendQueryParameter(RawContacts.ACCOUNT_NAME, "a")
+                            .appendQueryParameter(RawContacts.ACCOUNT_TYPE, "b")
+                            .appendQueryParameter(RawContacts.DATA_SET, "c")
+                            .build(),
+                    projection, selection, selectionArgs, sortOrder)) {
+                c.moveToFirst();
+            }
+        } catch (Throwable th) {
+            addFailure("Query with limit failed: URI=" + uri + " Message=" + th.getMessage(), th);
         }
     }
 
@@ -323,7 +354,7 @@
             String[] projection, String selection,
             String[] selectionArgs, String sortOrder) {
         try {
-            try (Cursor c = getContext().getContentResolver().query(uri, projection, selection,
+            try (Cursor c = mResolver.query(uri, projection, selection,
                     selectionArgs, sortOrder)) {
                 c.moveToFirst();
             }
@@ -331,7 +362,7 @@
             // pass.
             return;
         }
-        addFailure("Query on " + uri + " expected to fail, but succeeded.");
+        addFailure("Query on " + uri + " expected to fail, but succeeded.", null);
     }
 
     /**
@@ -358,8 +389,8 @@
             final Uri uri = getUri(path);
 
             for (String column : getColumns(uri)) {
-                if (column.toLowerCase().startsWith(ContactsContract.HIDDEN_COLUMN_PREFIX)) {  // doesn't seem to be working
-                    addFailure("Uri " + uri + " returned hidden column " + column);
+                if (column.toLowerCase().startsWith(ContactsContract.HIDDEN_COLUMN_PREFIX)) {
+                    addFailure("Uri " + uri + " returned hidden column " + column, null);
                 }
             }
         }
@@ -517,13 +548,13 @@
 
     private void checkColumnAccessible(Uri uri, String column) {
         try {
-            try (Cursor c = getContext().getContentResolver().query(
+            try (Cursor c = mResolver.query(
                     uri, new String[]{column}, column + "=0", null, column
             )) {
                 c.moveToFirst();
             }
         } catch (Throwable th) {
-            addFailure("Query failed: URI=" + uri + " Message=" + th.getMessage());
+            addFailure("Query failed: URI=" + uri + " Message=" + th.getMessage(), th);
         }
     }
 
@@ -541,7 +572,7 @@
     private void checkColumnNotAccessibleInner(Uri uri, String[] projection, String selection,
             String[] selectionArgs, String sortOrder) {
         try {
-            try (Cursor c = getContext().getContentResolver().query(uri, projection, selection,
+            try (Cursor c = mResolver.query(uri, projection, selection,
                     selectionArgs, sortOrder)) {
                 c.moveToFirst();
             }
@@ -550,7 +581,7 @@
             return;
         }
         addFailure("Query on " + uri +
-                " expected to throw IllegalArgumentException, but succeeded.");
+                " expected to throw IllegalArgumentException, but succeeded.", null);
     }
 
     private void checkColumnNotAccessible(Uri uri, String column) {
@@ -593,4 +624,93 @@
         }
         failIfFailed();
     }
+
+    private void checkExecutable(String operation, Uri uri, boolean shouldWork, Runnable r) {
+        if (shouldWork) {
+            try {
+                r.run();
+            } catch (Exception e) {
+                addFailure(operation + " for '" + uri + "' failed: " + e.getMessage(), e);
+            }
+        } else {
+            try {
+                r.run();
+                addFailure(operation + " for '" + uri + "' NOT failed.", null);
+            } catch (Exception expected) {
+            }
+        }
+    }
+
+    public void testAllOperations() {
+        final ContentValues cv = new ContentValues();
+
+        for (String[] path : URIs) {
+            final Uri uri = getUri(path);
+
+            cv.clear();
+            if (supportsQuery(path)) {
+                cv.put(getColumns(uri)[0], 1);
+            } else {
+                cv.put("_id", 1);
+            }
+            if (uri.toString().contains("syncstate")) {
+                cv.put(SyncState.ACCOUNT_NAME, "abc");
+                cv.put(SyncState.ACCOUNT_TYPE, "def");
+            }
+
+            checkExecutable("insert", uri, supportsInsert(path), () -> {
+                final Uri newUri = mResolver.insert(uri, cv);
+                if (newUri == null) {
+                    addFailure("Insert for '" + uri + "' returned null.", null);
+                } else {
+                    // "profile/raw_contacts/#" is missing update support.  too late to add, so
+                    // just skip.
+                    if (!newUri.toString().startsWith(
+                            "content://com.android.contacts/profile/raw_contacts/")) {
+                        checkExecutable("insert -> update", newUri, true, () -> {
+                            mResolver.update(newUri, cv, null, null);
+                        });
+                    }
+                    checkExecutable("insert -> delete", newUri, true, () -> {
+                        mResolver.delete(newUri, null, null);
+                    });
+                }
+            });
+            checkExecutable("update", uri, supportsUpdate(path), () -> {
+                mResolver.update(uri, cv, "1=2", null);
+            });
+            checkExecutable("delete", uri, supportsDelete(path), () -> {
+                mResolver.delete(uri, "1=2", null);
+            });
+        }
+        failIfFailed();
+    }
+
+    public void testAllFileOperations() {
+        for (String[] path : URIs) {
+            final Uri uri = getUri(path);
+
+            checkExecutable("openInputStream", uri, supportsRead(path), () -> {
+                try (InputStream st = mResolver.openInputStream(uri)) {
+                } catch (FileNotFoundException e) {
+                    // TODO This happens because we try to read nonexistent photos.  Ideally
+                    // we should actually check it's readable.
+                    if (e.getMessage().contains("Stream I/O not supported")) {
+                        throw new RuntimeException("Caught Exception: " + e.toString(), e);
+                    }
+                } catch (Exception e) {
+                    throw new RuntimeException("Caught Exception: " + e.toString(), e);
+                }
+            });
+            checkExecutable("openOutputStream", uri, supportsWrite(path), () -> {
+                try (OutputStream st = mResolver.openOutputStream(uri)) {
+                } catch (Exception e) {
+                    throw new RuntimeException("Caught Exception: " + e.toString(), e);
+                }
+            });
+        }
+        failIfFailed();
+    }
 }
+
+