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 try { 375 final Uri uri = getUri(path); 376 377 checkQueryExecutable(uri, // uri 378 null, // projection 379 null, // selection 380 null, // selection args 381 null // sort order 382 ); 383 } catch (Throwable th) { 384 addFailure("Failed: URI=" + path[0] + " Message=" + th.getMessage(), th); 385 } 386 } 387 failIfFailed(); 388 } 389 testNoHiddenColumns()390 public void testNoHiddenColumns() { 391 for (String[] path : URIs) { 392 if (!supportsQuery(path)) continue; 393 try { 394 final Uri uri = getUri(path); 395 396 for (String column : getColumns(uri)) { 397 if (column.toLowerCase().startsWith(ContactsContract.HIDDEN_COLUMN_PREFIX)) { 398 addFailure("Uri " + uri + " returned hidden column " + column, null); 399 } 400 } 401 } catch (Throwable th) { 402 addFailure("Failed: URI=" + path[0] + " Message=" + th.getMessage(), th); 403 } 404 } 405 failIfFailed(); 406 } 407 408 // Temporarily disabled due to taking too much time. 409 // /** 410 // * Make sure all URLs are accessible with a projection. 411 // */ 412 // public void testSelectWithProjection() { 413 // for (String[] path : URIs) { 414 // if (!supportsQuery(path)) continue; 415 // final Uri uri = getUri(path); 416 // 417 // for (String column : getColumns(uri)) { 418 // // Some columns are not selectable alone due to bugs, and we don't want to fix them 419 // // in order to avoid expanding the differences between versions, so here're some 420 // // hacks to make it work... 421 // 422 // String[] projection = {column}; 423 // 424 // final String u = path[0]; 425 // if ((u.startsWith("content://com.android.contacts/status_updates") 426 // || u.startsWith("content://com.android.contacts/profile/status_updates")) 427 // && ("im_handle".equals(column) 428 // || "im_account".equals(column) 429 // || "protocol".equals(column) 430 // || "custom_protocol".equals(column) 431 // || "presence_raw_contact_id".equals(column) 432 // )) { 433 // // These columns only show up when the projection contains certain columns. 434 // 435 // projection = new String[]{"mode", column}; 436 // } else if ((u.startsWith("content://com.android.contacts/search_suggest_query") 437 // || u.startsWith("content://contacts/search_suggest_query")) 438 // && "suggest_intent_action".equals(column)) { 439 // // Can't be included in the projection due to a bug in GlobalSearchSupport. 440 // continue; 441 // } else if (RawContacts.BACKUP_ID.equals(column)) { 442 // // Some URIs don't support a projection with BAKCUP_ID only. 443 // projection = new String[]{RawContacts.BACKUP_ID, RawContacts.SOURCE_ID}; 444 // } 445 // 446 // checkQueryExecutable(uri, 447 // projection, // projection 448 // null, // selection 449 // null, // selection args 450 // null // sort order 451 // ); 452 // } 453 // } 454 // failIfFailed(); 455 // } 456 457 /** 458 * Make sure all URLs are accessible with a selection. 459 */ testSelectWithSelection()460 public void testSelectWithSelection() { 461 for (String[] path : URIs) { 462 if (!supportsQuery(path)) continue; 463 final Uri uri = getUri(path); 464 465 checkQueryExecutable(uri, 466 null, // projection 467 "1=?", // selection 468 ARG1, // , // selection args 469 null // sort order 470 ); 471 } 472 failIfFailed(); 473 } 474 475 // /** 476 // * Make sure all URLs are accessible with a selection. 477 // */ 478 // public void testSelectWithSelectionUsingColumns() { 479 // for (String[] path : URIs) { 480 // if (!supportsQuery(path)) continue; 481 // final Uri uri = getUri(path); 482 // 483 // for (String column : getColumns(uri)) { 484 // checkQueryExecutable(uri, 485 // null, // projection 486 // column + "=?", // selection 487 // ARG1, // , // selection args 488 // null // sort order 489 // ); 490 // } 491 // } 492 // failIfFailed(); 493 // } 494 495 // Temporarily disabled due to taking too much time. 496 // /** 497 // * Make sure all URLs are accessible with an order-by. 498 // */ 499 // public void testSelectWithSortOrder() { 500 // for (String[] path : URIs) { 501 // if (!supportsQuery(path)) continue; 502 // final Uri uri = getUri(path); 503 // 504 // for (String column : getColumns(uri)) { 505 // checkQueryExecutable(uri, 506 // null, // projection 507 // "1=2", // selection 508 // null, // , // selection args 509 // column // sort order 510 // ); 511 // } 512 // } 513 // failIfFailed(); 514 // } 515 516 /** 517 * Make sure all URLs are accessible with all arguments. 518 */ testSelectWithAllArgs()519 public void testSelectWithAllArgs() { 520 for (String[] path : URIs) { 521 if (!supportsQuery(path)) continue; 522 final Uri uri = getUri(path); 523 524 final String[] projection = {getColumns(uri)[0]}; 525 526 checkQueryExecutable(uri, 527 projection, // projection 528 "1=?", // selection 529 ARG1, // , // selection args 530 getColumns(uri)[0] // sort order 531 ); 532 } 533 failIfFailed(); 534 } 535 testNonSelect()536 public void testNonSelect() { 537 for (String[] path : URIs) { 538 if (supportsQuery(path)) continue; 539 final Uri uri = getUri(path); 540 541 checkQueryNotExecutable(uri, // uri 542 null, // projection 543 null, // selection 544 null, // selection args 545 null // sort order 546 ); 547 } 548 failIfFailed(); 549 } 550 supportsTimesContacted(String[] path)551 private static boolean supportsTimesContacted(String[] path) { 552 return path.length > 1 && path[1].contains("s"); 553 } 554 supportsTimesUsed(String[] path)555 private static boolean supportsTimesUsed(String[] path) { 556 return path.length > 1 && path[1].contains("t"); 557 } 558 checkColumnAccessible(Uri uri, String column)559 private void checkColumnAccessible(Uri uri, String column) { 560 try { 561 try (Cursor c = mResolver.query( 562 uri, new String[]{column}, column + "=0", null, column 563 )) { 564 c.moveToFirst(); 565 } 566 } catch (Throwable th) { 567 addFailure("Query failed: URI=" + uri + " Message=" + th.getMessage(), th); 568 } 569 } 570 571 /** Test for {@link #checkColumnAccessible} */ testCheckColumnAccessible()572 public void testCheckColumnAccessible() { 573 checkColumnAccessible(Contacts.CONTENT_URI, "x_times_contacted"); 574 try { 575 failIfFailed(); 576 } catch (AssertionFailedError expected) { 577 return; // expected. 578 } 579 fail("Failed to detect issue."); 580 } 581 checkColumnNotAccessibleInner(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)582 private void checkColumnNotAccessibleInner(Uri uri, String[] projection, String selection, 583 String[] selectionArgs, String sortOrder) { 584 try { 585 try (Cursor c = mResolver.query(uri, projection, selection, 586 selectionArgs, sortOrder)) { 587 c.moveToFirst(); 588 } 589 } catch (IllegalArgumentException th) { 590 // pass. 591 return; 592 } 593 addFailure("Query on " + uri + 594 " expected to throw IllegalArgumentException, but succeeded.", null); 595 } 596 checkColumnNotAccessible(Uri uri, String column)597 private void checkColumnNotAccessible(Uri uri, String column) { 598 checkColumnNotAccessibleInner(uri, new String[] {column}, null, null, null); 599 checkColumnNotAccessibleInner(uri, null, column + "=1", null, null); 600 checkColumnNotAccessibleInner(uri, null, null, null, /* order by */ column); 601 } 602 603 /** Test for {@link #checkColumnNotAccessible} */ testCheckColumnNotAccessible()604 public void testCheckColumnNotAccessible() { 605 checkColumnNotAccessible(Contacts.CONTENT_URI, "times_contacted"); 606 try { 607 failIfFailed(); 608 } catch (AssertionFailedError expected) { 609 return; // expected. 610 } 611 fail("Failed to detect issue."); 612 } 613 614 /** 615 * Make sure the x_ columns are not accessible. 616 */ testProhibitedColumns()617 public void testProhibitedColumns() { 618 for (String[] path : URIs) { 619 final Uri uri = getUri(path); 620 if (supportsTimesContacted(path)) { 621 checkColumnAccessible(uri, "times_contacted"); 622 checkColumnAccessible(uri, "last_time_contacted"); 623 624 checkColumnNotAccessible(uri, "X_times_contacted"); 625 checkColumnNotAccessible(uri, "X_slast_time_contacted"); 626 } 627 if (supportsTimesUsed(path)) { 628 checkColumnAccessible(uri, "times_used"); 629 checkColumnAccessible(uri, "last_time_used"); 630 631 checkColumnNotAccessible(uri, "X_times_used"); 632 checkColumnNotAccessible(uri, "X_last_time_used"); 633 } 634 } 635 failIfFailed(); 636 } 637 checkExecutable(String operation, Uri uri, boolean shouldWork, Runnable r)638 private void checkExecutable(String operation, Uri uri, boolean shouldWork, Runnable r) { 639 if (shouldWork) { 640 try { 641 r.run(); 642 } catch (Exception e) { 643 addFailure(operation + " for '" + uri + "' failed: " + e.getMessage(), e); 644 } 645 } else { 646 try { 647 r.run(); 648 addFailure(operation + " for '" + uri + "' NOT failed.", null); 649 } catch (Exception expected) { 650 } 651 } 652 } 653 testAllOperations()654 public void testAllOperations() { 655 final ContentValues cv = new ContentValues(); 656 657 for (String[] path : URIs) { 658 final Uri uri = getUri(path); 659 660 try { 661 cv.clear(); 662 if (supportsQuery(path)) { 663 cv.put(getColumns(uri)[0], 1); 664 } else { 665 cv.put("_id", 1); 666 } 667 if (uri.toString().contains("syncstate")) { 668 cv.put(SyncState.ACCOUNT_NAME, "abc"); 669 cv.put(SyncState.ACCOUNT_TYPE, "def"); 670 } 671 672 checkExecutable("insert", uri, supportsInsert(path), () -> { 673 final Uri newUri = mResolver.insert(uri, cv); 674 if (newUri == null) { 675 addFailure("Insert for '" + uri + "' returned null.", null); 676 } else { 677 // "profile/raw_contacts/#" is missing update support. too late to add, so 678 // just skip. 679 if (!newUri.toString().startsWith( 680 "content://com.android.contacts/profile/raw_contacts/")) { 681 checkExecutable("insert -> update", newUri, true, () -> { 682 mResolver.update(newUri, cv, null, null); 683 }); 684 } 685 checkExecutable("insert -> delete", newUri, true, () -> { 686 mResolver.delete(newUri, null, null); 687 }); 688 } 689 }); 690 checkExecutable("update", uri, supportsUpdate(path), () -> { 691 mResolver.update(uri, cv, "1=2", null); 692 }); 693 checkExecutable("delete", uri, supportsDelete(path), () -> { 694 mResolver.delete(uri, "1=2", null); 695 }); 696 } catch (Throwable th) { 697 addFailure("Failed: URI=" + uri + " Message=" + th.getMessage(), th); 698 } 699 } 700 failIfFailed(); 701 } 702 testAllFileOperations()703 public void testAllFileOperations() { 704 for (String[] path : URIs) { 705 final Uri uri = getUri(path); 706 707 checkExecutable("openInputStream", uri, supportsRead(path), () -> { 708 try (InputStream st = mResolver.openInputStream(uri)) { 709 } catch (FileNotFoundException e) { 710 // TODO This happens because we try to read nonexistent photos. Ideally 711 // we should actually check it's readable. 712 if (e.getMessage().contains("Stream I/O not supported")) { 713 throw new RuntimeException("Caught Exception: " + e.toString(), e); 714 } 715 } catch (Exception e) { 716 throw new RuntimeException("Caught Exception: " + e.toString(), e); 717 } 718 }); 719 checkExecutable("openOutputStream", uri, supportsWrite(path), () -> { 720 try (OutputStream st = mResolver.openOutputStream(uri)) { 721 } catch (Exception e) { 722 throw new RuntimeException("Caught Exception: " + e.toString(), e); 723 } 724 }); 725 } 726 failIfFailed(); 727 } 728 } 729 730 731