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.internal.telephony; 18 19 import android.annotation.UnsupportedAppUsage; 20 import android.content.Context; 21 import android.os.Build; 22 import android.os.PersistableBundle; 23 import android.os.SystemProperties; 24 import android.telephony.CarrierConfigManager; 25 import android.telephony.Rlog; 26 import android.telephony.data.ApnSetting; 27 import android.text.TextUtils; 28 import android.util.Pair; 29 30 import java.util.ArrayList; 31 import java.util.Random; 32 33 /** 34 * Retry manager allows a simple way to declare a series of 35 * retry timeouts. After creating a RetryManager the configure 36 * method is used to define the sequence. A simple linear series 37 * may be initialized using configure with three integer parameters 38 * The other configure method allows a series to be declared using 39 * a string. 40 *<p> 41 * The format of the configuration string is the apn type followed by a series of parameters 42 * separated by a comma. There are two name value pair parameters plus a series 43 * of delay times. The units of of these delay times is unspecified. 44 * The name value pairs which may be specified are: 45 *<ul> 46 *<li>max_retries=<value> 47 *<li>default_randomizationTime=<value> 48 *</ul> 49 *<p> 50 * apn type specifies the APN type that the retry pattern will apply for. "others" is for all other 51 * APN types not specified in the config. 52 * 53 * max_retries is the number of times that incrementRetryCount 54 * maybe called before isRetryNeeded will return false. if value 55 * is infinite then isRetryNeeded will always return true. 56 * 57 * default_randomizationTime will be used as the randomizationTime 58 * for delay times which have no supplied randomizationTime. If 59 * default_randomizationTime is not defined it defaults to 0. 60 *<p> 61 * The other parameters define The series of delay times and each 62 * may have an optional randomization value separated from the 63 * delay time by a colon. 64 *<p> 65 * Examples: 66 * <ul> 67 * <li>3 retries for mms with no randomization value which means its 0: 68 * <ul><li><code>"mms:1000, 2000, 3000"</code></ul> 69 * 70 * <li>10 retries for default APN with a 500 default randomization value for each and 71 * the 4..10 retries all using 3000 as the delay: 72 * <ul><li><code>"default:max_retries=10, default_randomization=500, 1000, 2000, 3000"</code></ul> 73 * 74 * <li>4 retries for supl APN with a 100 as the default randomization value for the first 2 values 75 * and the other two having specified values of 500: 76 * <ul><li><code>"supl:default_randomization=100, 1000, 2000, 4000:500, 5000:500"</code></ul> 77 * 78 * <li>Infinite number of retries for all other APNs with the first one at 1000, the second at 2000 79 * all others will be at 3000. 80 * <ul><li><code>"others:max_retries=infinite,1000,2000,3000</code></ul> 81 * </ul> 82 * 83 * {@hide} 84 */ 85 public class RetryManager { 86 public static final String LOG_TAG = "RetryManager"; 87 public static final boolean DBG = true; 88 public static final boolean VDBG = false; // STOPSHIP if true 89 90 /** 91 * The default retry configuration for APNs. See above for the syntax. 92 */ 93 private static final String DEFAULT_DATA_RETRY_CONFIG = "max_retries=3, 5000, 5000, 5000"; 94 95 /** 96 * The APN type used for all other APNs retry configuration. 97 */ 98 private static final String OTHERS_APN_TYPE = "others"; 99 100 /** 101 * The default value (in milliseconds) for delay between APN trying (mInterApnDelay) 102 * within the same round 103 */ 104 private static final long DEFAULT_INTER_APN_DELAY = 20000; 105 106 /** 107 * The default value (in milliseconds) for delay between APN trying (mFailFastInterApnDelay) 108 * within the same round when we are in fail fast mode 109 */ 110 private static final long DEFAULT_INTER_APN_DELAY_FOR_PROVISIONING = 3000; 111 112 /** 113 * The default value (in milliseconds) for retrying APN after disconnect 114 */ 115 private static final long DEFAULT_APN_RETRY_AFTER_DISCONNECT_DELAY = 10000; 116 117 /** 118 * The value indicating no retry is needed 119 */ 120 public static final long NO_RETRY = -1; 121 122 /** 123 * The value indicating modem did not suggest any retry delay 124 */ 125 public static final long NO_SUGGESTED_RETRY_DELAY = -2; 126 127 /** 128 * If the modem suggests a retry delay in the data call setup response, we will retry 129 * the current APN setting again. However, if the modem keeps suggesting retrying the same 130 * APN setting, we'll fall into an infinite loop. Therefore adding a counter to retry up to 131 * MAX_SAME_APN_RETRY times can avoid it. 132 */ 133 private static final int MAX_SAME_APN_RETRY = 3; 134 135 /** 136 * The delay (in milliseconds) between APN trying within the same round 137 */ 138 @UnsupportedAppUsage 139 private long mInterApnDelay; 140 141 /** 142 * The delay (in milliseconds) between APN trying within the same round when we are in 143 * fail fast mode 144 */ 145 @UnsupportedAppUsage 146 private long mFailFastInterApnDelay; 147 148 /** 149 * The delay (in milliseconds) for APN retrying after disconnect (e.g. Modem suddenly reports 150 * data call lost) 151 */ 152 private long mApnRetryAfterDisconnectDelay; 153 154 /** 155 * Modem suggested delay for retrying the current APN 156 */ 157 private long mModemSuggestedDelay = NO_SUGGESTED_RETRY_DELAY; 158 159 /** 160 * The counter for same APN retrying. See MAX_SAME_APN_RETRY for the details. 161 */ 162 private int mSameApnRetryCount = 0; 163 164 /** 165 * Retry record with times in milli-seconds 166 */ 167 private static class RetryRec { RetryRec(int delayTime, int randomizationTime)168 RetryRec(int delayTime, int randomizationTime) { 169 mDelayTime = delayTime; 170 mRandomizationTime = randomizationTime; 171 } 172 173 int mDelayTime; 174 int mRandomizationTime; 175 } 176 177 /** 178 * The array of retry records 179 */ 180 private ArrayList<RetryRec> mRetryArray = new ArrayList<RetryRec>(); 181 182 @UnsupportedAppUsage 183 private Phone mPhone; 184 185 /** 186 * Flag indicating whether retrying forever regardless the maximum retry count mMaxRetryCount 187 */ 188 private boolean mRetryForever = false; 189 190 /** 191 * The maximum number of retries to attempt 192 */ 193 private int mMaxRetryCount; 194 195 /** 196 * The current number of retries 197 */ 198 private int mRetryCount = 0; 199 200 /** 201 * Random number generator. The random delay will be added into retry timer to avoid all devices 202 * around retrying the APN at the same time. 203 */ 204 private Random mRng = new Random(); 205 206 /** 207 * Retry manager configuration string. See top of the detailed explanation. 208 */ 209 private String mConfig; 210 211 /** 212 * The list to store APN setting candidates for data call setup. Most of the carriers only have 213 * one APN, but few carriers have more than one. 214 */ 215 private ArrayList<ApnSetting> mWaitingApns = null; 216 217 /** 218 * Index pointing to the current trying APN from mWaitingApns 219 */ 220 private int mCurrentApnIndex = -1; 221 222 /** 223 * Apn context type. Could be "default, "mms", "supl", etc... 224 */ 225 @UnsupportedAppUsage 226 private String mApnType; 227 228 /** 229 * Retry manager constructor 230 * @param phone Phone object 231 * @param apnType APN type 232 */ RetryManager(Phone phone, String apnType)233 public RetryManager(Phone phone, String apnType) { 234 mPhone = phone; 235 mApnType = apnType; 236 } 237 238 /** 239 * Configure for using string which allow arbitrary 240 * sequences of times. See class comments for the 241 * string format. 242 * 243 * @return true if successful 244 */ 245 @UnsupportedAppUsage configure(String configStr)246 private boolean configure(String configStr) { 247 // Strip quotes if present. 248 if ((configStr.startsWith("\"") && configStr.endsWith("\""))) { 249 configStr = configStr.substring(1, configStr.length() - 1); 250 } 251 252 // Reset the retry manager since delay, max retry count, etc...will be reset. 253 reset(); 254 255 if (DBG) log("configure: '" + configStr + "'"); 256 mConfig = configStr; 257 258 if (!TextUtils.isEmpty(configStr)) { 259 int defaultRandomization = 0; 260 261 if (VDBG) log("configure: not empty"); 262 263 String strArray[] = configStr.split(","); 264 for (int i = 0; i < strArray.length; i++) { 265 if (VDBG) log("configure: strArray[" + i + "]='" + strArray[i] + "'"); 266 Pair<Boolean, Integer> value; 267 String splitStr[] = strArray[i].split("=", 2); 268 splitStr[0] = splitStr[0].trim(); 269 if (VDBG) log("configure: splitStr[0]='" + splitStr[0] + "'"); 270 if (splitStr.length > 1) { 271 splitStr[1] = splitStr[1].trim(); 272 if (VDBG) log("configure: splitStr[1]='" + splitStr[1] + "'"); 273 if (TextUtils.equals(splitStr[0], "default_randomization")) { 274 value = parseNonNegativeInt(splitStr[0], splitStr[1]); 275 if (!value.first) return false; 276 defaultRandomization = value.second; 277 } else if (TextUtils.equals(splitStr[0], "max_retries")) { 278 if (TextUtils.equals("infinite", splitStr[1])) { 279 mRetryForever = true; 280 } else { 281 value = parseNonNegativeInt(splitStr[0], splitStr[1]); 282 if (!value.first) return false; 283 mMaxRetryCount = value.second; 284 } 285 } else { 286 Rlog.e(LOG_TAG, "Unrecognized configuration name value pair: " 287 + strArray[i]); 288 return false; 289 } 290 } else { 291 /** 292 * Assume a retry time with an optional randomization value 293 * following a ":" 294 */ 295 splitStr = strArray[i].split(":", 2); 296 splitStr[0] = splitStr[0].trim(); 297 RetryRec rr = new RetryRec(0, 0); 298 value = parseNonNegativeInt("delayTime", splitStr[0]); 299 if (!value.first) return false; 300 rr.mDelayTime = value.second; 301 302 // Check if optional randomization value present 303 if (splitStr.length > 1) { 304 splitStr[1] = splitStr[1].trim(); 305 if (VDBG) log("configure: splitStr[1]='" + splitStr[1] + "'"); 306 value = parseNonNegativeInt("randomizationTime", splitStr[1]); 307 if (!value.first) return false; 308 rr.mRandomizationTime = value.second; 309 } else { 310 rr.mRandomizationTime = defaultRandomization; 311 } 312 mRetryArray.add(rr); 313 } 314 } 315 if (mRetryArray.size() > mMaxRetryCount) { 316 mMaxRetryCount = mRetryArray.size(); 317 if (VDBG) log("configure: setting mMaxRetryCount=" + mMaxRetryCount); 318 } 319 } else { 320 log("configure: cleared"); 321 } 322 323 if (VDBG) log("configure: true"); 324 return true; 325 } 326 327 /** 328 * Configure the retry manager 329 */ configureRetry()330 private void configureRetry() { 331 String configString = null; 332 String otherConfigString = null; 333 334 try { 335 if (Build.IS_DEBUGGABLE) { 336 // Using system properties is easier for testing from command line. 337 String config = SystemProperties.get("test.data_retry_config"); 338 if (!TextUtils.isEmpty(config)) { 339 configure(config); 340 return; 341 } 342 } 343 344 CarrierConfigManager configManager = (CarrierConfigManager) 345 mPhone.getContext().getSystemService(Context.CARRIER_CONFIG_SERVICE); 346 PersistableBundle b = configManager.getConfigForSubId(mPhone.getSubId()); 347 348 mInterApnDelay = b.getLong( 349 CarrierConfigManager.KEY_CARRIER_DATA_CALL_APN_DELAY_DEFAULT_LONG, 350 DEFAULT_INTER_APN_DELAY); 351 mFailFastInterApnDelay = b.getLong( 352 CarrierConfigManager.KEY_CARRIER_DATA_CALL_APN_DELAY_FASTER_LONG, 353 DEFAULT_INTER_APN_DELAY_FOR_PROVISIONING); 354 mApnRetryAfterDisconnectDelay = b.getLong( 355 CarrierConfigManager.KEY_CARRIER_DATA_CALL_APN_RETRY_AFTER_DISCONNECT_LONG, 356 DEFAULT_APN_RETRY_AFTER_DISCONNECT_DELAY); 357 358 // Load all retry patterns for all different APNs. 359 String[] allConfigStrings = b.getStringArray( 360 CarrierConfigManager.KEY_CARRIER_DATA_CALL_RETRY_CONFIG_STRINGS); 361 if (allConfigStrings != null) { 362 for (String s : allConfigStrings) { 363 if (!TextUtils.isEmpty(s)) { 364 String splitStr[] = s.split(":", 2); 365 if (splitStr.length == 2) { 366 String apnType = splitStr[0].trim(); 367 // Check if this retry pattern is for the APN we want. 368 if (apnType.equals(mApnType)) { 369 // Extract the config string. Note that an empty string is valid 370 // here, meaning no retry for the specified APN. 371 configString = splitStr[1]; 372 break; 373 } else if (apnType.equals(OTHERS_APN_TYPE)) { 374 // Extract the config string. Note that an empty string is valid 375 // here, meaning no retry for all other APNs. 376 otherConfigString = splitStr[1]; 377 } 378 } 379 } 380 } 381 } 382 383 if (configString == null) { 384 if (otherConfigString != null) { 385 configString = otherConfigString; 386 } else { 387 // We should never reach here. If we reach here, it must be a configuration 388 // error bug. 389 log("Invalid APN retry configuration!. Use the default one now."); 390 configString = DEFAULT_DATA_RETRY_CONFIG; 391 } 392 } 393 } catch (NullPointerException ex) { 394 // We should never reach here unless there is a bug 395 log("Failed to read configuration! Use the hardcoded default value."); 396 397 mInterApnDelay = DEFAULT_INTER_APN_DELAY; 398 mFailFastInterApnDelay = DEFAULT_INTER_APN_DELAY_FOR_PROVISIONING; 399 configString = DEFAULT_DATA_RETRY_CONFIG; 400 } 401 402 if (VDBG) { 403 log("mInterApnDelay = " + mInterApnDelay + ", mFailFastInterApnDelay = " + 404 mFailFastInterApnDelay); 405 } 406 407 configure(configString); 408 } 409 410 /** 411 * Return the timer that should be used to trigger the data reconnection 412 */ 413 @UnsupportedAppUsage getRetryTimer()414 private int getRetryTimer() { 415 int index; 416 if (mRetryCount < mRetryArray.size()) { 417 index = mRetryCount; 418 } else { 419 index = mRetryArray.size() - 1; 420 } 421 422 int retVal; 423 if ((index >= 0) && (index < mRetryArray.size())) { 424 retVal = mRetryArray.get(index).mDelayTime + nextRandomizationTime(index); 425 } else { 426 retVal = 0; 427 } 428 429 if (DBG) log("getRetryTimer: " + retVal); 430 return retVal; 431 } 432 433 /** 434 * Parse an integer validating the value is not negative. 435 * @param name Name 436 * @param stringValue Value 437 * @return Pair.first == true if stringValue an integer >= 0 438 */ parseNonNegativeInt(String name, String stringValue)439 private Pair<Boolean, Integer> parseNonNegativeInt(String name, String stringValue) { 440 int value; 441 Pair<Boolean, Integer> retVal; 442 try { 443 value = Integer.parseInt(stringValue); 444 retVal = new Pair<Boolean, Integer>(validateNonNegativeInt(name, value), value); 445 } catch (NumberFormatException e) { 446 Rlog.e(LOG_TAG, name + " bad value: " + stringValue, e); 447 retVal = new Pair<Boolean, Integer>(false, 0); 448 } 449 if (VDBG) { 450 log("parseNonNetativeInt: " + name + ", " + stringValue + ", " 451 + retVal.first + ", " + retVal.second); 452 } 453 return retVal; 454 } 455 456 /** 457 * Validate an integer is >= 0 and logs an error if not 458 * @param name Name 459 * @param value Value 460 * @return Pair.first 461 */ validateNonNegativeInt(String name, int value)462 private boolean validateNonNegativeInt(String name, int value) { 463 boolean retVal; 464 if (value < 0) { 465 Rlog.e(LOG_TAG, name + " bad value: is < 0"); 466 retVal = false; 467 } else { 468 retVal = true; 469 } 470 if (VDBG) log("validateNonNegative: " + name + ", " + value + ", " + retVal); 471 return retVal; 472 } 473 474 /** 475 * Return next random number for the index 476 * @param index Retry index 477 */ nextRandomizationTime(int index)478 private int nextRandomizationTime(int index) { 479 int randomTime = mRetryArray.get(index).mRandomizationTime; 480 if (randomTime == 0) { 481 return 0; 482 } else { 483 return mRng.nextInt(randomTime); 484 } 485 } 486 487 /** 488 * Get the next APN setting for data call setup. 489 * @return APN setting to try 490 */ getNextApnSetting()491 public ApnSetting getNextApnSetting() { 492 493 if (mWaitingApns == null || mWaitingApns.size() == 0) { 494 log("Waiting APN list is null or empty."); 495 return null; 496 } 497 498 // If the modem had suggested a retry delay, we should retry the current APN again 499 // (up to MAX_SAME_APN_RETRY times) instead of getting the next APN setting from 500 // our own list. 501 if (mModemSuggestedDelay != NO_SUGGESTED_RETRY_DELAY && 502 mSameApnRetryCount < MAX_SAME_APN_RETRY) { 503 mSameApnRetryCount++; 504 return mWaitingApns.get(mCurrentApnIndex); 505 } 506 507 mSameApnRetryCount = 0; 508 509 int index = mCurrentApnIndex; 510 // Loop through the APN list to find out the index of next non-permanent failed APN. 511 while (true) { 512 if (++index == mWaitingApns.size()) index = 0; 513 514 // Stop if we find the non-failed APN. 515 if (!mWaitingApns.get(index).getPermanentFailed()) { 516 break; 517 } 518 519 // If we've already cycled through all the APNs, that means there is no APN we can try 520 if (index == mCurrentApnIndex) return null; 521 } 522 523 mCurrentApnIndex = index; 524 return mWaitingApns.get(mCurrentApnIndex); 525 } 526 527 /** 528 * Get the delay for trying the next waiting APN from the list. 529 * @param failFastEnabled True if fail fast mode enabled. In this case we'll use a shorter 530 * delay. 531 * @return delay in milliseconds 532 */ getDelayForNextApn(boolean failFastEnabled)533 public long getDelayForNextApn(boolean failFastEnabled) { 534 535 if (mWaitingApns == null || mWaitingApns.size() == 0) { 536 log("Waiting APN list is null or empty."); 537 return NO_RETRY; 538 } 539 540 if (mModemSuggestedDelay == NO_RETRY) { 541 log("Modem suggested not retrying."); 542 return NO_RETRY; 543 } 544 545 if (mModemSuggestedDelay != NO_SUGGESTED_RETRY_DELAY && 546 mSameApnRetryCount < MAX_SAME_APN_RETRY) { 547 // If the modem explicitly suggests a retry delay, we should use it, even in fail fast 548 // mode. 549 log("Modem suggested retry in " + mModemSuggestedDelay + " ms."); 550 return mModemSuggestedDelay; 551 } 552 553 // In order to determine the delay to try next APN, we need to peek the next available APN. 554 // Case 1 - If we will start the next round of APN trying, 555 // we use the exponential-growth delay. (e.g. 5s, 10s, 30s...etc.) 556 // Case 2 - If we are still within the same round of APN trying, 557 // we use the fixed standard delay between APNs. (e.g. 20s) 558 559 int index = mCurrentApnIndex; 560 while (true) { 561 if (++index >= mWaitingApns.size()) index = 0; 562 563 // Stop if we find the non-failed APN. 564 if (!mWaitingApns.get(index).getPermanentFailed()) { 565 break; 566 } 567 568 // If we've already cycled through all the APNs, that means all APNs have 569 // permanently failed 570 if (index == mCurrentApnIndex) { 571 log("All APNs have permanently failed."); 572 return NO_RETRY; 573 } 574 } 575 576 long delay; 577 if (index <= mCurrentApnIndex) { 578 // Case 1, if the next APN is in the next round. 579 if (!mRetryForever && mRetryCount + 1 > mMaxRetryCount) { 580 log("Reached maximum retry count " + mMaxRetryCount + "."); 581 return NO_RETRY; 582 } 583 delay = getRetryTimer(); 584 ++mRetryCount; 585 } else { 586 // Case 2, if the next APN is still in the same round. 587 delay = mInterApnDelay; 588 } 589 590 if (failFastEnabled && delay > mFailFastInterApnDelay) { 591 // If we enable fail fast mode, and the delay we got is longer than 592 // fail-fast delay (mFailFastInterApnDelay), use the fail-fast delay. 593 // If the delay we calculated is already shorter than fail-fast delay, 594 // then ignore fail-fast delay. 595 delay = mFailFastInterApnDelay; 596 } 597 598 return delay; 599 } 600 601 /** 602 * Mark the APN setting permanently failed. 603 * @param apn APN setting to be marked as permanently failed 604 * */ markApnPermanentFailed(ApnSetting apn)605 public void markApnPermanentFailed(ApnSetting apn) { 606 if (apn != null) { 607 apn.setPermanentFailed(true); 608 } 609 } 610 611 /** 612 * Reset the retry manager. 613 */ reset()614 private void reset() { 615 mMaxRetryCount = 0; 616 mRetryCount = 0; 617 mCurrentApnIndex = -1; 618 mSameApnRetryCount = 0; 619 mModemSuggestedDelay = NO_SUGGESTED_RETRY_DELAY; 620 mRetryArray.clear(); 621 } 622 623 /** 624 * Set waiting APNs for retrying in case needed. 625 * @param waitingApns Waiting APN list 626 */ setWaitingApns(ArrayList<ApnSetting> waitingApns)627 public void setWaitingApns(ArrayList<ApnSetting> waitingApns) { 628 629 if (waitingApns == null) { 630 log("No waiting APNs provided"); 631 return; 632 } 633 634 mWaitingApns = waitingApns; 635 636 // Since we replace the entire waiting APN list, we need to re-config this retry manager. 637 configureRetry(); 638 639 for (ApnSetting apn : mWaitingApns) { 640 apn.setPermanentFailed(false); 641 } 642 643 log("Setting " + mWaitingApns.size() + " waiting APNs."); 644 645 if (VDBG) { 646 for (int i = 0; i < mWaitingApns.size(); i++) { 647 log(" [" + i + "]:" + mWaitingApns.get(i)); 648 } 649 } 650 } 651 652 /** 653 * Get the list of waiting APNs. 654 * @return the list of waiting APNs 655 */ getWaitingApns()656 public ArrayList<ApnSetting> getWaitingApns() { 657 return mWaitingApns; 658 } 659 660 /** 661 * Save the modem suggested delay for retrying the current APN. 662 * This method is called when we get the suggested delay from RIL. 663 * @param delay The delay in milliseconds 664 */ setModemSuggestedDelay(long delay)665 public void setModemSuggestedDelay(long delay) { 666 mModemSuggestedDelay = delay; 667 } 668 669 /** 670 * Get the delay in milliseconds for APN retry after disconnect 671 * @return The delay in milliseconds 672 */ getRetryAfterDisconnectDelay()673 public long getRetryAfterDisconnectDelay() { 674 return mApnRetryAfterDisconnectDelay; 675 } 676 toString()677 public String toString() { 678 if (mConfig == null) return ""; 679 return "RetryManager: mApnType=" + mApnType + " mRetryCount=" + mRetryCount 680 + " mMaxRetryCount=" + mMaxRetryCount + " mCurrentApnIndex=" + mCurrentApnIndex 681 + " mSameApnRtryCount=" + mSameApnRetryCount + " mModemSuggestedDelay=" 682 + mModemSuggestedDelay + " mRetryForever=" + mRetryForever + " mInterApnDelay=" 683 + mInterApnDelay + " mApnRetryAfterDisconnectDelay=" + mApnRetryAfterDisconnectDelay 684 + " mConfig={" + mConfig + "}"; 685 } 686 687 @UnsupportedAppUsage log(String s)688 private void log(String s) { 689 Rlog.d(LOG_TAG, "[" + mApnType + "] " + s); 690 } 691 } 692