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