1 /* 2 * Copyright (C) 2015 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.contacts.dialog; 18 19 import android.animation.Animator; 20 import android.animation.AnimatorListenerAdapter; 21 import android.app.Activity; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.content.SharedPreferences; 25 import android.net.Uri; 26 import android.os.Bundle; 27 import android.preference.PreferenceManager; 28 import android.telecom.PhoneAccount; 29 import android.telecom.PhoneAccountHandle; 30 import android.telecom.TelecomManager; 31 import android.text.Editable; 32 import android.text.InputFilter; 33 import android.text.TextUtils; 34 import android.text.TextWatcher; 35 import android.util.Log; 36 import android.view.View; 37 import android.view.ViewGroup; 38 import android.view.ViewTreeObserver; 39 import android.view.inputmethod.InputMethodManager; 40 import android.widget.AdapterView; 41 import android.widget.ArrayAdapter; 42 import android.widget.EditText; 43 import android.widget.ListView; 44 import android.widget.QuickContactBadge; 45 import android.widget.TextView; 46 47 import com.android.contacts.CallUtil; 48 import com.android.contacts.ContactPhotoManager; 49 import com.android.contacts.R; 50 import com.android.contacts.compat.CompatUtils; 51 import com.android.contacts.compat.PhoneAccountSdkCompat; 52 import com.android.contacts.compat.telecom.TelecomManagerCompat; 53 import com.android.contacts.util.UriUtils; 54 import com.android.phone.common.animation.AnimUtils; 55 56 import java.nio.charset.Charset; 57 import java.util.ArrayList; 58 import java.util.List; 59 60 /** 61 * Implements a dialog which prompts for a call subject for an outgoing call. The dialog includes 62 * a pop up list of historical call subjects. 63 */ 64 public class CallSubjectDialog extends Activity { 65 private static final String TAG = "CallSubjectDialog"; 66 private static final int CALL_SUBJECT_LIMIT = 16; 67 private static final int CALL_SUBJECT_HISTORY_SIZE = 5; 68 69 private static final int REQUEST_SUBJECT = 1001; 70 71 public static final String PREF_KEY_SUBJECT_HISTORY_COUNT = "subject_history_count"; 72 public static final String PREF_KEY_SUBJECT_HISTORY_ITEM = "subject_history_item"; 73 74 /** 75 * Activity intent argument bundle keys: 76 */ 77 public static final String ARG_PHOTO_ID = "PHOTO_ID"; 78 public static final String ARG_PHOTO_URI = "PHOTO_URI"; 79 public static final String ARG_CONTACT_URI = "CONTACT_URI"; 80 public static final String ARG_NAME_OR_NUMBER = "NAME_OR_NUMBER"; 81 public static final String ARG_IS_BUSINESS = "IS_BUSINESS"; 82 public static final String ARG_NUMBER = "NUMBER"; 83 public static final String ARG_DISPLAY_NUMBER = "DISPLAY_NUMBER"; 84 public static final String ARG_NUMBER_LABEL = "NUMBER_LABEL"; 85 public static final String ARG_PHONE_ACCOUNT_HANDLE = "PHONE_ACCOUNT_HANDLE"; 86 87 private int mAnimationDuration; 88 private Charset mMessageEncoding; 89 private View mBackgroundView; 90 private View mDialogView; 91 private QuickContactBadge mContactPhoto; 92 private TextView mNameView; 93 private TextView mNumberView; 94 private EditText mCallSubjectView; 95 private TextView mCharacterLimitView; 96 private View mHistoryButton; 97 private View mSendAndCallButton; 98 private ListView mSubjectList; 99 100 private int mLimit = CALL_SUBJECT_LIMIT; 101 private int mPhotoSize; 102 private SharedPreferences mPrefs; 103 private List<String> mSubjectHistory; 104 105 private long mPhotoID; 106 private Uri mPhotoUri; 107 private Uri mContactUri; 108 private String mNameOrNumber; 109 private boolean mIsBusiness; 110 private String mNumber; 111 private String mDisplayNumber; 112 private String mNumberLabel; 113 private PhoneAccountHandle mPhoneAccountHandle; 114 115 /** 116 * Handles changes to the text in the subject box. Ensures the character limit is updated. 117 */ 118 private final TextWatcher mTextWatcher = new TextWatcher() { 119 @Override 120 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 121 // no-op 122 } 123 124 @Override 125 public void onTextChanged(CharSequence s, int start, int before, int count) { 126 updateCharacterLimit(); 127 } 128 129 @Override 130 public void afterTextChanged(Editable s) { 131 // no-op 132 } 133 }; 134 135 /** 136 * Click listener which handles user clicks outside of the dialog. 137 */ 138 private View.OnClickListener mBackgroundListener = new View.OnClickListener() { 139 @Override 140 public void onClick(View v) { 141 finish(); 142 } 143 }; 144 145 /** 146 * Handles displaying the list of past call subjects. 147 */ 148 private final View.OnClickListener mHistoryOnClickListener = new View.OnClickListener() { 149 @Override 150 public void onClick(View v) { 151 hideSoftKeyboard(CallSubjectDialog.this, mCallSubjectView); 152 showCallHistory(mSubjectList.getVisibility() == View.GONE); 153 } 154 }; 155 156 /** 157 * Handles starting a call with a call subject specified. 158 */ 159 private final View.OnClickListener mSendAndCallOnClickListener = new View.OnClickListener() { 160 @Override 161 public void onClick(View v) { 162 String subject = mCallSubjectView.getText().toString(); 163 Intent intent = CallUtil.getCallWithSubjectIntent(mNumber, mPhoneAccountHandle, 164 subject); 165 166 TelecomManagerCompat.placeCall( 167 CallSubjectDialog.this, 168 (TelecomManager) getSystemService(Context.TELECOM_SERVICE), 169 intent); 170 171 mSubjectHistory.add(subject); 172 saveSubjectHistory(mSubjectHistory); 173 finish(); 174 } 175 }; 176 177 /** 178 * Handles auto-hiding the call history when user clicks in the call subject field to give it 179 * focus. 180 */ 181 private final View.OnClickListener mCallSubjectClickListener = new View.OnClickListener() { 182 @Override 183 public void onClick(View v) { 184 if (mSubjectList.getVisibility() == View.VISIBLE) { 185 showCallHistory(false); 186 } 187 } 188 }; 189 190 /** 191 * Item click listener which handles user clicks on the items in the list view. Dismisses 192 * the activity, returning the subject to the caller and closing the activity with the 193 * {@link Activity#RESULT_OK} result code. 194 */ 195 private AdapterView.OnItemClickListener mItemClickListener = 196 new AdapterView.OnItemClickListener() { 197 @Override 198 public void onItemClick(AdapterView<?> arg0, View view, int position, long arg3) { 199 mCallSubjectView.setText(mSubjectHistory.get(position)); 200 showCallHistory(false); 201 } 202 }; 203 204 /** 205 * Show the call subject dialog given a phone number to dial (e.g. from the dialpad). 206 * 207 * @param activity The activity. 208 * @param number The number to dial. 209 */ start(Activity activity, String number)210 public static void start(Activity activity, String number) { 211 start(activity, 212 -1 /* photoId */, 213 null /* photoUri */, 214 null /* contactUri */, 215 number /* nameOrNumber */, 216 false /* isBusiness */, 217 number /* number */, 218 null /* displayNumber */, 219 null /* numberLabel */, 220 null /* phoneAccountHandle */); 221 } 222 223 /** 224 * Creates a call subject dialog. 225 * 226 * @param activity The current activity. 227 * @param photoId The photo ID (used to populate contact photo). 228 * @param photoUri The photo Uri (used to populate contact photo). 229 * @param contactUri The Contact URI (used so quick contact can be invoked from contact photo). 230 * @param nameOrNumber The name or number of the callee. 231 * @param isBusiness {@code true} if a business is being called (used for contact photo). 232 * @param number The raw number to dial. 233 * @param displayNumber The number to dial, formatted for display. 234 * @param numberLabel The label for the number (if from a contact). 235 * @param phoneAccountHandle The phone account handle. 236 */ start(Activity activity, long photoId, Uri photoUri, Uri contactUri, String nameOrNumber, boolean isBusiness, String number, String displayNumber, String numberLabel, PhoneAccountHandle phoneAccountHandle)237 public static void start(Activity activity, long photoId, Uri photoUri, Uri contactUri, 238 String nameOrNumber, boolean isBusiness, String number, String displayNumber, 239 String numberLabel, PhoneAccountHandle phoneAccountHandle) { 240 Bundle arguments = new Bundle(); 241 arguments.putLong(ARG_PHOTO_ID, photoId); 242 arguments.putParcelable(ARG_PHOTO_URI, photoUri); 243 arguments.putParcelable(ARG_CONTACT_URI, contactUri); 244 arguments.putString(ARG_NAME_OR_NUMBER, nameOrNumber); 245 arguments.putBoolean(ARG_IS_BUSINESS, isBusiness); 246 arguments.putString(ARG_NUMBER, number); 247 arguments.putString(ARG_DISPLAY_NUMBER, displayNumber); 248 arguments.putString(ARG_NUMBER_LABEL, numberLabel); 249 arguments.putParcelable(ARG_PHONE_ACCOUNT_HANDLE, phoneAccountHandle); 250 start(activity, arguments); 251 } 252 253 /** 254 * Shows the call subject dialog given a Bundle containing all the arguments required to 255 * display the dialog (e.g. from Quick Contacts). 256 * 257 * @param activity The activity. 258 * @param arguments The arguments bundle. 259 */ start(Activity activity, Bundle arguments)260 public static void start(Activity activity, Bundle arguments) { 261 Intent intent = new Intent(activity, CallSubjectDialog.class); 262 intent.putExtras(arguments); 263 activity.startActivity(intent); 264 } 265 266 /** 267 * Creates the dialog, inflating the layout and populating it with the name and phone number. 268 * 269 * @param savedInstanceState The last saved instance state of the Fragment, 270 * or null if this is a freshly created Fragment. 271 * 272 * @return Dialog instance. 273 */ 274 @Override onCreate(Bundle savedInstanceState)275 public void onCreate(Bundle savedInstanceState) { 276 super.onCreate(savedInstanceState); 277 getWindow().setHideOverlayWindows(true); 278 mAnimationDuration = getResources().getInteger(R.integer.call_subject_animation_duration); 279 mPrefs = PreferenceManager.getDefaultSharedPreferences(this); 280 mPhotoSize = getResources().getDimensionPixelSize( 281 R.dimen.call_subject_dialog_contact_photo_size); 282 readArguments(); 283 loadConfiguration(); 284 mSubjectHistory = loadSubjectHistory(mPrefs); 285 286 setContentView(R.layout.dialog_call_subject); 287 getWindow().setLayout(ViewGroup.LayoutParams.MATCH_PARENT, 288 ViewGroup.LayoutParams.MATCH_PARENT); 289 mBackgroundView = findViewById(R.id.call_subject_dialog); 290 mBackgroundView.setOnClickListener(mBackgroundListener); 291 mDialogView = findViewById(R.id.dialog_view); 292 mContactPhoto = (QuickContactBadge) findViewById(R.id.contact_photo); 293 mNameView = (TextView) findViewById(R.id.name); 294 mNumberView = (TextView) findViewById(R.id.number); 295 mCallSubjectView = (EditText) findViewById(R.id.call_subject); 296 mCallSubjectView.addTextChangedListener(mTextWatcher); 297 mCallSubjectView.setOnClickListener(mCallSubjectClickListener); 298 InputFilter[] filters = new InputFilter[1]; 299 filters[0] = new InputFilter.LengthFilter(mLimit); 300 mCallSubjectView.setFilters(filters); 301 mCharacterLimitView = (TextView) findViewById(R.id.character_limit); 302 mHistoryButton = findViewById(R.id.history_button); 303 mHistoryButton.setOnClickListener(mHistoryOnClickListener); 304 mHistoryButton.setVisibility(mSubjectHistory.isEmpty() ? View.GONE : View.VISIBLE); 305 mSendAndCallButton = findViewById(R.id.send_and_call_button); 306 mSendAndCallButton.setOnClickListener(mSendAndCallOnClickListener); 307 mSubjectList = (ListView) findViewById(R.id.subject_list); 308 mSubjectList.setOnItemClickListener(mItemClickListener); 309 mSubjectList.setVisibility(View.GONE); 310 311 updateContactInfo(); 312 updateCharacterLimit(); 313 } 314 315 /** 316 * Populates the contact info fields based on the current contact information. 317 */ updateContactInfo()318 private void updateContactInfo() { 319 if (mContactUri != null) { 320 setPhoto(mPhotoID, mPhotoUri, mContactUri, mNameOrNumber, mIsBusiness); 321 } else { 322 mContactPhoto.setVisibility(View.GONE); 323 } 324 mNameView.setText(mNameOrNumber); 325 if (!TextUtils.isEmpty(mNumberLabel) && !TextUtils.isEmpty(mDisplayNumber)) { 326 mNumberView.setVisibility(View.VISIBLE); 327 mNumberView.setText(getString(R.string.call_subject_type_and_number, 328 mNumberLabel, mDisplayNumber)); 329 } else { 330 mNumberView.setVisibility(View.GONE); 331 mNumberView.setText(null); 332 } 333 } 334 335 /** 336 * Reads arguments from the fragment arguments and populates the necessary instance variables. 337 */ readArguments()338 private void readArguments() { 339 Bundle arguments = getIntent().getExtras(); 340 if (arguments == null) { 341 Log.e(TAG, "Arguments cannot be null."); 342 return; 343 } 344 mPhotoID = arguments.getLong(ARG_PHOTO_ID); 345 mPhotoUri = arguments.getParcelable(ARG_PHOTO_URI); 346 mContactUri = arguments.getParcelable(ARG_CONTACT_URI); 347 mNameOrNumber = arguments.getString(ARG_NAME_OR_NUMBER); 348 mIsBusiness = arguments.getBoolean(ARG_IS_BUSINESS); 349 mNumber = arguments.getString(ARG_NUMBER); 350 mDisplayNumber = arguments.getString(ARG_DISPLAY_NUMBER); 351 mNumberLabel = arguments.getString(ARG_NUMBER_LABEL); 352 mPhoneAccountHandle = arguments.getParcelable(ARG_PHONE_ACCOUNT_HANDLE); 353 } 354 355 /** 356 * Updates the character limit display, coloring the text RED when the limit is reached or 357 * exceeded. 358 */ updateCharacterLimit()359 private void updateCharacterLimit() { 360 String subjectText = mCallSubjectView.getText().toString(); 361 final int length; 362 363 // If a message encoding is specified, use that to count bytes in the message. 364 if (mMessageEncoding != null) { 365 length = subjectText.getBytes(mMessageEncoding).length; 366 } else { 367 // No message encoding specified, so just count characters entered. 368 length = subjectText.length(); 369 } 370 371 mCharacterLimitView.setText( 372 getString(R.string.call_subject_limit, length, mLimit)); 373 if (length >= mLimit) { 374 mCharacterLimitView.setTextColor(getResources().getColor( 375 R.color.call_subject_limit_exceeded)); 376 } else { 377 mCharacterLimitView.setTextColor(getResources().getColor( 378 R.color.dialtacts_secondary_text_color)); 379 } 380 } 381 382 /** 383 * Sets the photo on the quick contact photo. 384 * 385 * @param photoId 386 * @param photoUri 387 * @param contactUri 388 * @param displayName 389 * @param isBusiness 390 */ setPhoto(long photoId, Uri photoUri, Uri contactUri, String displayName, boolean isBusiness)391 private void setPhoto(long photoId, Uri photoUri, Uri contactUri, String displayName, 392 boolean isBusiness) { 393 mContactPhoto.assignContactUri(contactUri); 394 if (CompatUtils.isLollipopCompatible()) { 395 mContactPhoto.setOverlay(null); 396 } 397 398 int contactType; 399 if (isBusiness) { 400 contactType = ContactPhotoManager.TYPE_BUSINESS; 401 } else { 402 contactType = ContactPhotoManager.TYPE_DEFAULT; 403 } 404 405 String lookupKey = null; 406 if (contactUri != null) { 407 lookupKey = UriUtils.getLookupKeyFromUri(contactUri); 408 } 409 410 ContactPhotoManager.DefaultImageRequest 411 request = new ContactPhotoManager.DefaultImageRequest( 412 displayName, lookupKey, contactType, true /* isCircular */); 413 414 if (photoId == 0 && photoUri != null) { 415 ContactPhotoManager.getInstance(this).loadPhoto(mContactPhoto, photoUri, 416 mPhotoSize, false /* darkTheme */, true /* isCircular */, request); 417 } else { 418 ContactPhotoManager.getInstance(this).loadThumbnail(mContactPhoto, photoId, 419 false /* darkTheme */, true /* isCircular */, request); 420 } 421 } 422 423 /** 424 * Loads the subject history from shared preferences. 425 * 426 * @param prefs Shared preferences. 427 * @return List of subject history strings. 428 */ loadSubjectHistory(SharedPreferences prefs)429 public static List<String> loadSubjectHistory(SharedPreferences prefs) { 430 int historySize = prefs.getInt(PREF_KEY_SUBJECT_HISTORY_COUNT, 0); 431 List<String> subjects = new ArrayList(historySize); 432 433 for (int ix = 0 ; ix < historySize; ix++) { 434 String historyItem = prefs.getString(PREF_KEY_SUBJECT_HISTORY_ITEM + ix, null); 435 if (!TextUtils.isEmpty(historyItem)) { 436 subjects.add(historyItem); 437 } 438 } 439 440 return subjects; 441 } 442 443 /** 444 * Saves the subject history list to shared prefs, removing older items so that there are only 445 * {@link #CALL_SUBJECT_HISTORY_SIZE} items at most. 446 * 447 * @param history The history. 448 */ saveSubjectHistory(List<String> history)449 private void saveSubjectHistory(List<String> history) { 450 // Remove oldest subject(s). 451 while (history.size() > CALL_SUBJECT_HISTORY_SIZE) { 452 history.remove(0); 453 } 454 455 SharedPreferences.Editor editor = mPrefs.edit(); 456 int historyCount = 0; 457 for (String subject : history) { 458 if (!TextUtils.isEmpty(subject)) { 459 editor.putString(PREF_KEY_SUBJECT_HISTORY_ITEM + historyCount, 460 subject); 461 historyCount++; 462 } 463 } 464 editor.putInt(PREF_KEY_SUBJECT_HISTORY_COUNT, historyCount); 465 editor.apply(); 466 } 467 468 /** 469 * Hide software keyboard for the given {@link View}. 470 */ hideSoftKeyboard(Context context, View view)471 public void hideSoftKeyboard(Context context, View view) { 472 InputMethodManager imm = (InputMethodManager) context.getSystemService( 473 Context.INPUT_METHOD_SERVICE); 474 if (imm != null) { 475 imm.hideSoftInputFromWindow(view.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS); 476 } 477 } 478 479 /** 480 * Hides or shows the call history list. 481 * 482 * @param show {@code true} if the call history should be shown, {@code false} otherwise. 483 */ showCallHistory(final boolean show)484 private void showCallHistory(final boolean show) { 485 // Bail early if the visibility has not changed. 486 if ((show && mSubjectList.getVisibility() == View.VISIBLE) || 487 (!show && mSubjectList.getVisibility() == View.GONE)) { 488 return; 489 } 490 491 final int dialogStartingBottom = mDialogView.getBottom(); 492 if (show) { 493 // Showing the subject list; bind the list of history items to the list and show it. 494 ArrayAdapter<String> adapter = new ArrayAdapter<String>(CallSubjectDialog.this, 495 R.layout.call_subject_history_list_item, mSubjectHistory); 496 mSubjectList.setAdapter(adapter); 497 mSubjectList.setVisibility(View.VISIBLE); 498 } else { 499 // Hiding the subject list. 500 mSubjectList.setVisibility(View.GONE); 501 } 502 503 // Use a ViewTreeObserver so that we can animate between the pre-layout and post-layout 504 // states. 505 final ViewTreeObserver observer = mBackgroundView.getViewTreeObserver(); 506 observer.addOnPreDrawListener( 507 new ViewTreeObserver.OnPreDrawListener() { 508 @Override 509 public boolean onPreDraw() { 510 // We don't want to continue getting called. 511 if (observer.isAlive()) { 512 observer.removeOnPreDrawListener(this); 513 } 514 515 // Determine the amount the dialog has shifted due to the relayout. 516 int shiftAmount = dialogStartingBottom - mDialogView.getBottom(); 517 518 // If the dialog needs to be shifted, do that now. 519 if (shiftAmount != 0) { 520 // Start animation in translated state and animate to translationY 0. 521 mDialogView.setTranslationY(shiftAmount); 522 mDialogView.animate() 523 .translationY(0) 524 .setInterpolator(AnimUtils.EASE_OUT_EASE_IN) 525 .setDuration(mAnimationDuration) 526 .start(); 527 } 528 529 if (show) { 530 // Show the subhect list. 531 mSubjectList.setTranslationY(mSubjectList.getHeight()); 532 533 mSubjectList.animate() 534 .translationY(0) 535 .setInterpolator(AnimUtils.EASE_OUT_EASE_IN) 536 .setDuration(mAnimationDuration) 537 .setListener(new AnimatorListenerAdapter() { 538 @Override 539 public void onAnimationEnd(Animator animation) { 540 super.onAnimationEnd(animation); 541 } 542 543 @Override 544 public void onAnimationStart(Animator animation) { 545 super.onAnimationStart(animation); 546 mSubjectList.setVisibility(View.VISIBLE); 547 } 548 }) 549 .start(); 550 } else { 551 // Hide the subject list. 552 mSubjectList.setTranslationY(0); 553 554 mSubjectList.animate() 555 .translationY(mSubjectList.getHeight()) 556 .setInterpolator(AnimUtils.EASE_OUT_EASE_IN) 557 .setDuration(mAnimationDuration) 558 .setListener(new AnimatorListenerAdapter() { 559 @Override 560 public void onAnimationEnd(Animator animation) { 561 super.onAnimationEnd(animation); 562 mSubjectList.setVisibility(View.GONE); 563 } 564 565 @Override 566 public void onAnimationStart(Animator animation) { 567 super.onAnimationStart(animation); 568 } 569 }) 570 .start(); 571 } 572 return true; 573 } 574 } 575 ); 576 } 577 578 /** 579 * Loads the message encoding and maximum message length from the phone account extras for the 580 * current phone account. 581 */ loadConfiguration()582 private void loadConfiguration() { 583 // Only attempt to load configuration from the phone account extras if the SDK is N or 584 // later. If we've got a prior SDK the default encoding and message length will suffice. 585 int sdk = android.os.Build.VERSION.SDK_INT; 586 if(sdk <= android.os.Build.VERSION_CODES.M) { 587 return; 588 } 589 590 if (mPhoneAccountHandle == null) { 591 return; 592 } 593 594 TelecomManager telecomManager = 595 (TelecomManager) getSystemService(Context.TELECOM_SERVICE); 596 final PhoneAccount account = telecomManager.getPhoneAccount(mPhoneAccountHandle); 597 598 Bundle phoneAccountExtras = PhoneAccountSdkCompat.getExtras(account); 599 if (phoneAccountExtras == null) { 600 return; 601 } 602 603 // Get limit, if provided; otherwise default to existing value. 604 mLimit = phoneAccountExtras 605 .getInt(PhoneAccountSdkCompat.EXTRA_CALL_SUBJECT_MAX_LENGTH, mLimit); 606 607 // Get charset; default to none (e.g. count characters 1:1). 608 String charsetName = phoneAccountExtras.getString( 609 PhoneAccountSdkCompat.EXTRA_CALL_SUBJECT_CHARACTER_ENCODING); 610 611 if (!TextUtils.isEmpty(charsetName)) { 612 try { 613 mMessageEncoding = Charset.forName(charsetName); 614 } catch (java.nio.charset.UnsupportedCharsetException uce) { 615 // Character set was invalid; log warning and fallback to none. 616 Log.w(TAG, "Invalid charset: " + charsetName); 617 mMessageEncoding = null; 618 } 619 } else { 620 // No character set specified, so count characters 1:1. 621 mMessageEncoding = null; 622 } 623 } 624 } 625