1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License
15  */
16 
17 package android.provider.cts.contacts;
18 
19 import android.content.ContentResolver;
20 import android.content.ContentValues;
21 import android.database.Cursor;
22 import android.net.Uri;
23 import android.os.CancellationSignal;
24 import android.provider.ContactsContract;
25 import android.provider.ContactsContract.Contacts;
26 import android.provider.ContactsContract.RawContacts;
27 import android.provider.ContactsContract.SyncState;
28 import android.test.AndroidTestCase;
29 import android.test.suitebuilder.annotation.LargeTest;
30 import android.util.Log;
31 
32 import junit.framework.AssertionFailedError;
33 
34 import java.io.FileNotFoundException;
35 import java.io.InputStream;
36 import java.io.OutputStream;
37 import java.util.ArrayList;
38 
39 @LargeTest
40 public class ContactsContract_AllUriTest extends AndroidTestCase {
41     private static final String TAG = "AllUrlTest";
42 
43     // "-" : Query not supported.
44     // "!" : Can't query because it requires the cross-user permission.
45     // The following markers are planned, but not implemented and the definition below is not all
46     // correct yet.
47     // "d" : supports delete.
48     // "u" : supports update.
49     // "i" : supports insert.
50     // "r" : supports read.
51     // "w" : supports write.
52     // "s" : has x_times_contacted and x_last_time_contacted.
53     // "t" : has x_times_used and x_last_time_used.
54     private static final String[][] URIs = {
55             {"content://com.android.contacts/contacts", "sud"},
56             {"content://com.android.contacts/contacts/1", "sud"},
57             {"content://com.android.contacts/contacts/1/data", "t"},
58             {"content://com.android.contacts/contacts/1/entities", "t"},
59             {"content://com.android.contacts/contacts/1/suggestions"},
60             {"content://com.android.contacts/contacts/1/suggestions/XXX"},
61             {"content://com.android.contacts/contacts/1/photo", "r"},
62             {"content://com.android.contacts/contacts/1/display_photo", "-r"},
63             {"content://com.android.contacts/contacts_corp/1/photo", "-r"},
64             {"content://com.android.contacts/contacts_corp/1/display_photo", "-r"},
65 
66             {"content://com.android.contacts/contacts/filter", "s"},
67             {"content://com.android.contacts/contacts/filter/XXX", "s"},
68 
69             {"content://com.android.contacts/contacts/lookup/nlookup", "sud"},
70             {"content://com.android.contacts/contacts/lookup/nlookup/data", "t"},
71             {"content://com.android.contacts/contacts/lookup/nlookup/photo", "tr"},
72 
73             {"content://com.android.contacts/contacts/lookup/nlookup/1", "sud"},
74             {"content://com.android.contacts/contacts/lookup/nlookup/1/data"},
75             {"content://com.android.contacts/contacts/lookup/nlookup/1/photo", "r"},
76             {"content://com.android.contacts/contacts/lookup/nlookup/display_photo", "-r"},
77             {"content://com.android.contacts/contacts/lookup/nlookup/1/display_photo", "-r"},
78             {"content://com.android.contacts/contacts/lookup/nlookup/entities"},
79             {"content://com.android.contacts/contacts/lookup/nlookup/1/entities"},
80 
81             {"content://com.android.contacts/contacts/as_vcard/nlookup", "r"},
82             {"content://com.android.contacts/contacts/as_multi_vcard/XXX"},
83 
84             {"content://com.android.contacts/contacts/strequent/", "s"},
85             {"content://com.android.contacts/contacts/strequent/filter/XXX", "s"},
86 
87             {"content://com.android.contacts/contacts/group/XXX"},
88 
89             {"content://com.android.contacts/contacts/frequent", "s"},
90             {"content://com.android.contacts/contacts/delete_usage", "-d"},
91             {"content://com.android.contacts/contacts/filter_enterprise?directory=0", "s"},
92             {"content://com.android.contacts/contacts/filter_enterprise/XXX?directory=0", "s"},
93 
94             {"content://com.android.contacts/raw_contacts", "siud"},
95             {"content://com.android.contacts/raw_contacts/1", "sud"},
96             {"content://com.android.contacts/raw_contacts/1/data", "tu"},
97             {"content://com.android.contacts/raw_contacts/1/display_photo", "-rw"},
98             {"content://com.android.contacts/raw_contacts/1/entity"},
99 
100             {"content://com.android.contacts/raw_contact_entities"},
101             {"content://com.android.contacts/raw_contact_entities_corp", "!"},
102 
103             {"content://com.android.contacts/data", "tud"},
104             {"content://com.android.contacts/data/1", "tudr"},
105             {"content://com.android.contacts/data/phones", "t"},
106             {"content://com.android.contacts/data_enterprise/phones", "!"},
107             {"content://com.android.contacts/data/phones/1", "tud"},
108             {"content://com.android.contacts/data/phones/filter", "t"},
109             {"content://com.android.contacts/data/phones/filter/XXX", "t"},
110 
111             {"content://com.android.contacts/data/phones/filter_enterprise?directory=0", "t"},
112             {"content://com.android.contacts/data/phones/filter_enterprise/XXX?directory=0", "t"},
113 
114             {"content://com.android.contacts/data/emails", "t"},
115             {"content://com.android.contacts/data/emails/1", "tud"},
116             {"content://com.android.contacts/data/emails/lookup", "t"},
117             {"content://com.android.contacts/data/emails/lookup/XXX", "t"},
118             {"content://com.android.contacts/data/emails/filter", "t"},
119             {"content://com.android.contacts/data/emails/filter/XXX", "t"},
120             {"content://com.android.contacts/data/emails/filter_enterprise?directory=0", "t"},
121             {"content://com.android.contacts/data/emails/filter_enterprise/XXX?directory=0", "t"},
122             {"content://com.android.contacts/data/emails/lookup_enterprise", "t"},
123             {"content://com.android.contacts/data/emails/lookup_enterprise/XXX", "t"},
124             {"content://com.android.contacts/data/postals", "t"},
125             {"content://com.android.contacts/data/postals/1", "tud"},
126             {"content://com.android.contacts/data/usagefeedback/1,2,3", "-u"},
127             {"content://com.android.contacts/data/callables/", "t"},
128             {"content://com.android.contacts/data/callables/1", "tud"},
129             {"content://com.android.contacts/data/callables/filter", "t"},
130             {"content://com.android.contacts/data/callables/filter/XXX", "t"},
131             {"content://com.android.contacts/data/callables/filter_enterprise?directory=0", "t"},
132             {"content://com.android.contacts/data/callables/filter_enterprise/XXX?directory=0",
133                     "t"},
134             {"content://com.android.contacts/data/contactables/", "t"},
135             {"content://com.android.contacts/data/contactables/filter", "t"},
136             {"content://com.android.contacts/data/contactables/filter/XXX", "t"},
137 
138             {"content://com.android.contacts/groups", "iud"},
139             {"content://com.android.contacts/groups/1", "ud"},
140             {"content://com.android.contacts/groups_summary"},
141             {"content://com.android.contacts/syncstate", "iud"},
142             {"content://com.android.contacts/syncstate/1", "-ud"},
143             {"content://com.android.contacts/profile/syncstate", "iud"},
144             {"content://com.android.contacts/phone_lookup/XXX"},
145             {"content://com.android.contacts/phone_lookup_enterprise/XXX"},
146             {"content://com.android.contacts/aggregation_exceptions", "u"},
147             {"content://com.android.contacts/settings", "ud"},
148             {"content://com.android.contacts/status_updates", "ud"},
149             {"content://com.android.contacts/status_updates/1"},
150             {"content://com.android.contacts/search_suggest_query"},
151             {"content://com.android.contacts/search_suggest_query/XXX"},
152             {"content://com.android.contacts/search_suggest_shortcut/XXX"},
153             {"content://com.android.contacts/provider_status"},
154             {"content://com.android.contacts/directories", "u"},
155             {"content://com.android.contacts/directories/1"},
156             {"content://com.android.contacts/directories_enterprise"},
157             {"content://com.android.contacts/directories_enterprise/1"},
158             {"content://com.android.contacts/complete_name"},
159             {"content://com.android.contacts/profile", "su"},
160             {"content://com.android.contacts/profile/entities", "s"},
161             {"content://com.android.contacts/profile/data", "tud"},
162             {"content://com.android.contacts/profile/data/1", "td"},
163             {"content://com.android.contacts/profile/photo", "t"},
164             {"content://com.android.contacts/profile/display_photo", "-r"},
165             {"content://com.android.contacts/profile/as_vcard", "r"},
166             {"content://com.android.contacts/profile/raw_contacts", "siud"},
167 
168             // Note this should have supported update... Too late to add.
169             {"content://com.android.contacts/profile/raw_contacts/1", "sd"},
170             {"content://com.android.contacts/profile/raw_contacts/1/data", "tu"},
171             {"content://com.android.contacts/profile/raw_contacts/1/entity"},
172             {"content://com.android.contacts/profile/status_updates", "ud"},
173             {"content://com.android.contacts/profile/raw_contact_entities"},
174             {"content://com.android.contacts/display_photo/1", "-r"},
175             {"content://com.android.contacts/photo_dimensions"},
176             {"content://com.android.contacts/deleted_contacts"},
177             {"content://com.android.contacts/deleted_contacts/1"},
178             {"content://com.android.contacts/directory_file_enterprise/XXX?directory=0", "-"},
179     };
180 
181     private static final String[] ARG1 = {"-1"};
182 
183     private ContentResolver mResolver;
184 
185     private ArrayList<String> mFailures;
186 
187     @Override
setUp()188     protected void setUp() throws Exception {
189         super.setUp();
190 
191         mFailures = new ArrayList<>();
192         mResolver = getContext().getContentResolver();
193     }
194 
195     @Override
tearDown()196     protected void tearDown() throws Exception {
197         if (mFailures != null) {
198             fail("mFailures is not null.  Did you forget to call failIfFailed()?");
199         }
200 
201         super.tearDown();
202     }
203 
addFailure(String message, Throwable th)204     private void addFailure(String message, Throwable th) {
205         Log.e(TAG, "Failed: " + message, th);
206 
207         final int MAX = 100;
208         if (mFailures.size() == MAX) {
209             mFailures.add("Too many failures.");
210         } else if (mFailures.size() > MAX) {
211             // Too many failures already...
212         } else {
213             mFailures.add(message);
214         }
215     }
216 
failIfFailed()217     private void failIfFailed() {
218         if (mFailures == null) {
219             fail("mFailures is null.  Maybe called failIfFailed() twice?");
220         }
221         if (mFailures.size() > 0) {
222             StringBuilder message = new StringBuilder();
223 
224             if (mFailures.size() > 0) {
225                 Log.e(TAG, "Something went wrong:");
226                 for (String s : mFailures) {
227                     Log.e(TAG, s);
228                     if (message.length() > 0) {
229                         message.append("\n");
230                     }
231                     message.append(s);
232                 }
233             }
234             mFailures = null;
235             fail("Following test(s) failed:\n" + message);
236         }
237         mFailures = null;
238     }
239 
getUri(String[] path)240     private static Uri getUri(String[] path) {
241         return Uri.parse(path[0]);
242     }
243 
supportsQuery(String[] path)244     private static boolean supportsQuery(String[] path) {
245         if (path.length == 1) {
246             return true; // supports query by default.
247         }
248         return !(path[1].contains("-") || path[1].contains("!"));
249     }
250 
supportsInsert(String[] path)251     private static boolean supportsInsert(String[] path) {
252         return (path.length) >= 2 && path[1].contains("i");
253     }
254 
supportsUpdate(String[] path)255     private static boolean supportsUpdate(String[] path) {
256         return (path.length) >= 2 && path[1].contains("u");
257     }
258 
supportsDelete(String[] path)259     private static boolean supportsDelete(String[] path) {
260         return (path.length) >= 2 && path[1].contains("d");
261     }
262 
supportsRead(String[] path)263     private static boolean supportsRead(String[] path) {
264         return (path.length) >= 2 && path[1].contains("r");
265     }
266 
supportsWrite(String[] path)267     private static boolean supportsWrite(String[] path) {
268         return (path.length) >= 2 && path[1].contains("w");
269     }
270 
getColumns(Uri uri)271     private String[] getColumns(Uri uri) {
272         try (Cursor c = mResolver.query(uri,
273                 null, // projection
274                 "1=2", // selection
275                 null, // selection args
276                 null // sort order
277         )) {
278             return c.getColumnNames();
279         }
280     }
281 
checkQueryExecutable(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)282     private void checkQueryExecutable(Uri uri,
283             String[] projection, String selection,
284             String[] selectionArgs, String sortOrder) {
285         try {
286             try (Cursor c = mResolver.query(uri, projection, selection,
287                     selectionArgs, sortOrder)) {
288                 c.moveToFirst();
289             }
290         } catch (Throwable th) {
291             addFailure("Query failed: URI=" + uri + " Message=" + th.getMessage(), th);
292         }
293         try {
294             // With CancellationSignal.
295             try (Cursor c = mResolver.query(uri, projection, selection,
296                     selectionArgs, sortOrder, new CancellationSignal())) {
297                 c.moveToFirst();
298             }
299         } catch (Throwable th) {
300             addFailure("Query with cancel failed: URI=" + uri + " Message=" + th.getMessage(), th);
301         }
302         try {
303             // With limit.
304             try (Cursor c = mResolver.query(
305                     uri.buildUpon().appendQueryParameter(
306                             ContactsContract.LIMIT_PARAM_KEY, "0").build(),
307                     projection, selection, selectionArgs, sortOrder)) {
308                 c.moveToFirst();
309             }
310         } catch (Throwable th) {
311             addFailure("Query with limit failed: URI=" + uri + " Message=" + th.getMessage(), th);
312         }
313 
314         try {
315             // With account.
316             try (Cursor c = mResolver.query(
317                     uri.buildUpon()
318                             .appendQueryParameter(RawContacts.ACCOUNT_NAME, "a")
319                             .appendQueryParameter(RawContacts.ACCOUNT_TYPE, "b")
320                             .appendQueryParameter(RawContacts.DATA_SET, "c")
321                             .build(),
322                     projection, selection, selectionArgs, sortOrder)) {
323                 c.moveToFirst();
324             }
325         } catch (Throwable th) {
326             addFailure("Query with limit failed: URI=" + uri + " Message=" + th.getMessage(), th);
327         }
328     }
329 
checkQueryNotExecutable(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)330     private void checkQueryNotExecutable(Uri uri,
331             String[] projection, String selection,
332             String[] selectionArgs, String sortOrder) {
333         try {
334             try (Cursor c = mResolver.query(uri, projection, selection,
335                     selectionArgs, sortOrder)) {
336                 c.moveToFirst();
337             }
338         } catch (Throwable th) {
339             // pass.
340             return;
341         }
342         addFailure("Query on " + uri + " expected to fail, but succeeded.", null);
343     }
344 
345     /**
346      * Make sure all URLs are accessible with all arguments = null.
347      */
testSelect()348     public void testSelect() {
349         for (String[] path : URIs) {
350             if (!supportsQuery(path)) continue;
351             final Uri uri = getUri(path);
352 
353             checkQueryExecutable(uri, // uri
354                     null, // projection
355                     null, // selection
356                     null, // selection args
357                     null // sort order
358             );
359         }
360         failIfFailed();
361     }
362 
testNoHiddenColumns()363     public void testNoHiddenColumns() {
364         for (String[] path : URIs) {
365             if (!supportsQuery(path)) continue;
366             final Uri uri = getUri(path);
367 
368             for (String column : getColumns(uri)) {
369                 if (column.toLowerCase().startsWith(ContactsContract.HIDDEN_COLUMN_PREFIX)) {
370                     addFailure("Uri " + uri + " returned hidden column " + column, null);
371                 }
372             }
373         }
374         failIfFailed();
375     }
376 
377 // Temporarily disabled due to taking too much time.
378 //    /**
379 //     * Make sure all URLs are accessible with a projection.
380 //     */
381 //    public void testSelectWithProjection() {
382 //        for (String[] path : URIs) {
383 //            if (!supportsQuery(path)) continue;
384 //            final Uri uri = getUri(path);
385 //
386 //            for (String column : getColumns(uri)) {
387 //                // Some columns are not selectable alone due to bugs, and we don't want to fix them
388 //                // in order to avoid expanding the differences between versions, so here're some
389 //                // hacks to make it work...
390 //
391 //                String[] projection = {column};
392 //
393 //                final String u = path[0];
394 //                if ((u.startsWith("content://com.android.contacts/status_updates")
395 //                        || u.startsWith("content://com.android.contacts/profile/status_updates"))
396 //                        && ("im_handle".equals(column)
397 //                        || "im_account".equals(column)
398 //                        || "protocol".equals(column)
399 //                        || "custom_protocol".equals(column)
400 //                        || "presence_raw_contact_id".equals(column)
401 //                )) {
402 //                    // These columns only show up when the projection contains certain columns.
403 //
404 //                    projection = new String[]{"mode", column};
405 //                } else if ((u.startsWith("content://com.android.contacts/search_suggest_query")
406 //                        || u.startsWith("content://contacts/search_suggest_query"))
407 //                        && "suggest_intent_action".equals(column)) {
408 //                    // Can't be included in the projection due to a bug in GlobalSearchSupport.
409 //                    continue;
410 //                } else if (RawContacts.BACKUP_ID.equals(column)) {
411 //                    // Some URIs don't support a projection with BAKCUP_ID only.
412 //                    projection = new String[]{RawContacts.BACKUP_ID, RawContacts.SOURCE_ID};
413 //                }
414 //
415 //                checkQueryExecutable(uri,
416 //                        projection, // projection
417 //                        null, // selection
418 //                        null, // selection args
419 //                        null // sort order
420 //                );
421 //            }
422 //        }
423 //        failIfFailed();
424 //    }
425 
426     /**
427      * Make sure all URLs are accessible with a selection.
428      */
testSelectWithSelection()429     public void testSelectWithSelection() {
430         for (String[] path : URIs) {
431             if (!supportsQuery(path)) continue;
432             final Uri uri = getUri(path);
433 
434             checkQueryExecutable(uri,
435                     null, // projection
436                     "1=?", // selection
437                     ARG1, // , // selection args
438                     null // sort order
439             );
440         }
441         failIfFailed();
442     }
443 
444 //    /**
445 //     * Make sure all URLs are accessible with a selection.
446 //     */
447 //    public void testSelectWithSelectionUsingColumns() {
448 //        for (String[] path : URIs) {
449 //            if (!supportsQuery(path)) continue;
450 //            final Uri uri = getUri(path);
451 //
452 //            for (String column : getColumns(uri)) {
453 //                checkQueryExecutable(uri,
454 //                        null, // projection
455 //                        column + "=?", // selection
456 //                        ARG1, // , // selection args
457 //                        null // sort order
458 //                );
459 //            }
460 //        }
461 //        failIfFailed();
462 //    }
463 
464 // Temporarily disabled due to taking too much time.
465 //    /**
466 //     * Make sure all URLs are accessible with an order-by.
467 //     */
468 //    public void testSelectWithSortOrder() {
469 //        for (String[] path : URIs) {
470 //            if (!supportsQuery(path)) continue;
471 //            final Uri uri = getUri(path);
472 //
473 //            for (String column : getColumns(uri)) {
474 //                checkQueryExecutable(uri,
475 //                        null, // projection
476 //                        "1=2", // selection
477 //                        null, // , // selection args
478 //                        column // sort order
479 //                );
480 //            }
481 //        }
482 //        failIfFailed();
483 //    }
484 
485     /**
486      * Make sure all URLs are accessible with all arguments.
487      */
testSelectWithAllArgs()488     public void testSelectWithAllArgs() {
489         for (String[] path : URIs) {
490             if (!supportsQuery(path)) continue;
491             final Uri uri = getUri(path);
492 
493             final String[] projection = {getColumns(uri)[0]};
494 
495             checkQueryExecutable(uri,
496                     projection, // projection
497                     "1=?", // selection
498                     ARG1, // , // selection args
499                     getColumns(uri)[0] // sort order
500             );
501         }
502         failIfFailed();
503     }
504 
testNonSelect()505     public void testNonSelect() {
506         for (String[] path : URIs) {
507             if (supportsQuery(path)) continue;
508             final Uri uri = getUri(path);
509 
510             checkQueryNotExecutable(uri, // uri
511                     null, // projection
512                     null, // selection
513                     null, // selection args
514                     null // sort order
515             );
516         }
517         failIfFailed();
518     }
519 
supportsTimesContacted(String[] path)520     private static boolean supportsTimesContacted(String[] path) {
521         return path.length > 1 && path[1].contains("s");
522     }
523 
supportsTimesUsed(String[] path)524     private static boolean supportsTimesUsed(String[] path) {
525         return path.length > 1 && path[1].contains("t");
526     }
527 
checkColumnAccessible(Uri uri, String column)528     private void checkColumnAccessible(Uri uri, String column) {
529         try {
530             try (Cursor c = mResolver.query(
531                     uri, new String[]{column}, column + "=0", null, column
532             )) {
533                 c.moveToFirst();
534             }
535         } catch (Throwable th) {
536             addFailure("Query failed: URI=" + uri + " Message=" + th.getMessage(), th);
537         }
538     }
539 
540     /** Test for {@link #checkColumnAccessible} */
testCheckColumnAccessible()541     public void testCheckColumnAccessible() {
542         checkColumnAccessible(Contacts.CONTENT_URI, "x_times_contacted");
543         try {
544             failIfFailed();
545         } catch (AssertionFailedError expected) {
546             return; // expected.
547         }
548         fail("Failed to detect issue.");
549     }
550 
checkColumnNotAccessibleInner(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)551     private void checkColumnNotAccessibleInner(Uri uri, String[] projection, String selection,
552             String[] selectionArgs, String sortOrder) {
553         try {
554             try (Cursor c = mResolver.query(uri, projection, selection,
555                     selectionArgs, sortOrder)) {
556                 c.moveToFirst();
557             }
558         } catch (IllegalArgumentException th) {
559             // pass.
560             return;
561         }
562         addFailure("Query on " + uri +
563                 " expected to throw IllegalArgumentException, but succeeded.", null);
564     }
565 
checkColumnNotAccessible(Uri uri, String column)566     private void checkColumnNotAccessible(Uri uri, String column) {
567         checkColumnNotAccessibleInner(uri, new String[] {column}, null, null, null);
568         checkColumnNotAccessibleInner(uri, null, column + "=1", null, null);
569         checkColumnNotAccessibleInner(uri, null, null, null, /* order by */ column);
570     }
571 
572     /** Test for {@link #checkColumnNotAccessible} */
testCheckColumnNotAccessible()573     public void testCheckColumnNotAccessible() {
574         checkColumnNotAccessible(Contacts.CONTENT_URI, "times_contacted");
575         try {
576             failIfFailed();
577         } catch (AssertionFailedError expected) {
578             return; // expected.
579         }
580         fail("Failed to detect issue.");
581     }
582 
583     /**
584      * Make sure the x_ columns are not accessible.
585      */
testProhibitedColumns()586     public void testProhibitedColumns() {
587         for (String[] path : URIs) {
588             final Uri uri = getUri(path);
589             if (supportsTimesContacted(path)) {
590                 checkColumnAccessible(uri, "times_contacted");
591                 checkColumnAccessible(uri, "last_time_contacted");
592 
593                 checkColumnNotAccessible(uri, "X_times_contacted");
594                 checkColumnNotAccessible(uri, "X_slast_time_contacted");
595             }
596             if (supportsTimesUsed(path)) {
597                 checkColumnAccessible(uri, "times_used");
598                 checkColumnAccessible(uri, "last_time_used");
599 
600                 checkColumnNotAccessible(uri, "X_times_used");
601                 checkColumnNotAccessible(uri, "X_last_time_used");
602             }
603         }
604         failIfFailed();
605     }
606 
checkExecutable(String operation, Uri uri, boolean shouldWork, Runnable r)607     private void checkExecutable(String operation, Uri uri, boolean shouldWork, Runnable r) {
608         if (shouldWork) {
609             try {
610                 r.run();
611             } catch (Exception e) {
612                 addFailure(operation + " for '" + uri + "' failed: " + e.getMessage(), e);
613             }
614         } else {
615             try {
616                 r.run();
617                 addFailure(operation + " for '" + uri + "' NOT failed.", null);
618             } catch (Exception expected) {
619             }
620         }
621     }
622 
testAllOperations()623     public void testAllOperations() {
624         final ContentValues cv = new ContentValues();
625 
626         for (String[] path : URIs) {
627             final Uri uri = getUri(path);
628 
629             cv.clear();
630             if (supportsQuery(path)) {
631                 cv.put(getColumns(uri)[0], 1);
632             } else {
633                 cv.put("_id", 1);
634             }
635             if (uri.toString().contains("syncstate")) {
636                 cv.put(SyncState.ACCOUNT_NAME, "abc");
637                 cv.put(SyncState.ACCOUNT_TYPE, "def");
638             }
639 
640             checkExecutable("insert", uri, supportsInsert(path), () -> {
641                 final Uri newUri = mResolver.insert(uri, cv);
642                 if (newUri == null) {
643                     addFailure("Insert for '" + uri + "' returned null.", null);
644                 } else {
645                     // "profile/raw_contacts/#" is missing update support.  too late to add, so
646                     // just skip.
647                     if (!newUri.toString().startsWith(
648                             "content://com.android.contacts/profile/raw_contacts/")) {
649                         checkExecutable("insert -> update", newUri, true, () -> {
650                             mResolver.update(newUri, cv, null, null);
651                         });
652                     }
653                     checkExecutable("insert -> delete", newUri, true, () -> {
654                         mResolver.delete(newUri, null, null);
655                     });
656                 }
657             });
658             checkExecutable("update", uri, supportsUpdate(path), () -> {
659                 mResolver.update(uri, cv, "1=2", null);
660             });
661             checkExecutable("delete", uri, supportsDelete(path), () -> {
662                 mResolver.delete(uri, "1=2", null);
663             });
664         }
665         failIfFailed();
666     }
667 
testAllFileOperations()668     public void testAllFileOperations() {
669         for (String[] path : URIs) {
670             final Uri uri = getUri(path);
671 
672             checkExecutable("openInputStream", uri, supportsRead(path), () -> {
673                 try (InputStream st = mResolver.openInputStream(uri)) {
674                 } catch (FileNotFoundException e) {
675                     // TODO This happens because we try to read nonexistent photos.  Ideally
676                     // we should actually check it's readable.
677                     if (e.getMessage().contains("Stream I/O not supported")) {
678                         throw new RuntimeException("Caught Exception: " + e.toString(), e);
679                     }
680                 } catch (Exception e) {
681                     throw new RuntimeException("Caught Exception: " + e.toString(), e);
682                 }
683             });
684             checkExecutable("openOutputStream", uri, supportsWrite(path), () -> {
685                 try (OutputStream st = mResolver.openOutputStream(uri)) {
686                 } catch (Exception e) {
687                     throw new RuntimeException("Caught Exception: " + e.toString(), e);
688                 }
689             });
690         }
691         failIfFailed();
692     }
693 }
694 
695 
696