1 package org.robolectric.shadows; 2 3 import static android.os.Build.VERSION_CODES.LOLLIPOP; 4 import static com.google.common.truth.Truth.assertThat; 5 import static org.mockito.Mockito.mock; 6 import static org.mockito.Mockito.never; 7 import static org.mockito.Mockito.verify; 8 import static org.robolectric.Shadows.shadowOf; 9 import static org.robolectric.shadows.ShadowLooper.shadowMainLooper; 10 11 import android.app.Activity; 12 import android.os.Bundle; 13 import android.speech.tts.TextToSpeech; 14 import android.speech.tts.TextToSpeech.Engine; 15 import android.speech.tts.UtteranceProgressListener; 16 import android.speech.tts.Voice; 17 import androidx.test.ext.junit.runners.AndroidJUnit4; 18 import com.google.common.collect.ImmutableSet; 19 import java.io.File; 20 import java.io.IOException; 21 import java.util.HashMap; 22 import java.util.Locale; 23 import java.util.concurrent.atomic.AtomicReference; 24 import org.junit.Before; 25 import org.junit.Test; 26 import org.junit.rules.TemporaryFolder; 27 import org.junit.runner.RunWith; 28 import org.robolectric.Robolectric; 29 import org.robolectric.Shadows; 30 import org.robolectric.annotation.Config; 31 32 @RunWith(AndroidJUnit4.class) 33 public class ShadowTextToSpeechTest { 34 private Activity activity; 35 36 @Before setUp()37 public void setUp() { 38 activity = Robolectric.buildActivity(Activity.class).create().get(); 39 } 40 41 @Test shouldNotBeNull()42 public void shouldNotBeNull() { 43 TextToSpeech textToSpeech = new TextToSpeech(activity, result -> {}); 44 assertThat(textToSpeech).isNotNull(); 45 assertThat(shadowOf(textToSpeech)).isNotNull(); 46 } 47 48 @Test onInitListener_success_getsCalledAsynchronously()49 public void onInitListener_success_getsCalledAsynchronously() { 50 AtomicReference<Integer> onInitCalled = new AtomicReference<>(); 51 TextToSpeech.OnInitListener listener = onInitCalled::set; 52 TextToSpeech textToSpeech = new TextToSpeech(activity, listener); 53 assertThat(textToSpeech).isNotNull(); 54 Shadows.shadowOf(textToSpeech).getOnInitListener().onInit(TextToSpeech.SUCCESS); 55 assertThat(onInitCalled.get()).isEqualTo(TextToSpeech.SUCCESS); 56 } 57 58 @Test onInitListener_error()59 public void onInitListener_error() { 60 AtomicReference<Integer> onInitCalled = new AtomicReference<>(); 61 TextToSpeech.OnInitListener listener = onInitCalled::set; 62 TextToSpeech textToSpeech = new TextToSpeech(activity, listener); 63 assertThat(textToSpeech).isNotNull(); 64 Shadows.shadowOf(textToSpeech).getOnInitListener().onInit(TextToSpeech.ERROR); 65 assertThat(onInitCalled.get()).isEqualTo(TextToSpeech.ERROR); 66 } 67 68 @Test getContext_shouldReturnContext()69 public void getContext_shouldReturnContext() { 70 TextToSpeech textToSpeech = new TextToSpeech(activity, result -> {}); 71 assertThat(shadowOf(textToSpeech).getContext()).isEqualTo(activity); 72 } 73 74 @Test getOnInitListener_shouldReturnListener()75 public void getOnInitListener_shouldReturnListener() { 76 TextToSpeech.OnInitListener listener = result -> {}; 77 TextToSpeech textToSpeech = new TextToSpeech(activity, listener); 78 assertThat(shadowOf(textToSpeech).getOnInitListener()).isEqualTo(listener); 79 } 80 81 @Test getLastSpokenText_shouldReturnSpokenText()82 public void getLastSpokenText_shouldReturnSpokenText() { 83 TextToSpeech textToSpeech = new TextToSpeech(activity, result -> {}); 84 textToSpeech.speak("Hello", TextToSpeech.QUEUE_FLUSH, null); 85 assertThat(shadowOf(textToSpeech).getLastSpokenText()).isEqualTo("Hello"); 86 } 87 88 @Test getLastSpokenText_shouldReturnMostRecentText()89 public void getLastSpokenText_shouldReturnMostRecentText() { 90 TextToSpeech textToSpeech = new TextToSpeech(activity, result -> {}); 91 textToSpeech.speak("Hello", TextToSpeech.QUEUE_FLUSH, null); 92 textToSpeech.speak("Hi", TextToSpeech.QUEUE_FLUSH, null); 93 assertThat(shadowOf(textToSpeech).getLastSpokenText()).isEqualTo("Hi"); 94 } 95 96 @Test clearLastSpokenText_shouldSetLastSpokenTextToNull()97 public void clearLastSpokenText_shouldSetLastSpokenTextToNull() { 98 TextToSpeech textToSpeech = new TextToSpeech(activity, result -> {}); 99 textToSpeech.speak("Hello", TextToSpeech.QUEUE_FLUSH, null); 100 shadowOf(textToSpeech).clearLastSpokenText(); 101 assertThat(shadowOf(textToSpeech).getLastSpokenText()).isNull(); 102 } 103 104 @Test isShutdown_shouldReturnFalseBeforeShutdown()105 public void isShutdown_shouldReturnFalseBeforeShutdown() { 106 TextToSpeech textToSpeech = new TextToSpeech(activity, result -> {}); 107 assertThat(shadowOf(textToSpeech).isShutdown()).isFalse(); 108 } 109 110 @Test isShutdown_shouldReturnTrueAfterShutdown()111 public void isShutdown_shouldReturnTrueAfterShutdown() { 112 TextToSpeech textToSpeech = new TextToSpeech(activity, result -> {}); 113 textToSpeech.shutdown(); 114 assertThat(shadowOf(textToSpeech).isShutdown()).isTrue(); 115 } 116 117 @Test isStopped_shouldReturnTrueBeforeSpeak()118 public void isStopped_shouldReturnTrueBeforeSpeak() { 119 TextToSpeech textToSpeech = new TextToSpeech(activity, result -> {}); 120 assertThat(shadowOf(textToSpeech).isStopped()).isTrue(); 121 } 122 123 @Test isStopped_shouldReturnTrueAfterStop()124 public void isStopped_shouldReturnTrueAfterStop() { 125 TextToSpeech textToSpeech = new TextToSpeech(activity, result -> {}); 126 textToSpeech.stop(); 127 assertThat(shadowOf(textToSpeech).isStopped()).isTrue(); 128 } 129 130 @Test isStopped_shouldReturnFalseAfterSpeak()131 public void isStopped_shouldReturnFalseAfterSpeak() { 132 TextToSpeech textToSpeech = new TextToSpeech(activity, result -> {}); 133 textToSpeech.speak("Hello", TextToSpeech.QUEUE_FLUSH, null); 134 assertThat(shadowOf(textToSpeech).isStopped()).isFalse(); 135 } 136 137 @Test getQueueMode_shouldReturnMostRecentQueueMode()138 public void getQueueMode_shouldReturnMostRecentQueueMode() { 139 TextToSpeech textToSpeech = new TextToSpeech(activity, result -> {}); 140 textToSpeech.speak("Hello", TextToSpeech.QUEUE_ADD, null); 141 assertThat(shadowOf(textToSpeech).getQueueMode()).isEqualTo(TextToSpeech.QUEUE_ADD); 142 } 143 144 @Test threeArgumentSpeak_withUtteranceId_shouldGetCallbackUtteranceId()145 public void threeArgumentSpeak_withUtteranceId_shouldGetCallbackUtteranceId() { 146 TextToSpeech textToSpeech = new TextToSpeech(activity, result -> {}); 147 UtteranceProgressListener mockListener = mock(UtteranceProgressListener.class); 148 textToSpeech.setOnUtteranceProgressListener(mockListener); 149 HashMap<String, String> paramsMap = new HashMap<>(); 150 paramsMap.put(Engine.KEY_PARAM_UTTERANCE_ID, "ThreeArgument"); 151 textToSpeech.speak("Hello", TextToSpeech.QUEUE_FLUSH, paramsMap); 152 153 shadowMainLooper().idle(); 154 155 verify(mockListener).onStart("ThreeArgument"); 156 verify(mockListener).onDone("ThreeArgument"); 157 } 158 159 @Test threeArgumentSpeak_withoutUtteranceId_shouldDoesNotGetCallback()160 public void threeArgumentSpeak_withoutUtteranceId_shouldDoesNotGetCallback() { 161 TextToSpeech textToSpeech = new TextToSpeech(activity, result -> {}); 162 UtteranceProgressListener mockListener = mock(UtteranceProgressListener.class); 163 textToSpeech.setOnUtteranceProgressListener(mockListener); 164 textToSpeech.speak("Hello", TextToSpeech.QUEUE_FLUSH, null); 165 166 shadowMainLooper().idle(); 167 168 verify(mockListener, never()).onStart(null); 169 verify(mockListener, never()).onDone(null); 170 } 171 172 @Test 173 @Config(minSdk = LOLLIPOP) speak_withUtteranceId_shouldReturnSpokenText()174 public void speak_withUtteranceId_shouldReturnSpokenText() { 175 TextToSpeech textToSpeech = new TextToSpeech(activity, result -> {}); 176 textToSpeech.speak("Hello", TextToSpeech.QUEUE_FLUSH, null, "TTSEnable"); 177 assertThat(shadowOf(textToSpeech).getLastSpokenText()).isEqualTo("Hello"); 178 } 179 180 @Test 181 @Config(minSdk = LOLLIPOP) onUtteranceProgressListener_shouldGetCallbackUtteranceId()182 public void onUtteranceProgressListener_shouldGetCallbackUtteranceId() { 183 TextToSpeech textToSpeech = new TextToSpeech(activity, result -> {}); 184 UtteranceProgressListener mockListener = mock(UtteranceProgressListener.class); 185 textToSpeech.setOnUtteranceProgressListener(mockListener); 186 textToSpeech.speak("Hello", TextToSpeech.QUEUE_FLUSH, null, "TTSEnable"); 187 188 shadowMainLooper().idle(); 189 190 verify(mockListener).onStart("TTSEnable"); 191 verify(mockListener).onDone("TTSEnable"); 192 } 193 194 @Test 195 @Config(minSdk = LOLLIPOP) synthesizeToFile_lastSynthesizeToFileTextStored()196 public void synthesizeToFile_lastSynthesizeToFileTextStored() throws IOException { 197 TextToSpeech textToSpeech = new TextToSpeech(activity, result -> {}); 198 Bundle bundle = new Bundle(); 199 File file = createFile("example.txt"); 200 int result = textToSpeech.synthesizeToFile("text", bundle, file, "id"); 201 202 assertThat(result).isEqualTo(TextToSpeech.SUCCESS); 203 assertThat(shadowOf(textToSpeech).getLastSynthesizeToFileText()).isEqualTo("text"); 204 } 205 206 @Test 207 @Config(minSdk = LOLLIPOP) synthesizeToFile_byDefault_doesNotCallOnStart()208 public void synthesizeToFile_byDefault_doesNotCallOnStart() throws IOException { 209 TextToSpeech textToSpeech = new TextToSpeech(activity, result -> {}); 210 UtteranceProgressListener mockListener = mock(UtteranceProgressListener.class); 211 textToSpeech.setOnUtteranceProgressListener(mockListener); 212 Bundle bundle = new Bundle(); 213 File file = createFile("example.txt"); 214 215 textToSpeech.synthesizeToFile("text", bundle, file, "id"); 216 217 verify(mockListener, never()).onDone("id"); 218 } 219 220 @Test 221 @Config(minSdk = LOLLIPOP) synthesizeToFile_byDefault_doesNotCallOnDone()222 public void synthesizeToFile_byDefault_doesNotCallOnDone() throws IOException { 223 TextToSpeech textToSpeech = new TextToSpeech(activity, result -> {}); 224 UtteranceProgressListener mockListener = mock(UtteranceProgressListener.class); 225 textToSpeech.setOnUtteranceProgressListener(mockListener); 226 Bundle bundle = new Bundle(); 227 File file = createFile("example.txt"); 228 229 textToSpeech.synthesizeToFile("text", bundle, file, "id"); 230 231 verify(mockListener, never()).onDone("id"); 232 } 233 234 @Test 235 @Config(minSdk = LOLLIPOP) synthesizeToFile_successSimulated_callsOnStart()236 public void synthesizeToFile_successSimulated_callsOnStart() throws IOException { 237 TextToSpeech textToSpeech = new TextToSpeech(activity, result -> {}); 238 UtteranceProgressListener mockListener = mock(UtteranceProgressListener.class); 239 textToSpeech.setOnUtteranceProgressListener(mockListener); 240 Bundle bundle = new Bundle(); 241 File file = createFile("example.txt"); 242 243 ShadowTextToSpeech shadowTextToSpeech = shadowOf(textToSpeech); 244 shadowTextToSpeech.simulateSynthesizeToFileResult(TextToSpeech.SUCCESS); 245 246 textToSpeech.synthesizeToFile("text", bundle, file, "id"); 247 248 verify(mockListener).onStart("id"); 249 } 250 251 @Test 252 @Config(minSdk = LOLLIPOP) synthesizeToFile_successSimulated_callsOnDone()253 public void synthesizeToFile_successSimulated_callsOnDone() throws IOException { 254 TextToSpeech textToSpeech = new TextToSpeech(activity, result -> {}); 255 UtteranceProgressListener mockListener = mock(UtteranceProgressListener.class); 256 textToSpeech.setOnUtteranceProgressListener(mockListener); 257 Bundle bundle = new Bundle(); 258 File file = createFile("example.txt"); 259 260 ShadowTextToSpeech shadowTextToSpeech = shadowOf(textToSpeech); 261 shadowTextToSpeech.simulateSynthesizeToFileResult(TextToSpeech.SUCCESS); 262 263 textToSpeech.synthesizeToFile("text", bundle, file, "id"); 264 265 verify(mockListener).onDone("id"); 266 } 267 268 @Test 269 @Config(minSdk = LOLLIPOP) synthesizeToFile_setToFail_doesNotCallIsDone()270 public void synthesizeToFile_setToFail_doesNotCallIsDone() throws IOException { 271 TextToSpeech textToSpeech = new TextToSpeech(activity, result -> {}); 272 UtteranceProgressListener mockListener = mock(UtteranceProgressListener.class); 273 textToSpeech.setOnUtteranceProgressListener(mockListener); 274 Bundle bundle = new Bundle(); 275 File file = createFile("example.txt"); 276 277 ShadowTextToSpeech shadowTextToSpeech = shadowOf(textToSpeech); 278 // The actual error used does not matter for this test. 279 shadowTextToSpeech.simulateSynthesizeToFileResult(TextToSpeech.ERROR_NETWORK_TIMEOUT); 280 281 textToSpeech.synthesizeToFile("text", bundle, file, "id"); 282 283 verify(mockListener, never()).onDone("id"); 284 } 285 286 @Test 287 @Config(minSdk = LOLLIPOP) synthesizeToFile_setToFail_callsOnErrorWithErrorCode()288 public void synthesizeToFile_setToFail_callsOnErrorWithErrorCode() throws IOException { 289 TextToSpeech textToSpeech = new TextToSpeech(activity, result -> {}); 290 UtteranceProgressListener mockListener = mock(UtteranceProgressListener.class); 291 textToSpeech.setOnUtteranceProgressListener(mockListener); 292 Bundle bundle = new Bundle(); 293 File file = createFile("example.txt"); 294 295 ShadowTextToSpeech shadowTextToSpeech = shadowOf(textToSpeech); 296 int errorCode = TextToSpeech.ERROR_NETWORK_TIMEOUT; 297 shadowTextToSpeech.simulateSynthesizeToFileResult(errorCode); 298 299 textToSpeech.synthesizeToFile("text", bundle, file, "id"); 300 301 verify(mockListener).onError("id", errorCode); 302 } 303 304 @Test 305 @Config(minSdk = LOLLIPOP) synthesizeToFile_neverCalled_lastSynthesizeToFileTextNull()306 public void synthesizeToFile_neverCalled_lastSynthesizeToFileTextNull() { 307 TextToSpeech textToSpeech = new TextToSpeech(activity, result -> {}); 308 assertThat(shadowOf(textToSpeech).getLastSynthesizeToFileText()).isNull(); 309 } 310 311 @Test getCurrentLanguage_languageSet_returnsLanguage()312 public void getCurrentLanguage_languageSet_returnsLanguage() { 313 TextToSpeech textToSpeech = new TextToSpeech(activity, result -> {}); 314 Locale language = Locale.forLanguageTag("pl-pl"); 315 textToSpeech.setLanguage(language); 316 assertThat(shadowOf(textToSpeech).getCurrentLanguage()).isEqualTo(language); 317 } 318 319 @Test getCurrentLanguage_languageNeverSet_returnsNull()320 public void getCurrentLanguage_languageNeverSet_returnsNull() { 321 TextToSpeech textToSpeech = new TextToSpeech(activity, result -> {}); 322 assertThat(shadowOf(textToSpeech).getCurrentLanguage()).isNull(); 323 } 324 325 @Test isLanguageAvailable_neverAdded_returnsUnsupported()326 public void isLanguageAvailable_neverAdded_returnsUnsupported() { 327 TextToSpeech textToSpeech = new TextToSpeech(activity, result -> {}); 328 assertThat( 329 textToSpeech.isLanguageAvailable( 330 new Locale.Builder().setLanguage("pl").setRegion("pl").build())) 331 .isEqualTo(TextToSpeech.LANG_NOT_SUPPORTED); 332 } 333 334 @Test isLanguageAvailable_twoLanguageAvailabilities_returnsRequestedAvailability()335 public void isLanguageAvailable_twoLanguageAvailabilities_returnsRequestedAvailability() { 336 TextToSpeech textToSpeech = new TextToSpeech(activity, result -> {}); 337 ShadowTextToSpeech.addLanguageAvailability( 338 new Locale.Builder().setLanguage("pl").setRegion("pl").build()); 339 ShadowTextToSpeech.addLanguageAvailability( 340 new Locale.Builder().setLanguage("ja").setRegion("jp").build()); 341 342 assertThat( 343 textToSpeech.isLanguageAvailable( 344 new Locale.Builder().setLanguage("pl").setRegion("pl").build())) 345 .isEqualTo(TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE); 346 } 347 348 @Test isLanguageAvailable_matchingVariant_returnsCountryVarAvailable()349 public void isLanguageAvailable_matchingVariant_returnsCountryVarAvailable() { 350 TextToSpeech textToSpeech = new TextToSpeech(activity, result -> {}); 351 ShadowTextToSpeech.addLanguageAvailability( 352 new Locale.Builder().setLanguage("en").setRegion("us").setVariant("WOLTK").build()); 353 354 assertThat( 355 textToSpeech.isLanguageAvailable( 356 new Locale.Builder().setLanguage("en").setRegion("us").setVariant("WOLTK").build())) 357 .isEqualTo(TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE); 358 } 359 360 @Test isLanguageAvailable_matchingCountry_returnsLangCountryAvailable()361 public void isLanguageAvailable_matchingCountry_returnsLangCountryAvailable() { 362 TextToSpeech textToSpeech = new TextToSpeech(activity, result -> {}); 363 ShadowTextToSpeech.addLanguageAvailability( 364 new Locale.Builder().setLanguage("en").setRegion("us").setVariant("ONETW").build()); 365 366 assertThat( 367 textToSpeech.isLanguageAvailable( 368 new Locale.Builder().setLanguage("en").setRegion("us").setVariant("THREE").build())) 369 .isEqualTo(TextToSpeech.LANG_COUNTRY_AVAILABLE); 370 } 371 372 @Test isLanguageAvailable_matchingLanguage_returnsLangAvailable()373 public void isLanguageAvailable_matchingLanguage_returnsLangAvailable() { 374 TextToSpeech textToSpeech = new TextToSpeech(activity, result -> {}); 375 ShadowTextToSpeech.addLanguageAvailability( 376 new Locale.Builder().setLanguage("en").setRegion("us").build()); 377 378 assertThat( 379 textToSpeech.isLanguageAvailable( 380 new Locale.Builder().setLanguage("en").setRegion("gb").build())) 381 .isEqualTo(TextToSpeech.LANG_AVAILABLE); 382 } 383 384 @Test isLanguageAvailable_matchingNone_returnsLangNotSupported()385 public void isLanguageAvailable_matchingNone_returnsLangNotSupported() { 386 TextToSpeech textToSpeech = new TextToSpeech(activity, result -> {}); 387 ShadowTextToSpeech.addLanguageAvailability( 388 new Locale.Builder().setLanguage("en").setRegion("us").build()); 389 390 assertThat( 391 textToSpeech.isLanguageAvailable( 392 new Locale.Builder().setLanguage("ja").setRegion("jp").build())) 393 .isEqualTo(TextToSpeech.LANG_NOT_SUPPORTED); 394 } 395 396 @Test getLastTextToSpeechInstance_neverConstructed_returnsNull()397 public void getLastTextToSpeechInstance_neverConstructed_returnsNull() { 398 assertThat(ShadowTextToSpeech.getLastTextToSpeechInstance()).isNull(); 399 } 400 401 @Test getLastTextToSpeechInstance_constructed_returnsInstance()402 public void getLastTextToSpeechInstance_constructed_returnsInstance() { 403 TextToSpeech textToSpeech = new TextToSpeech(activity, result -> {}); 404 assertThat(ShadowTextToSpeech.getLastTextToSpeechInstance()).isEqualTo(textToSpeech); 405 } 406 407 @Test getLastTextToSpeechInstance_constructedTwice_returnsMostRecentInstance()408 public void getLastTextToSpeechInstance_constructedTwice_returnsMostRecentInstance() { 409 TextToSpeech textToSpeechOne = new TextToSpeech(activity, result -> {}); 410 TextToSpeech textToSpeechTwo = new TextToSpeech(activity, result -> {}); 411 412 assertThat(ShadowTextToSpeech.getLastTextToSpeechInstance()).isEqualTo(textToSpeechTwo); 413 assertThat(ShadowTextToSpeech.getLastTextToSpeechInstance()).isNotEqualTo(textToSpeechOne); 414 } 415 416 @Test getSpokenTextList_neverSpoke_returnsEmpty()417 public void getSpokenTextList_neverSpoke_returnsEmpty() { 418 TextToSpeech textToSpeech = new TextToSpeech(activity, result -> {}); 419 assertThat(shadowOf(textToSpeech).getSpokenTextList()).isEmpty(); 420 } 421 422 @Test getSpokenTextList_spoke_returnsSpokenTexts()423 public void getSpokenTextList_spoke_returnsSpokenTexts() { 424 TextToSpeech textToSpeech = new TextToSpeech(activity, result -> {}); 425 426 textToSpeech.speak("one", TextToSpeech.QUEUE_FLUSH, null); 427 textToSpeech.speak("two", TextToSpeech.QUEUE_FLUSH, null); 428 textToSpeech.speak("three", TextToSpeech.QUEUE_FLUSH, null); 429 430 assertThat(shadowOf(textToSpeech).getSpokenTextList()).containsExactly("one", "two", "three"); 431 } 432 433 @Test 434 @Config(minSdk = LOLLIPOP) getCurrentVoice_voiceSet_returnsVoice()435 public void getCurrentVoice_voiceSet_returnsVoice() { 436 TextToSpeech textToSpeech = new TextToSpeech(activity, result -> {}); 437 438 Voice voice = 439 new Voice( 440 "test voice", 441 Locale.getDefault(), 442 Voice.QUALITY_VERY_HIGH, 443 Voice.LATENCY_LOW, 444 false /* requiresNetworkConnection */, 445 ImmutableSet.of()); 446 textToSpeech.setVoice(voice); 447 448 assertThat(shadowOf(textToSpeech).getCurrentVoice()).isEqualTo(voice); 449 } 450 451 @Test 452 @Config(minSdk = LOLLIPOP) getVoices_returnsAvailableVoices()453 public void getVoices_returnsAvailableVoices() { 454 TextToSpeech textToSpeech = new TextToSpeech(activity, result -> {}); 455 456 Voice voice = 457 new Voice( 458 "test voice", 459 Locale.getDefault(), 460 Voice.QUALITY_VERY_HIGH, 461 Voice.LATENCY_LOW, 462 false /* requiresNetworkConnection */, 463 ImmutableSet.of()); 464 ShadowTextToSpeech.addVoice(voice); 465 466 assertThat(shadowOf(textToSpeech).getVoices()).containsExactly(voice); 467 } 468 createFile(String filename)469 private static File createFile(String filename) throws IOException { 470 TemporaryFolder temporaryFolder = new TemporaryFolder(); 471 temporaryFolder.create(); 472 return temporaryFolder.newFile(filename); 473 } 474 } 475