1 /* 2 * Copyright (C) 2009 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 com.android.providers.contacts; 18 19 import android.content.ContentValues; 20 import android.net.Uri; 21 import android.net.Uri.Builder; 22 import android.provider.ContactsContract.CommonDataKinds.Im; 23 import android.provider.ContactsContract.CommonDataKinds.Organization; 24 import android.provider.ContactsContract.CommonDataKinds.StructuredName; 25 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; 26 import android.provider.ContactsContract.Contacts; 27 import android.provider.ContactsContract.SearchSnippetColumns; 28 import android.test.suitebuilder.annotation.MediumTest; 29 import android.test.suitebuilder.annotation.Suppress; 30 31 import java.text.Collator; 32 import java.util.Arrays; 33 import java.util.Locale; 34 35 /** 36 * Unit tests for {@link SearchIndexManager}. 37 * 38 * Run the test like this: 39 * <code> 40 * adb shell am instrument -e class com.android.providers.contacts.SearchIndexManagerTest -w \ 41 * com.android.providers.contacts.tests/android.test.InstrumentationTestRunner 42 * </code> 43 */ 44 @MediumTest 45 public class SearchIndexManagerTest extends BaseContactsProvider2Test { 46 47 public void testSearchIndexForStructuredName() { 48 long rawContactId = createRawContact(); 49 long contactId = queryContactId(rawContactId); 50 insertStructuredName(rawContactId, "John", "Doe"); 51 ContentValues values = new ContentValues(); 52 values.put(StructuredName.DISPLAY_NAME, "Bob I. Parr"); 53 insertStructuredName(rawContactId, values); 54 values.clear(); 55 values.put(StructuredName.PREFIX, "Mrs."); 56 values.put(StructuredName.GIVEN_NAME, "Helen"); 57 values.put(StructuredName.MIDDLE_NAME, "I."); 58 values.put(StructuredName.FAMILY_NAME, "Parr"); 59 values.put(StructuredName.SUFFIX, "PhD"); 60 values.put(StructuredName.PHONETIC_FAMILY_NAME, "par"); 61 values.put(StructuredName.PHONETIC_GIVEN_NAME, "helen"); 62 insertStructuredName(rawContactId, values); 63 64 assertSearchIndex( 65 contactId, null, "John Doe Bob I Parr Helen I Parr PhD par helen parhelen", null); 66 } 67 68 public void testSearchIndexForChineseName() { 69 // Only run this test when Chinese collation is supported 70 if (!Arrays.asList(Collator.getAvailableLocales()).contains(Locale.CHINA)) { 71 return; 72 } 73 74 long rawContactId = createRawContact(); 75 long contactId = queryContactId(rawContactId); 76 ContentValues values = new ContentValues(); 77 values.put(StructuredName.DISPLAY_NAME, "\u695A\u8FAD"); // CHUCI 78 insertStructuredName(rawContactId, values); 79 80 assertSearchIndex( 81 contactId, null, "\u695A\u8FAD \u695A\u8FAD CI \u8FAD CHUCI CC C", null); 82 } 83 84 public void testSearchByChineseName() { 85 // Only run this test when Chinese collation is supported 86 if (!Arrays.asList(Collator.getAvailableLocales()).contains(Locale.CHINA)) { 87 return; 88 } 89 90 long rawContactId = createRawContact(); 91 ContentValues values = new ContentValues(); 92 values.put(StructuredName.DISPLAY_NAME, "\u695A\u8FAD"); // CHUCI 93 insertStructuredName(rawContactId, values); 94 95 assertStoredValue(buildSearchUri("\u695A\u8FAD"), SearchSnippetColumns.SNIPPET, null); 96 assertStoredValue(buildSearchUri("\u8FAD"), SearchSnippetColumns.SNIPPET, null); 97 assertStoredValue(buildSearchUri("CI"), SearchSnippetColumns.SNIPPET, null); 98 assertStoredValue(buildSearchUri("CHUCI"), SearchSnippetColumns.SNIPPET, null); 99 assertStoredValue(buildSearchUri("CC"), SearchSnippetColumns.SNIPPET, null); 100 assertStoredValue(buildSearchUri("C"), SearchSnippetColumns.SNIPPET, null); 101 } 102 103 public void testSearchIndexForKoreanName() { 104 // Only run this test when Korean collation is supported 105 if (!Arrays.asList(Collator.getAvailableLocales()).contains(Locale.KOREA)) { 106 return; 107 } 108 109 long rawContactId = createRawContact(); 110 long contactId = queryContactId(rawContactId); 111 ContentValues values = new ContentValues(); 112 values.put(StructuredName.DISPLAY_NAME, "\uC774\uC0C1\uC77C"); // Lee Sang Il 113 insertStructuredName(rawContactId, values); 114 115 assertSearchIndex(contactId, null, 116 "\uC774\uC0C1\uC77C \uC0C1\uC77C \u1109\u110B \u110B\u1109\u110B", null); 117 } 118 119 public void testSearchByKoreanName() { 120 // Only run this test when Korean collation is supported 121 if (!Arrays.asList(Collator.getAvailableLocales()).contains(Locale.KOREA)) { 122 return; 123 } 124 125 long rawContactId = createRawContact(); 126 ContentValues values = new ContentValues(); 127 values.put(StructuredName.DISPLAY_NAME, "\uC774\uC0C1\uC77C"); // Lee Sang Il 128 insertStructuredName(rawContactId, values); 129 130 // Full name: Lee Sang Il 131 assertStoredValue(buildSearchUri("\uC774\uC0C1\uC77C"), SearchSnippetColumns.SNIPPET, null); 132 133 // Given name: Sang Il 134 assertStoredValue(buildSearchUri("\uC0C1\uC77C"), SearchSnippetColumns.SNIPPET, null); 135 136 // Consonants of given name: SIOS IEUNG 137 assertStoredValue(buildSearchUri("\u1109\u110B"), SearchSnippetColumns.SNIPPET, null); 138 139 // Consonants of full name: RIEUL SIOS IEUNG 140 assertStoredValue(buildSearchUri("\u110B\u1109\u110B"), SearchSnippetColumns.SNIPPET, null); 141 } 142 143 public void testSearchByKoreanNameWithTwoCharactersFamilyName() { 144 // Only run this test when Korean collation is supported. 145 if (!Arrays.asList(Collator.getAvailableLocales()).contains(Locale.KOREA)) { 146 return; 147 } 148 149 long rawContactId = createRawContact(); 150 151 // Sun Woo Young Nyeu 152 ContentValues values = new ContentValues(); 153 values.put(StructuredName.DISPLAY_NAME, "\uC120\uC6B0\uC6A9\uB140"); 154 155 insertStructuredName(rawContactId, values); 156 157 // Full name: Sun Woo Young Nyeu 158 assertStoredValue( 159 buildSearchUri("\uC120\uC6B0\uC6A9\uB140"), SearchSnippetColumns.SNIPPET, null); 160 161 // Given name: Young Nyeu 162 assertStoredValue(buildSearchUri("\uC6A9\uB140"), SearchSnippetColumns.SNIPPET, null); 163 164 // Consonants of given name: IEUNG NIEUN 165 assertStoredValue(buildSearchUri("\u110B\u1102"), SearchSnippetColumns.SNIPPET, null); 166 167 // Consonants of full name: SIOS IEUNG IEUNG NIEUN 168 assertStoredValue( 169 buildSearchUri("\u1109\u110B\u110B\u1102"), SearchSnippetColumns.SNIPPET, null); 170 } 171 172 public void testSearchIndexForOrganization() { 173 long rawContactId = createRawContact(); 174 long contactId = queryContactId(rawContactId); 175 ContentValues values = new ContentValues(); 176 values.put(Organization.COMPANY, "Acme Inc."); 177 values.put(Organization.TITLE, "Director"); 178 values.put(Organization.DEPARTMENT, "Phones and tablets"); 179 values.put(Organization.JOB_DESCRIPTION, "full text search"); 180 values.put(Organization.SYMBOL, "ACME"); 181 values.put(Organization.PHONETIC_NAME, "ack-me"); 182 values.put(Organization.OFFICE_LOCATION, "virtual"); 183 insertOrganization(rawContactId, values); 184 185 assertSearchIndex(contactId, 186 "Director, Acme Inc. (ack-me) (ACME)/Phones and tablets/virtual/full text search", 187 null, null); 188 } 189 190 public void testSearchIndexForPhoneNumber() { 191 long rawContactId = createRawContact(); 192 long contactId = queryContactId(rawContactId); 193 insertPhoneNumber(rawContactId, "800555GOOG"); 194 insertPhoneNumber(rawContactId, "8005551234"); 195 196 assertSearchIndex(contactId, null, null, "8005554664 +18005554664 8005551234 +18005551234"); 197 } 198 199 public void testSearchIndexForEmail() { 200 long rawContactId = createRawContact(); 201 long contactId = queryContactId(rawContactId); 202 insertEmail(rawContactId, "Bob Parr <incredible (at) android.com>"); 203 insertEmail(rawContactId, "bob_parr (at) android.com"); 204 205 assertSearchIndex(contactId, "Bob Parr <incredible (at) android.com>\nbob_parr (at) android.com", 206 null, null); 207 } 208 209 public void testSearchIndexForNickname() { 210 long rawContactId = createRawContact(); 211 long contactId = queryContactId(rawContactId); 212 insertNickname(rawContactId, "incredible"); 213 214 assertSearchIndex(contactId, "incredible", null, null); 215 } 216 217 public void testSearchIndexForStructuredPostal() { 218 long rawContactId = createRawContact(); 219 long contactId = queryContactId(rawContactId); 220 insertPostalAddress(rawContactId, "1600 Amphitheatre Pkwy\nMountain View, CA 94043"); 221 ContentValues values = new ContentValues(); 222 values.put(StructuredPostal.CITY, "London"); 223 values.put(StructuredPostal.STREET, "76 Buckingham Palace Road"); 224 values.put(StructuredPostal.POSTCODE, "SW1W 9TQ"); 225 values.put(StructuredPostal.COUNTRY, "United Kingdom"); 226 insertPostalAddress(rawContactId, values); 227 228 assertSearchIndex(contactId, "1600 Amphitheatre Pkwy Mountain View, CA 94043\n" 229 + "76 Buckingham Palace Road London SW1W 9TQ United Kingdom", null, null); 230 } 231 232 public void testSearchIndexForIm() { 233 long rawContactId = createRawContact(); 234 long contactId = queryContactId(rawContactId); 235 insertImHandle(rawContactId, Im.PROTOCOL_JABBER, null, "bp (at) android.com"); 236 insertImHandle(rawContactId, Im.PROTOCOL_CUSTOM, "android_im", "android (at) android.com"); 237 238 assertSearchIndex( 239 contactId, "Jabber/bp@android.com\nandroid_im/android@android.com", null, null); 240 } 241 242 public void testSearchIndexForNote() { 243 long rawContactId = createRawContact(); 244 long contactId = queryContactId(rawContactId); 245 insertNote(rawContactId, "Please note: three notes or more make up a chord."); 246 247 assertSearchIndex( 248 contactId, "Please note: three notes or more make up a chord.", null, null); 249 } 250 251 public void testSnippetArgs() { 252 long rawContactId = createRawContact(); 253 insertNote(rawContactId, "Please note: three notes or more make up a chord."); 254 255 assertStoredValue( 256 buildSearchUri("thr", "[,],-,2", false), SearchSnippetColumns.SNIPPET, 257 "-note: [three]-"); 258 } 259 260 public void testEmptyFilter() { 261 createRawContactWithName("John", "Doe"); 262 assertEquals(0, getCount(buildSearchUri(""), null, null)); 263 } 264 265 public void testSearchByName() { 266 createRawContactWithName("John Jay", "Doe"); 267 268 // We are supposed to find the contact, but return a null snippet 269 assertStoredValue(buildSearchUri("john"), SearchSnippetColumns.SNIPPET, null); 270 assertStoredValue(buildSearchUri("jay"), SearchSnippetColumns.SNIPPET, null); 271 assertStoredValue(buildSearchUri("doe"), SearchSnippetColumns.SNIPPET, null); 272 } 273 274 public void testSearchByPrefixName() { 275 createRawContactWithName("John Jay", "Doe"); 276 277 // prefix searches 278 assertStoredValue(buildSearchUri("jo ja"), SearchSnippetColumns.SNIPPET, null); 279 assertStoredValue(buildSearchUri("J D"), SearchSnippetColumns.SNIPPET, null); 280 assertStoredValue(buildSearchUri("Doe, John"), SearchSnippetColumns.SNIPPET, null); 281 } 282 283 public void testGermanUmlautFullameCapitalizationSearch() { 284 createRawContactWithName("Matthus BJRN Bnyamin", "Reier"); 285 286 // make sure we can find those, independent of the capitalization 287 assertStoredValue(buildSearchUri("matthus"), SearchSnippetColumns.SNIPPET, null); 288 assertStoredValue(buildSearchUri("Matthus"), SearchSnippetColumns.SNIPPET, null); 289 assertStoredValue(buildSearchUri("MATTHUS"), SearchSnippetColumns.SNIPPET, null); 290 291 assertStoredValue(buildSearchUri("bjrn"), SearchSnippetColumns.SNIPPET, null); 292 assertStoredValue(buildSearchUri("Bjrn"), SearchSnippetColumns.SNIPPET, null); 293 assertStoredValue(buildSearchUri("BJRN"), SearchSnippetColumns.SNIPPET, null); 294 295 assertStoredValue(buildSearchUri("bnyamin"), SearchSnippetColumns.SNIPPET, null); 296 assertStoredValue(buildSearchUri("Bnyamin"), SearchSnippetColumns.SNIPPET, null); 297 assertStoredValue(buildSearchUri("BUNYAMIN"), SearchSnippetColumns.SNIPPET, null); 298 299 // There is no capital version of . It is capitalized as double-S instead 300 assertStoredValue(buildSearchUri("Reier"), SearchSnippetColumns.SNIPPET, null); 301 assertStoredValue(buildSearchUri("Reisser"), SearchSnippetColumns.SNIPPET, null); 302 assertStoredValue(buildSearchUri("REISSER"), SearchSnippetColumns.SNIPPET, null); 303 } 304 305 public void testHangulNameLeadConsonantAsYouTypeSearch() { 306 createRawContactWithDisplayName(""); 307 // the korean name uses three compound characters. this test makes sure 308 // that the name can be found by typing in only the lead consonant 309 assertStoredValue(buildSearchUri(""), SearchSnippetColumns.SNIPPET, null); 310 assertStoredValue(buildSearchUri(""), SearchSnippetColumns.SNIPPET, null); 311 assertStoredValue(buildSearchUri(""), SearchSnippetColumns.SNIPPET, null); 312 313 // same again, this time only for the first name 314 assertStoredValue(buildSearchUri(""), SearchSnippetColumns.SNIPPET, null); 315 assertStoredValue(buildSearchUri(""), SearchSnippetColumns.SNIPPET, null); 316 } 317 318 public void testHangulNameFullAsYouTypeSearch() { 319 createRawContactWithDisplayName(""); 320 321 // the korean name uses three compound characters. this test makes sure 322 // that the name can be found by typing in the full nine letters. the search string 323 // shows the name is being built "as you type" 324 assertStoredValue(buildSearchUri(""), SearchSnippetColumns.SNIPPET, null); 325 assertStoredValue(buildSearchUri(""), SearchSnippetColumns.SNIPPET, null); 326 assertStoredValue(buildSearchUri(""), SearchSnippetColumns.SNIPPET, null); 327 assertStoredValue(buildSearchUri(""), SearchSnippetColumns.SNIPPET, null); 328 assertStoredValue(buildSearchUri(""), SearchSnippetColumns.SNIPPET, null); 329 assertStoredValue(buildSearchUri(""), SearchSnippetColumns.SNIPPET, null); 330 assertStoredValue(buildSearchUri(""), SearchSnippetColumns.SNIPPET, null); 331 assertStoredValue(buildSearchUri(""), SearchSnippetColumns.SNIPPET, null); 332 assertStoredValue(buildSearchUri(""), SearchSnippetColumns.SNIPPET, null); 333 334 // same again, this time only for the first name 335 assertStoredValue(buildSearchUri(""), SearchSnippetColumns.SNIPPET, null); 336 assertStoredValue(buildSearchUri(""), SearchSnippetColumns.SNIPPET, null); 337 assertStoredValue(buildSearchUri(""), SearchSnippetColumns.SNIPPET, null); 338 assertStoredValue(buildSearchUri(""), SearchSnippetColumns.SNIPPET, null); 339 assertStoredValue(buildSearchUri(""), SearchSnippetColumns.SNIPPET, null); 340 assertStoredValue(buildSearchUri(""), SearchSnippetColumns.SNIPPET, null); 341 } 342 343 344 /** Decomposed Hangul is not yet supported. This text is how we would test it */ 345 @Suppress 346 public void testHangulNameDecomposedSearch() { 347 createRawContactWithDisplayName(""); 348 349 // the korean name uses three compound characters. this test makes sure 350 // that the name can be found by typing each syllable as a single character. 351 // This can be achieved using the Korean IM by pressing , space, backspace, and so on 352 assertStoredValue(buildSearchUri(""), SearchSnippetColumns.SNIPPET, null); 353 assertStoredValue(buildSearchUri(""), SearchSnippetColumns.SNIPPET, null); 354 assertStoredValue(buildSearchUri(""), SearchSnippetColumns.SNIPPET, null); 355 assertStoredValue(buildSearchUri(""), SearchSnippetColumns.SNIPPET, null); 356 assertStoredValue(buildSearchUri(""), SearchSnippetColumns.SNIPPET, null); 357 assertStoredValue(buildSearchUri(""), SearchSnippetColumns.SNIPPET, null); 358 assertStoredValue(buildSearchUri(""), SearchSnippetColumns.SNIPPET, null); 359 assertStoredValue(buildSearchUri(""), SearchSnippetColumns.SNIPPET, null); 360 assertStoredValue(buildSearchUri(""), SearchSnippetColumns.SNIPPET, null); 361 362 // same again, this time only for the first name 363 assertStoredValue(buildSearchUri(""), SearchSnippetColumns.SNIPPET, null); 364 assertStoredValue(buildSearchUri(""), SearchSnippetColumns.SNIPPET, null); 365 assertStoredValue(buildSearchUri(""), SearchSnippetColumns.SNIPPET, null); 366 assertStoredValue(buildSearchUri(""), SearchSnippetColumns.SNIPPET, null); 367 assertStoredValue(buildSearchUri(""), SearchSnippetColumns.SNIPPET, null); 368 assertStoredValue(buildSearchUri(""), SearchSnippetColumns.SNIPPET, null); 369 } 370 371 public void testNameWithHyphen() { 372 createRawContactWithName("First", "Last-name"); 373 374 assertStoredValue(buildSearchUri("First"), SearchSnippetColumns.SNIPPET, null); 375 assertStoredValue(buildSearchUri("Last"), SearchSnippetColumns.SNIPPET, null); 376 assertStoredValue(buildSearchUri("Last-"), SearchSnippetColumns.SNIPPET, null); 377 assertStoredValue(buildSearchUri("Last-n"), SearchSnippetColumns.SNIPPET, null); 378 assertStoredValue(buildSearchUri("Last-name"), SearchSnippetColumns.SNIPPET, null); 379 380 // With the current implementation this even works, but this may stop working when we 381 // fix the "O'Neill" case below. 382 assertStoredValue(buildSearchUri("name"), SearchSnippetColumns.SNIPPET, null); 383 } 384 385 /** Same as {@link #testNameWithHyphen} except the name has double hyphens. */ 386 public void testNameWithDoubleHyphens() { 387 createRawContactWithName("First", "Last--name"); 388 389 assertStoredValue(buildSearchUri("First"), SearchSnippetColumns.SNIPPET, null); 390 assertStoredValue(buildSearchUri("Last"), SearchSnippetColumns.SNIPPET, null); 391 assertStoredValue(buildSearchUri("Last-"), SearchSnippetColumns.SNIPPET, null); 392 assertStoredValue(buildSearchUri("Last-n"), SearchSnippetColumns.SNIPPET, null); 393 assertStoredValue(buildSearchUri("Last-name"), SearchSnippetColumns.SNIPPET, null); 394 } 395 396 /** 397 * Probably both "oneill" and "o'neill" should match "o'neill", but at this point only "oneill" 398 * works. 399 */ 400 @Suppress 401 public void testNameWithPunctuations() { 402 createRawContactWithName("First", "O'Neill"); 403 404 assertStoredValue(buildSearchUri("first"), SearchSnippetColumns.SNIPPET, null); 405 assertStoredValue(buildSearchUri("oneill"), SearchSnippetColumns.SNIPPET, null); 406 assertStoredValue(buildSearchUri("o'neill"), SearchSnippetColumns.SNIPPET, null); 407 } 408 409 public void testSearchByEmailAddress() { 410 long rawContactId = createRawContact(); 411 insertPhoneNumber(rawContactId, "1234567890"); 412 insertEmail(rawContactId, "john (at) doe.com"); 413 insertNote(rawContactId, "a hundred dollar note for doe (at) john.com and bob parr"); 414 415 assertStoredValue(buildSearchUri("john@d", true), SearchSnippetColumns.SNIPPET, 416 "[john (at) doe.com]"); 417 assertStoredValue(buildSearchUri("doe@j", true), SearchSnippetColumns.SNIPPET, 418 "...note for [doe (at) john.com] and bob..."); 419 assertStoredValue(buildSearchUri("bob@p", true), SearchSnippetColumns.SNIPPET, null); 420 } 421 422 public void testSearchByPhoneNumber() { 423 long rawContactId = createRawContact(); 424 insertPhoneNumber(rawContactId, "330142685300"); 425 insertPhoneNumber(rawContactId, "(800)GOOG-123"); 426 insertEmail(rawContactId, "john (at) doe.com"); 427 insertNote(rawContactId, "the eighteenth episode of Seinfeld, 650-253-0000"); 428 429 assertStoredValue(buildSearchUri("33 (0)1 42 68 53 00"), SearchSnippetColumns.SNIPPET, 430 "[330142685300]"); 431 assertStoredValue(buildSearchUri("8004664"), SearchSnippetColumns.SNIPPET, 432 "[(800)GOOG-123]"); 433 assertStoredValue(buildSearchUri("650-2"), SearchSnippetColumns.SNIPPET, 434 "...doe.com\nthe eighteenth episode of Seinfeld, [650]-[253]-0000"); 435 436 // for numbers outside of the real phone field, any order (and prefixing) is allowed 437 assertStoredValue(buildSearchUri("25 650"), SearchSnippetColumns.SNIPPET, 438 "...doe.com\nthe eighteenth episode of Seinfeld, [650]-[253]-0000"); 439 } 440 441 private Uri buildSearchUri(String filter) { 442 return buildSearchUri(filter, false); 443 } 444 445 private Uri buildSearchUri(String filter, boolean deferredSnippeting) { 446 return buildSearchUri(filter, null, deferredSnippeting); 447 } 448 449 private Uri buildSearchUri(String filter, String args, boolean deferredSnippeting) { 450 Builder builder = Contacts.CONTENT_FILTER_URI.buildUpon().appendPath(filter); 451 if (args != null) { 452 builder.appendQueryParameter(SearchSnippetColumns.SNIPPET_ARGS_PARAM_KEY, args); 453 } 454 if (deferredSnippeting) { 455 builder.appendQueryParameter(SearchSnippetColumns.DEFERRED_SNIPPETING_KEY, "1"); 456 } 457 return builder.build(); 458 } 459 460 private void createRawContactWithDisplayName(String name) { 461 long rawContactId = createRawContact(); 462 ContentValues values = new ContentValues(); 463 values.put(StructuredName.DISPLAY_NAME, name); 464 insertStructuredName(rawContactId, values); 465 } 466 467 // TODO: expectedName must be tested. Many tests in here are quite useless at the moment 468 private void assertSearchIndex( 469 long contactId, String expectedContent, String expectedName, String expectedTokens) { 470 ContactsDatabaseHelper dbHelper = (ContactsDatabaseHelper) getContactsProvider() 471 .getDatabaseHelper(); 472 assertEquals(expectedContent, dbHelper.querySearchIndexContentForTest(contactId)); 473 assertEquals(expectedTokens, dbHelper.querySearchIndexTokensForTest(contactId)); 474 } 475 } 476 477