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.editor; 18 19 import com.android.contacts.R; 20 import com.android.contacts.common.model.RawContactDelta; 21 import com.android.contacts.common.model.RawContactModifier; 22 import com.android.contacts.common.model.ValuesDelta; 23 import com.android.contacts.common.model.account.AccountType; 24 import com.android.contacts.common.model.dataitem.DataKind; 25 26 import android.content.Context; 27 import android.database.Cursor; 28 import android.provider.ContactsContract.CommonDataKinds.Event; 29 import android.provider.ContactsContract.CommonDataKinds.GroupMembership; 30 import android.provider.ContactsContract.CommonDataKinds.Nickname; 31 import android.provider.ContactsContract.CommonDataKinds.StructuredName; 32 import android.util.AttributeSet; 33 import android.view.LayoutInflater; 34 import android.view.View; 35 import android.view.ViewGroup; 36 import android.widget.ImageView; 37 import android.widget.LinearLayout; 38 import android.widget.TextView; 39 40 import java.util.ArrayList; 41 import java.util.List; 42 43 /** 44 * Version of {@link KindSectionView} that supports multiple RawContactDeltas. 45 */ 46 public class CompactKindSectionView extends LinearLayout { 47 48 /** 49 * Marks a name as super primary when it is changed. 50 * 51 * This is for the case when two or more raw contacts with names are joined where neither is 52 * marked as super primary. 53 */ 54 private static final class StructuredNameEditorListener implements Editor.EditorListener { 55 56 private final ValuesDelta mValuesDelta; 57 private final long mRawContactId; 58 private final CompactRawContactsEditorView.Listener mListener; 59 StructuredNameEditorListener(ValuesDelta valuesDelta, long rawContactId, CompactRawContactsEditorView.Listener listener)60 public StructuredNameEditorListener(ValuesDelta valuesDelta, long rawContactId, 61 CompactRawContactsEditorView.Listener listener) { 62 mValuesDelta = valuesDelta; 63 mRawContactId = rawContactId; 64 mListener = listener; 65 } 66 67 @Override onRequest(int request)68 public void onRequest(int request) { 69 if (request == Editor.EditorListener.FIELD_CHANGED) { 70 mValuesDelta.setSuperPrimary(true); 71 if (mListener != null) { 72 mListener.onNameFieldChanged(mRawContactId, mValuesDelta); 73 } 74 } else if (request == Editor.EditorListener.FIELD_TURNED_EMPTY) { 75 mValuesDelta.setSuperPrimary(false); 76 } 77 } 78 79 @Override onDeleteRequested(Editor editor)80 public void onDeleteRequested(Editor editor) { 81 editor.clearAllFields(); 82 } 83 } 84 85 /** 86 * Clears fields when deletes are requested (on phonetic and nickename fields); 87 * does not change the number of editors. 88 */ 89 private static final class OtherNameKindEditorListener implements Editor.EditorListener { 90 91 @Override onRequest(int request)92 public void onRequest(int request) { 93 } 94 95 @Override onDeleteRequested(Editor editor)96 public void onDeleteRequested(Editor editor) { 97 editor.clearAllFields(); 98 } 99 } 100 101 /** 102 * Updates empty fields when fields are deleted or turns empty. 103 * Whether a new empty editor is added is controlled by {@link #setShowOneEmptyEditor} and 104 * {@link #setHideWhenEmpty}. 105 */ 106 private class NonNameEditorListener implements Editor.EditorListener { 107 108 @Override onRequest(int request)109 public void onRequest(int request) { 110 // If a field has become empty or non-empty, then check if another row 111 // can be added dynamically. 112 if (request == FIELD_TURNED_EMPTY || request == FIELD_TURNED_NON_EMPTY) { 113 updateEmptyEditors(/* shouldAnimate = */ true); 114 } 115 } 116 117 @Override onDeleteRequested(Editor editor)118 public void onDeleteRequested(Editor editor) { 119 if (mShowOneEmptyEditor && mEditors.getChildCount() == 1) { 120 // If there is only 1 editor in the section, then don't allow the user to 121 // delete it. Just clear the fields in the editor. 122 editor.clearAllFields(); 123 } else { 124 editor.deleteEditor(); 125 } 126 } 127 } 128 129 private class EventEditorListener extends NonNameEditorListener { 130 131 @Override onRequest(int request)132 public void onRequest(int request) { 133 super.onRequest(request); 134 } 135 136 @Override onDeleteRequested(Editor editor)137 public void onDeleteRequested(Editor editor) { 138 if (editor instanceof EventFieldEditorView){ 139 final EventFieldEditorView delView = (EventFieldEditorView) editor; 140 if (delView.isBirthdayType() && mEditors.getChildCount() > 1) { 141 final EventFieldEditorView bottomView = (EventFieldEditorView) mEditors 142 .getChildAt(mEditors.getChildCount() - 1); 143 bottomView.restoreBirthday(); 144 } 145 } 146 super.onDeleteRequested(editor); 147 } 148 } 149 150 private KindSectionDataList mKindSectionDataList; 151 private ViewIdGenerator mViewIdGenerator; 152 private CompactRawContactsEditorView.Listener mListener; 153 154 private boolean mIsUserProfile; 155 private boolean mShowOneEmptyEditor = false; 156 private boolean mHideIfEmpty = true; 157 158 private LayoutInflater mLayoutInflater; 159 private ViewGroup mEditors; 160 private ImageView mIcon; 161 CompactKindSectionView(Context context)162 public CompactKindSectionView(Context context) { 163 this(context, /* attrs =*/ null); 164 } 165 CompactKindSectionView(Context context, AttributeSet attrs)166 public CompactKindSectionView(Context context, AttributeSet attrs) { 167 super(context, attrs); 168 } 169 170 @Override setEnabled(boolean enabled)171 public void setEnabled(boolean enabled) { 172 super.setEnabled(enabled); 173 if (mEditors != null) { 174 int childCount = mEditors.getChildCount(); 175 for (int i = 0; i < childCount; i++) { 176 mEditors.getChildAt(i).setEnabled(enabled); 177 } 178 } 179 } 180 181 @Override onFinishInflate()182 protected void onFinishInflate() { 183 setDrawingCacheEnabled(true); 184 setAlwaysDrawnWithCacheEnabled(true); 185 186 mLayoutInflater = (LayoutInflater) getContext().getSystemService( 187 Context.LAYOUT_INFLATER_SERVICE); 188 189 mEditors = (ViewGroup) findViewById(R.id.kind_editors); 190 mIcon = (ImageView) findViewById(R.id.kind_icon); 191 } 192 setIsUserProfile(boolean isUserProfile)193 public void setIsUserProfile(boolean isUserProfile) { 194 mIsUserProfile = isUserProfile; 195 } 196 197 /** 198 * @param showOneEmptyEditor If true, we will always show one empty editor, otherwise an empty 199 * editor will not be shown until the user enters a value. Note, this does not apply 200 * to name editors since those are always displayed. 201 */ setShowOneEmptyEditor(boolean showOneEmptyEditor)202 public void setShowOneEmptyEditor(boolean showOneEmptyEditor) { 203 mShowOneEmptyEditor = showOneEmptyEditor; 204 } 205 206 /** 207 * @param hideWhenEmpty If true, the entire section will be hidden if all inputs are empty, 208 * otherwise one empty input will always be displayed. Note, this does not apply 209 * to name editors since those are always displayed. 210 */ setHideWhenEmpty(boolean hideWhenEmpty)211 public void setHideWhenEmpty(boolean hideWhenEmpty) { 212 mHideIfEmpty = hideWhenEmpty; 213 } 214 215 /** Binds the given group data to every {@link GroupMembershipView}. */ setGroupMetaData(Cursor cursor)216 public void setGroupMetaData(Cursor cursor) { 217 for (int i = 0; i < mEditors.getChildCount(); i++) { 218 final View view = mEditors.getChildAt(i); 219 if (view instanceof GroupMembershipView) { 220 ((GroupMembershipView) view).setGroupMetaData(cursor); 221 } 222 } 223 } 224 225 /** 226 * Whether this is a name kind section view and all name fields (structured, phonetic, 227 * and nicknames) are empty. 228 */ isEmptyName()229 public boolean isEmptyName() { 230 if (!StructuredName.CONTENT_ITEM_TYPE.equals(mKindSectionDataList.getMimeType())) { 231 return false; 232 } 233 for (int i = 0; i < mEditors.getChildCount(); i++) { 234 final View view = mEditors.getChildAt(i); 235 if (view instanceof Editor) { 236 final Editor editor = (Editor) view; 237 if (!editor.isEmpty()) { 238 return false; 239 } 240 } 241 } 242 return true; 243 } 244 245 /** 246 * Sets the given display name as the structured name as if the user input it, but 247 * without informing editor listeners. 248 */ setName(String displayName)249 public void setName(String displayName) { 250 if (!StructuredName.CONTENT_ITEM_TYPE.equals(mKindSectionDataList.getMimeType())) { 251 return; 252 } 253 for (int i = 0; i < mEditors.getChildCount(); i++) { 254 final View view = mEditors.getChildAt(i); 255 if (view instanceof StructuredNameEditorView) { 256 final StructuredNameEditorView editor = (StructuredNameEditorView) view; 257 258 // Detach listeners since so we don't show suggested aggregations 259 final Editor.EditorListener editorListener = editor.getEditorListener(); 260 editor.setEditorListener(null); 261 262 editor.setDisplayName(displayName); 263 264 // Reattach listeners 265 editor.setEditorListener(editorListener); 266 267 return; 268 } 269 } 270 } 271 getPrimaryNameEditorView()272 public StructuredNameEditorView getPrimaryNameEditorView() { 273 if (!StructuredName.CONTENT_ITEM_TYPE.equals(mKindSectionDataList.getMimeType()) 274 || mEditors.getChildCount() == 0) { 275 return null; 276 } 277 return (StructuredNameEditorView) mEditors.getChildAt(0); 278 } 279 280 /** 281 * Binds views for the given {@link KindSectionData} list. 282 * 283 * We create a structured name and phonetic name editor for each {@link DataKind} with a 284 * {@link StructuredName#CONTENT_ITEM_TYPE} mime type. The number and order of editors are 285 * rendered as they are given to {@link #setState}. 286 * 287 * Empty name editors are never added and at least one structured name editor is always 288 * displayed, even if it is empty. 289 */ setState(KindSectionDataList kindSectionDataList, ViewIdGenerator viewIdGenerator, CompactRawContactsEditorView.Listener listener, ValuesDelta primaryValuesDelta)290 public void setState(KindSectionDataList kindSectionDataList, 291 ViewIdGenerator viewIdGenerator, CompactRawContactsEditorView.Listener listener, 292 ValuesDelta primaryValuesDelta) { 293 mKindSectionDataList = kindSectionDataList; 294 mViewIdGenerator = viewIdGenerator; 295 mListener = listener; 296 297 // Set the icon using the first DataKind 298 final DataKind dataKind = mKindSectionDataList.getDataKind(); 299 if (dataKind != null) { 300 mIcon.setImageDrawable(EditorUiUtils.getMimeTypeDrawable(getContext(), 301 dataKind.mimeType)); 302 if (mIcon.getDrawable() != null) { 303 mIcon.setContentDescription(dataKind.titleRes == -1 || dataKind.titleRes == 0 304 ? "" : getResources().getString(dataKind.titleRes)); 305 } 306 } 307 308 rebuildFromState(primaryValuesDelta); 309 310 updateEmptyEditors(/* shouldAnimate = */ false); 311 } 312 rebuildFromState(ValuesDelta primaryValuesDelta)313 private void rebuildFromState(ValuesDelta primaryValuesDelta) { 314 mEditors.removeAllViews(); 315 316 final String mimeType = mKindSectionDataList.getMimeType(); 317 for (KindSectionData kindSectionData : mKindSectionDataList) { 318 if (StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)) { 319 addNameEditorViews(kindSectionData.getAccountType(), 320 primaryValuesDelta, kindSectionData.getRawContactDelta()); 321 } else if (GroupMembership.CONTENT_ITEM_TYPE.equals(mimeType)) { 322 addGroupEditorView(kindSectionData.getRawContactDelta(), 323 kindSectionData.getDataKind()); 324 } else { 325 final Editor.EditorListener editorListener; 326 if (Nickname.CONTENT_ITEM_TYPE.equals(mimeType)) { 327 editorListener = new OtherNameKindEditorListener(); 328 } else if (Event.CONTENT_ITEM_TYPE.equals(mimeType)) { 329 editorListener = new EventEditorListener(); 330 } else { 331 editorListener = new NonNameEditorListener(); 332 } 333 for (ValuesDelta valuesDelta : kindSectionData.getVisibleValuesDeltas()) { 334 addNonNameEditorView(kindSectionData.getRawContactDelta(), 335 kindSectionData.getDataKind(), valuesDelta, editorListener); 336 } 337 } 338 } 339 } 340 addNameEditorViews(AccountType accountType, ValuesDelta valuesDelta, RawContactDelta rawContactDelta)341 private void addNameEditorViews(AccountType accountType, 342 ValuesDelta valuesDelta, RawContactDelta rawContactDelta) { 343 final boolean readOnly = !accountType.areContactsWritable(); 344 345 if (readOnly) { 346 final View nameView = mLayoutInflater.inflate( 347 R.layout.structured_name_readonly_editor_view, mEditors, 348 /* attachToRoot =*/ false); 349 350 // Display name 351 ((TextView) nameView.findViewById(R.id.display_name)) 352 .setText(valuesDelta.getDisplayName()); 353 354 // Account type info 355 final LinearLayout accountTypeLayout = (LinearLayout) 356 nameView.findViewById(R.id.account_type); 357 accountTypeLayout.setVisibility(View.VISIBLE); 358 ((ImageView) accountTypeLayout.findViewById(R.id.account_type_icon)) 359 .setImageDrawable(accountType.getDisplayIcon(getContext())); 360 ((TextView) accountTypeLayout.findViewById(R.id.account_type_name)) 361 .setText(accountType.getDisplayLabel(getContext())); 362 363 mEditors.addView(nameView); 364 return; 365 } 366 367 // Structured name 368 final StructuredNameEditorView nameView = (StructuredNameEditorView) mLayoutInflater 369 .inflate(R.layout.structured_name_editor_view, mEditors, /* attachToRoot =*/ false); 370 if (!mIsUserProfile) { 371 // Don't set super primary for the me contact 372 nameView.setEditorListener(new StructuredNameEditorListener( 373 valuesDelta, rawContactDelta.getRawContactId(), mListener)); 374 } 375 nameView.setDeletable(false); 376 nameView.setValues( 377 accountType.getKindForMimetype(DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME), 378 valuesDelta, rawContactDelta, /* readOnly =*/ false, mViewIdGenerator); 379 380 // Correct start margin since there is a second icon in the structured name layout 381 nameView.findViewById(R.id.kind_icon).setVisibility(View.GONE); 382 mEditors.addView(nameView); 383 384 // Phonetic name 385 final PhoneticNameEditorView phoneticNameView = (PhoneticNameEditorView) mLayoutInflater 386 .inflate(R.layout.phonetic_name_editor_view, mEditors, /* attachToRoot =*/ false); 387 phoneticNameView.setEditorListener(new OtherNameKindEditorListener()); 388 phoneticNameView.setDeletable(false); 389 phoneticNameView.setValues( 390 accountType.getKindForMimetype(DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME), 391 valuesDelta, rawContactDelta, /* readOnly =*/ false, mViewIdGenerator); 392 393 // Fix the start margin for phonetic name views 394 final LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams( 395 LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT); 396 layoutParams.setMargins(0, 0, 0, 0); 397 phoneticNameView.setLayoutParams(layoutParams); 398 mEditors.addView(phoneticNameView); 399 } 400 addGroupEditorView(RawContactDelta rawContactDelta, DataKind dataKind)401 private void addGroupEditorView(RawContactDelta rawContactDelta, DataKind dataKind) { 402 final GroupMembershipView view = (GroupMembershipView) mLayoutInflater.inflate( 403 R.layout.item_group_membership, mEditors, /* attachToRoot =*/ false); 404 view.setKind(dataKind); 405 view.setEnabled(isEnabled()); 406 view.setState(rawContactDelta); 407 408 // Correct start margin since there is a second icon in the group layout 409 view.findViewById(R.id.kind_icon).setVisibility(View.GONE); 410 411 mEditors.addView(view); 412 } 413 addNonNameEditorView(RawContactDelta rawContactDelta, DataKind dataKind, ValuesDelta valuesDelta, Editor.EditorListener editorListener)414 private View addNonNameEditorView(RawContactDelta rawContactDelta, DataKind dataKind, 415 ValuesDelta valuesDelta, Editor.EditorListener editorListener) { 416 // Inflate the layout 417 final View view = mLayoutInflater.inflate( 418 EditorUiUtils.getLayoutResourceId(dataKind.mimeType), mEditors, false); 419 view.setEnabled(isEnabled()); 420 if (view instanceof Editor) { 421 final Editor editor = (Editor) view; 422 editor.setDeletable(true); 423 editor.setEditorListener(editorListener); 424 editor.setValues(dataKind, valuesDelta, rawContactDelta, !dataKind.editable, 425 mViewIdGenerator); 426 } 427 mEditors.addView(view); 428 429 return view; 430 } 431 432 /** 433 * Updates the editors being displayed to the user removing extra empty 434 * {@link Editor}s, so there is only max 1 empty {@link Editor} view at a time. 435 * If there is only 1 empty editor and {@link #setHideWhenEmpty} was set to true, 436 * then the entire section is hidden. 437 */ updateEmptyEditors(boolean shouldAnimate)438 public void updateEmptyEditors(boolean shouldAnimate) { 439 final boolean isNameKindSection = StructuredName.CONTENT_ITEM_TYPE.equals( 440 mKindSectionDataList.getMimeType()); 441 final boolean isGroupKindSection = GroupMembership.CONTENT_ITEM_TYPE.equals( 442 mKindSectionDataList.getMimeType()); 443 444 if (isNameKindSection) { 445 // The name kind section is always visible 446 setVisibility(VISIBLE); 447 updateEmptyNameEditors(shouldAnimate); 448 } else if (isGroupKindSection) { 449 // Check whether metadata has been bound for all group views 450 for (int i = 0; i < mEditors.getChildCount(); i++) { 451 final View view = mEditors.getChildAt(i); 452 if (view instanceof GroupMembershipView) { 453 final GroupMembershipView groupView = (GroupMembershipView) view; 454 if (!groupView.wasGroupMetaDataBound() || !groupView.accountHasGroups()) { 455 setVisibility(GONE); 456 return; 457 } 458 } 459 } 460 // Check that the user has selected to display all fields 461 if (mHideIfEmpty) { 462 setVisibility(GONE); 463 return; 464 } 465 setVisibility(VISIBLE); 466 467 // We don't check the emptiness of the group views 468 } else { 469 // Determine if the entire kind section should be visible 470 final int editorCount = mEditors.getChildCount(); 471 final List<View> emptyEditors = getEmptyEditors(); 472 if (editorCount == emptyEditors.size() && mHideIfEmpty) { 473 setVisibility(GONE); 474 return; 475 } 476 setVisibility(VISIBLE); 477 478 updateEmptyNonNameEditors(shouldAnimate); 479 } 480 } 481 updateEmptyNameEditors(boolean shouldAnimate)482 private void updateEmptyNameEditors(boolean shouldAnimate) { 483 boolean isEmptyNameEditorVisible = false; 484 485 for (int i = 0; i < mEditors.getChildCount(); i++) { 486 final View view = mEditors.getChildAt(i); 487 if (view instanceof Editor) { 488 final Editor editor = (Editor) view; 489 if (view instanceof StructuredNameEditorView) { 490 // We always show one empty structured name view 491 if (editor.isEmpty()) { 492 if (isEmptyNameEditorVisible) { 493 // If we're already showing an empty editor then hide any other empties 494 if (mHideIfEmpty) { 495 view.setVisibility(View.GONE); 496 } 497 } else { 498 isEmptyNameEditorVisible = true; 499 } 500 } else { 501 showView(view, shouldAnimate); 502 isEmptyNameEditorVisible = true; 503 } 504 } else { 505 // Since we can't add phonetic names and nicknames, just show or hide them 506 if (mHideIfEmpty && editor.isEmpty()) { 507 hideView(view); 508 } else { 509 showView(view, /* shouldAnimate =*/ false); // Animation here causes jank 510 } 511 } 512 } else { 513 // For read only names, only show them if we're not hiding empty views 514 if (mHideIfEmpty) { 515 hideView(view); 516 } else { 517 showView(view, shouldAnimate); 518 } 519 } 520 } 521 } 522 updateEmptyNonNameEditors(boolean shouldAnimate)523 private void updateEmptyNonNameEditors(boolean shouldAnimate) { 524 // Prune excess empty editors 525 final List<View> emptyEditors = getEmptyEditors(); 526 if (emptyEditors.size() > 1) { 527 // If there is more than 1 empty editor, then remove it from the list of editors. 528 int deleted = 0; 529 for (final View view : emptyEditors) { 530 // If no child {@link View}s are being focused on within this {@link View}, then 531 // remove this empty editor. We can assume that at least one empty editor has 532 // focus. One way to get two empty editors is by deleting characters from a 533 // non-empty editor, in which case this editor has focus. Another way is if 534 // there is more values delta so we must also count number of editors deleted. 535 if (view.findFocus() == null) { 536 deleteView(view, shouldAnimate); 537 deleted++; 538 if (deleted == emptyEditors.size() - 1) break; 539 } 540 } 541 return; 542 } 543 // Determine if we should add a new empty editor 544 final DataKind dataKind = mKindSectionDataList.get(0).getDataKind(); 545 final RawContactDelta rawContactDelta = 546 mKindSectionDataList.get(0).getRawContactDelta(); 547 if (dataKind == null // There is nothing we can do. 548 // We have already reached the maximum number of editors, don't add any more. 549 || !RawContactModifier.canInsert(rawContactDelta, dataKind) 550 // We have already reached the maximum number of empty editors, don't add any more. 551 || emptyEditors.size() == 1) { 552 return; 553 } 554 // Add a new empty editor 555 if (mShowOneEmptyEditor) { 556 final String mimeType = mKindSectionDataList.getMimeType(); 557 if (Nickname.CONTENT_ITEM_TYPE.equals(mimeType) && mEditors.getChildCount() > 0) { 558 return; 559 } 560 final ValuesDelta values = RawContactModifier.insertChild(rawContactDelta, dataKind); 561 final Editor.EditorListener editorListener = Event.CONTENT_ITEM_TYPE.equals(mimeType) 562 ? new EventEditorListener() : new NonNameEditorListener(); 563 final View view = addNonNameEditorView(rawContactDelta, dataKind, values, 564 editorListener); 565 showView(view, shouldAnimate); 566 } 567 } 568 hideView(View view)569 private void hideView(View view) { 570 view.setVisibility(View.GONE); 571 } 572 deleteView(View view, boolean shouldAnimate)573 private void deleteView(View view, boolean shouldAnimate) { 574 if (shouldAnimate) { 575 final Editor editor = (Editor) view; 576 editor.deleteEditor(); 577 } else { 578 mEditors.removeView(view); 579 } 580 } 581 showView(View view, boolean shouldAnimate)582 private void showView(View view, boolean shouldAnimate) { 583 if (shouldAnimate) { 584 view.setVisibility(View.GONE); 585 EditorAnimator.getInstance().showFieldFooter(view); 586 } else { 587 view.setVisibility(View.VISIBLE); 588 } 589 } 590 getEmptyEditors()591 private List<View> getEmptyEditors() { 592 final List<View> emptyEditors = new ArrayList<>(); 593 for (int i = 0; i < mEditors.getChildCount(); i++) { 594 final View view = mEditors.getChildAt(i); 595 if (view instanceof Editor && ((Editor) view).isEmpty()) { 596 emptyEditors.add(view); 597 } 598 } 599 return emptyEditors; 600 } 601 } 602