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