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