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 17package com.android.contacts.editor; 18 19import android.content.Context; 20import android.database.Cursor; 21import android.provider.ContactsContract.CommonDataKinds.Event; 22import android.provider.ContactsContract.CommonDataKinds.GroupMembership; 23import android.provider.ContactsContract.CommonDataKinds.Nickname; 24import android.provider.ContactsContract.CommonDataKinds.StructuredName; 25import android.util.AttributeSet; 26import android.view.LayoutInflater; 27import android.view.View; 28import android.view.ViewGroup; 29import android.widget.ImageView; 30import android.widget.LinearLayout; 31import android.widget.TextView; 32 33import com.android.contacts.R; 34import com.android.contacts.model.RawContactDelta; 35import com.android.contacts.model.RawContactModifier; 36import com.android.contacts.model.ValuesDelta; 37import com.android.contacts.model.account.AccountType; 38import com.android.contacts.model.dataitem.DataKind; 39import com.android.contacts.preference.ContactsPreferences; 40 41import java.util.ArrayList; 42import 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 */ 49public 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 63 public StructuredNameEditorListener(ValuesDelta valuesDelta, long rawContactId, 64 RawContactEditorView.Listener listener) { 65 mValuesDelta = valuesDelta; 66 mRawContactId = rawContactId; 67 mListener = listener; 68 } 69 70 @Override 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 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 95 public void onRequest(int request) { 96 } 97 98 @Override 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 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 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 135 public void onRequest(int request) { 136 super.onRequest(request); 137 } 138 139 @Override 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 165 public KindSectionView(Context context) { 166 this(context, /* attrs =*/ null); 167 } 168 169 public KindSectionView(Context context, AttributeSet attrs) { 170 super(context, attrs); 171 } 172 173 @Override 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 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 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 */ 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 */ 215 public void setHideWhenEmpty(boolean hideWhenEmpty) { 216 mHideIfEmpty = hideWhenEmpty; 217 } 218 219 /** Binds the given group data to every {@link GroupMembershipView}. */ 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 */ 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 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 257 /** 258 * Binds views for the given {@link KindSectionData}. 259 * 260 * We create a structured name and phonetic name editor for each {@link DataKind} with a 261 * {@link StructuredName#CONTENT_ITEM_TYPE} mime type. The number and order of editors are 262 * rendered as they are given to {@link #setState}. 263 * 264 * Empty name editors are never added and at least one structured name editor is always 265 * displayed, even if it is empty. 266 */ 267 public void setState(KindSectionData kindSectionData, 268 ViewIdGenerator viewIdGenerator, RawContactEditorView.Listener listener) { 269 mKindSectionData = kindSectionData; 270 mViewIdGenerator = viewIdGenerator; 271 mListener = listener; 272 273 // Set the icon using the DataKind 274 final DataKind dataKind = mKindSectionData.getDataKind(); 275 if (dataKind != null) { 276 mIcon.setImageDrawable(EditorUiUtils.getMimeTypeDrawable(getContext(), 277 dataKind.mimeType)); 278 if (mIcon.getDrawable() != null) { 279 mIcon.setContentDescription(dataKind.titleRes == -1 || dataKind.titleRes == 0 280 ? "" : getResources().getString(dataKind.titleRes)); 281 } 282 } 283 284 rebuildFromState(); 285 286 updateEmptyEditors(/* shouldAnimate = */ false); 287 } 288 289 private void rebuildFromState() { 290 mEditors.removeAllViews(); 291 292 final String mimeType = mKindSectionData.getMimeType(); 293 if (StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)) { 294 addNameEditorViews(mKindSectionData.getAccountType(), 295 mKindSectionData.getRawContactDelta()); 296 } else if (GroupMembership.CONTENT_ITEM_TYPE.equals(mimeType)) { 297 addGroupEditorView(mKindSectionData.getRawContactDelta(), 298 mKindSectionData.getDataKind()); 299 } else { 300 final Editor.EditorListener editorListener; 301 if (Nickname.CONTENT_ITEM_TYPE.equals(mimeType)) { 302 editorListener = new OtherNameKindEditorListener(); 303 } else if (Event.CONTENT_ITEM_TYPE.equals(mimeType)) { 304 editorListener = new EventEditorListener(); 305 } else { 306 editorListener = new NonNameEditorListener(); 307 } 308 final List<ValuesDelta> valuesDeltas = mKindSectionData.getVisibleValuesDeltas(); 309 for (int i = 0; i < valuesDeltas.size(); i++ ) { 310 addNonNameEditorView(mKindSectionData.getRawContactDelta(), 311 mKindSectionData.getDataKind(), valuesDeltas.get(i), editorListener); 312 } 313 } 314 } 315 316 private void addNameEditorViews(AccountType accountType, RawContactDelta rawContactDelta) { 317 final boolean readOnly = !accountType.areContactsWritable(); 318 final ValuesDelta nameValuesDelta = rawContactDelta 319 .getSuperPrimaryEntry(StructuredName.CONTENT_ITEM_TYPE); 320 321 if (readOnly) { 322 final View nameView = mLayoutInflater.inflate( 323 R.layout.structured_name_readonly_editor_view, mEditors, 324 /* attachToRoot =*/ false); 325 326 // Display name 327 ((TextView) nameView.findViewById(R.id.display_name)) 328 .setText(nameValuesDelta.getDisplayName()); 329 330 // Account type info 331 final LinearLayout accountTypeLayout = (LinearLayout) 332 nameView.findViewById(R.id.account_type); 333 accountTypeLayout.setVisibility(View.VISIBLE); 334 ((ImageView) accountTypeLayout.findViewById(R.id.account_type_icon)) 335 .setImageDrawable(accountType.getDisplayIcon(getContext())); 336 ((TextView) accountTypeLayout.findViewById(R.id.account_type_name)) 337 .setText(accountType.getDisplayLabel(getContext())); 338 339 mEditors.addView(nameView); 340 return; 341 } 342 343 // Structured name 344 final StructuredNameEditorView nameView = (StructuredNameEditorView) mLayoutInflater 345 .inflate(R.layout.structured_name_editor_view, mEditors, /* attachToRoot =*/ false); 346 if (!mIsUserProfile) { 347 // Don't set super primary for the me contact 348 nameView.setEditorListener(new StructuredNameEditorListener( 349 nameValuesDelta, rawContactDelta.getRawContactId(), mListener)); 350 } 351 nameView.setDeletable(false); 352 nameView.setValues(accountType.getKindForMimetype(DataKind.PSEUDO_MIME_TYPE_NAME), 353 nameValuesDelta, rawContactDelta, /* readOnly =*/ false, mViewIdGenerator); 354 355 // Correct start margin since there is a second icon in the structured name layout 356 nameView.findViewById(R.id.kind_icon).setVisibility(View.GONE); 357 mEditors.addView(nameView); 358 359 // Phonetic name 360 final DataKind phoneticNameKind = accountType 361 .getKindForMimetype(DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME); 362 // The account type doesn't support phonetic name. 363 if (phoneticNameKind == null) return; 364 365 final TextFieldsEditorView phoneticNameView = (TextFieldsEditorView) mLayoutInflater 366 .inflate(R.layout.text_fields_editor_view, mEditors, /* attachToRoot =*/ false); 367 phoneticNameView.setEditorListener(new OtherNameKindEditorListener()); 368 phoneticNameView.setDeletable(false); 369 phoneticNameView.setValues( 370 accountType.getKindForMimetype(DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME), 371 nameValuesDelta, rawContactDelta, /* readOnly =*/ false, mViewIdGenerator); 372 373 // Fix the start margin for phonetic name views 374 final LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams( 375 LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT); 376 layoutParams.setMargins(0, 0, 0, 0); 377 phoneticNameView.setLayoutParams(layoutParams); 378 mEditors.addView(phoneticNameView); 379 // Display of phonetic name fields is controlled from settings preferences. 380 mHideIfEmpty = new ContactsPreferences(getContext()).shouldHidePhoneticNamesIfEmpty(); 381 } 382 383 private void addGroupEditorView(RawContactDelta rawContactDelta, DataKind dataKind) { 384 final GroupMembershipView view = (GroupMembershipView) mLayoutInflater.inflate( 385 R.layout.item_group_membership, mEditors, /* attachToRoot =*/ false); 386 view.setKind(dataKind); 387 view.setEnabled(isEnabled()); 388 view.setState(rawContactDelta); 389 390 // Correct start margin since there is a second icon in the group layout 391 view.findViewById(R.id.kind_icon).setVisibility(View.GONE); 392 393 mEditors.addView(view); 394 } 395 396 private View addNonNameEditorView(RawContactDelta rawContactDelta, DataKind dataKind, 397 ValuesDelta valuesDelta, Editor.EditorListener editorListener) { 398 // Inflate the layout 399 final View view = mLayoutInflater.inflate( 400 EditorUiUtils.getLayoutResourceId(dataKind.mimeType), mEditors, false); 401 view.setEnabled(isEnabled()); 402 if (view instanceof Editor) { 403 final Editor editor = (Editor) view; 404 editor.setDeletable(true); 405 editor.setEditorListener(editorListener); 406 editor.setValues(dataKind, valuesDelta, rawContactDelta, !dataKind.editable, 407 mViewIdGenerator); 408 } 409 mEditors.addView(view); 410 411 return view; 412 } 413 414 /** 415 * Updates the editors being displayed to the user removing extra empty 416 * {@link Editor}s, so there is only max 1 empty {@link Editor} view at a time. 417 * If there is only 1 empty editor and {@link #setHideWhenEmpty} was set to true, 418 * then the entire section is hidden. 419 */ 420 public void updateEmptyEditors(boolean shouldAnimate) { 421 final boolean isNameKindSection = StructuredName.CONTENT_ITEM_TYPE.equals( 422 mKindSectionData.getMimeType()); 423 final boolean isGroupKindSection = GroupMembership.CONTENT_ITEM_TYPE.equals( 424 mKindSectionData.getMimeType()); 425 426 if (isNameKindSection) { 427 // The name kind section is always visible 428 setVisibility(VISIBLE); 429 updateEmptyNameEditors(shouldAnimate); 430 } else if (isGroupKindSection) { 431 // Check whether metadata has been bound for all group views 432 for (int i = 0; i < mEditors.getChildCount(); i++) { 433 final View view = mEditors.getChildAt(i); 434 if (view instanceof GroupMembershipView) { 435 final GroupMembershipView groupView = (GroupMembershipView) view; 436 if (!groupView.wasGroupMetaDataBound() || !groupView.accountHasGroups()) { 437 setVisibility(GONE); 438 return; 439 } 440 } 441 } 442 // Check that the user has selected to display all fields 443 if (mHideIfEmpty) { 444 setVisibility(GONE); 445 return; 446 } 447 setVisibility(VISIBLE); 448 449 // We don't check the emptiness of the group views 450 } else { 451 // Determine if the entire kind section should be visible 452 final int editorCount = mEditors.getChildCount(); 453 final List<View> emptyEditors = getEmptyEditors(); 454 if (editorCount == emptyEditors.size() && mHideIfEmpty) { 455 setVisibility(GONE); 456 return; 457 } 458 setVisibility(VISIBLE); 459 460 updateEmptyNonNameEditors(shouldAnimate); 461 } 462 } 463 464 private void updateEmptyNameEditors(boolean shouldAnimate) { 465 boolean isEmptyNameEditorVisible = false; 466 467 for (int i = 0; i < mEditors.getChildCount(); i++) { 468 final View view = mEditors.getChildAt(i); 469 if (view instanceof Editor) { 470 final Editor editor = (Editor) view; 471 if (view instanceof StructuredNameEditorView) { 472 // We always show one empty structured name view 473 if (editor.isEmpty()) { 474 if (isEmptyNameEditorVisible) { 475 // If we're already showing an empty editor then hide any other empties 476 if (mHideIfEmpty) { 477 view.setVisibility(View.GONE); 478 } 479 } else { 480 isEmptyNameEditorVisible = true; 481 } 482 } else { 483 showView(view, shouldAnimate); 484 isEmptyNameEditorVisible = true; 485 } 486 } else { 487 // Since we can't add phonetic names and nicknames, just show or hide them 488 if (mHideIfEmpty && editor.isEmpty()) { 489 hideView(view); 490 } else { 491 showView(view, /* shouldAnimate =*/ false); // Animation here causes jank 492 } 493 } 494 } else { 495 // For read only names, only show them if we're not hiding empty views 496 if (mHideIfEmpty) { 497 hideView(view); 498 } else { 499 showView(view, shouldAnimate); 500 } 501 } 502 } 503 } 504 505 private void updateEmptyNonNameEditors(boolean shouldAnimate) { 506 // Prune excess empty editors 507 final List<View> emptyEditors = getEmptyEditors(); 508 if (emptyEditors.size() > 1) { 509 // If there is more than 1 empty editor, then remove it from the list of editors. 510 int deleted = 0; 511 for (int i = 0; i < emptyEditors.size(); i++) { 512 final View view = emptyEditors.get(i); 513 // If no child {@link View}s are being focused on within this {@link View}, then 514 // remove this empty editor. We can assume that at least one empty editor has 515 // focus. One way to get two empty editors is by deleting characters from a 516 // non-empty editor, in which case this editor has focus. Another way is if 517 // there is more values delta so we must also count number of editors deleted. 518 if (view.findFocus() == null) { 519 deleteView(view, shouldAnimate); 520 deleted++; 521 if (deleted == emptyEditors.size() - 1) break; 522 } 523 } 524 return; 525 } 526 // Determine if we should add a new empty editor 527 final DataKind dataKind = mKindSectionData.getDataKind(); 528 final RawContactDelta rawContactDelta = mKindSectionData.getRawContactDelta(); 529 if (dataKind == null // There is nothing we can do. 530 // We have already reached the maximum number of editors, don't add any more. 531 || !RawContactModifier.canInsert(rawContactDelta, dataKind) 532 // We have already reached the maximum number of empty editors, don't add any more. 533 || emptyEditors.size() == 1) { 534 return; 535 } 536 // Add a new empty editor 537 if (mShowOneEmptyEditor) { 538 final String mimeType = mKindSectionData.getMimeType(); 539 if (Nickname.CONTENT_ITEM_TYPE.equals(mimeType) && mEditors.getChildCount() > 0) { 540 return; 541 } 542 final ValuesDelta values = RawContactModifier.insertChild(rawContactDelta, dataKind); 543 final Editor.EditorListener editorListener = Event.CONTENT_ITEM_TYPE.equals(mimeType) 544 ? new EventEditorListener() : new NonNameEditorListener(); 545 final View view = addNonNameEditorView(rawContactDelta, dataKind, values, 546 editorListener); 547 showView(view, shouldAnimate); 548 } 549 } 550 551 private void hideView(View view) { 552 view.setVisibility(View.GONE); 553 } 554 555 private void deleteView(View view, boolean shouldAnimate) { 556 if (shouldAnimate) { 557 final Editor editor = (Editor) view; 558 editor.deleteEditor(); 559 } else { 560 mEditors.removeView(view); 561 } 562 } 563 564 private void showView(View view, boolean shouldAnimate) { 565 if (shouldAnimate) { 566 view.setVisibility(View.GONE); 567 EditorAnimator.getInstance().showFieldFooter(view); 568 } else { 569 view.setVisibility(View.VISIBLE); 570 } 571 } 572 573 private List<View> getEmptyEditors() { 574 final List<View> emptyEditors = new ArrayList<>(); 575 for (int i = 0; i < mEditors.getChildCount(); i++) { 576 final View view = mEditors.getChildAt(i); 577 if (view instanceof Editor && ((Editor) view).isEmpty()) { 578 emptyEditors.add(view); 579 } 580 } 581 return emptyEditors; 582 } 583} 584