1 /* 2 * Copyright (C) 2010 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.graphics.Rect; 21 import android.graphics.drawable.Drawable; 22 import android.os.Parcel; 23 import android.os.Parcelable; 24 import android.provider.ContactsContract; 25 import android.provider.ContactsContract.CommonDataKinds.Im; 26 import android.provider.ContactsContract.CommonDataKinds.SipAddress; 27 import android.provider.ContactsContract.CommonDataKinds.StructuredName; 28 import android.text.Editable; 29 import android.text.InputType; 30 import android.text.Spannable; 31 import android.text.Spanned; 32 import android.text.TextUtils; 33 import android.text.TextWatcher; 34 import android.text.style.TtsSpan; 35 import android.util.AttributeSet; 36 import android.util.Log; 37 import android.util.TypedValue; 38 import android.view.View; 39 import android.view.ViewGroup; 40 import android.view.inputmethod.EditorInfo; 41 import android.view.inputmethod.InputMethodManager; 42 import android.widget.EditText; 43 import android.widget.ImageView; 44 import android.widget.LinearLayout; 45 46 import com.android.contacts.ContactsUtils; 47 import com.android.contacts.R; 48 import com.android.contacts.compat.PhoneNumberUtilsCompat; 49 import com.android.contacts.model.RawContactDelta; 50 import com.android.contacts.model.ValuesDelta; 51 import com.android.contacts.model.account.AccountType.EditField; 52 import com.android.contacts.model.dataitem.DataKind; 53 import com.android.contacts.util.PhoneNumberFormatter; 54 import com.android.contacts.ClipboardUtils; 55 56 /** 57 * Simple editor that handles labels and any {@link EditField} defined for the 58 * entry. Uses {@link ValuesDelta} to read any existing {@link RawContact} values, 59 * and to correctly write any changes values. 60 */ 61 public class TextFieldsEditorView extends LabeledEditorView { 62 private static final String TAG = TextFieldsEditorView.class.getSimpleName(); 63 private EditText[] mFieldEditTexts = null; 64 private ViewGroup mFields = null; 65 protected View mExpansionViewContainer; 66 protected ImageView mExpansionView; 67 protected String mCollapseButtonDescription; 68 protected String mExpandButtonDescription; 69 protected String mCollapsedAnnouncement; 70 protected String mExpandedAnnouncement; 71 private boolean mHideOptional = true; 72 private boolean mHasShortAndLongForms; 73 private int mMinFieldHeight; 74 private int mPreviousViewHeight; 75 private int mHintTextColorUnfocused; 76 private String mFixedPhonetic = ""; 77 private String mFixedDisplayName = ""; 78 private boolean needInputInitialize; 79 80 private final OnLongClickListener mOnLongClickListener = 81 v -> { 82 ClipboardUtils.copyText( 83 getContext(), /* label= */ null, (CharSequence) v.getTag(R.id.text_to_copy), true); 84 return true; 85 }; 86 TextFieldsEditorView(Context context)87 public TextFieldsEditorView(Context context) { 88 super(context); 89 } 90 TextFieldsEditorView(Context context, AttributeSet attrs)91 public TextFieldsEditorView(Context context, AttributeSet attrs) { 92 super(context, attrs); 93 } 94 TextFieldsEditorView(Context context, AttributeSet attrs, int defStyle)95 public TextFieldsEditorView(Context context, AttributeSet attrs, int defStyle) { 96 super(context, attrs, defStyle); 97 } 98 99 /** {@inheritDoc} */ 100 @Override onFinishInflate()101 protected void onFinishInflate() { 102 super.onFinishInflate(); 103 104 setDrawingCacheEnabled(true); 105 setAlwaysDrawnWithCacheEnabled(true); 106 107 mMinFieldHeight = getContext().getResources().getDimensionPixelSize( 108 R.dimen.editor_min_line_item_height); 109 mFields = (ViewGroup) findViewById(R.id.editors); 110 mHintTextColorUnfocused = getResources().getColor(R.color.editor_disabled_text_color); 111 mExpansionView = (ImageView) findViewById(R.id.expansion_view); 112 mCollapseButtonDescription = getResources() 113 .getString(R.string.collapse_fields_description); 114 mCollapsedAnnouncement = getResources() 115 .getString(R.string.announce_collapsed_fields); 116 mExpandButtonDescription = getResources() 117 .getString(R.string.expand_fields_description); 118 mExpandedAnnouncement = getResources() 119 .getString(R.string.announce_expanded_fields); 120 121 mExpansionViewContainer = findViewById(R.id.expansion_view_container); 122 if (mExpansionViewContainer != null) { 123 mExpansionViewContainer.setOnClickListener(new OnClickListener() { 124 @Override 125 public void onClick(View v) { 126 mPreviousViewHeight = mFields.getHeight(); 127 128 // Save focus 129 final View focusedChild = findFocus(); 130 final int focusedViewId = focusedChild == null ? -1 : focusedChild.getId(); 131 132 // Reconfigure GUI 133 mHideOptional = !mHideOptional; 134 onOptionalFieldVisibilityChange(); 135 rebuildValues(); 136 137 // Restore focus 138 View newFocusView = findViewById(focusedViewId); 139 if (newFocusView == null || newFocusView.getVisibility() == GONE) { 140 // find first visible child 141 newFocusView = TextFieldsEditorView.this; 142 } 143 newFocusView.requestFocus(); 144 145 EditorAnimator.getInstance().slideAndFadeIn(mFields, mPreviousViewHeight); 146 announceForAccessibility(mHideOptional ? 147 mCollapsedAnnouncement : mExpandedAnnouncement); 148 } 149 }); 150 } 151 } 152 153 @Override editNewlyAddedField()154 public void editNewlyAddedField() { 155 // Some editors may have multiple fields (eg: first-name/last-name), but since the user 156 // has not selected a particular one, it is reasonable to simply pick the first. 157 final View editor = mFields.getChildAt(0); 158 159 // Show the soft-keyboard. 160 InputMethodManager imm = 161 (InputMethodManager)getContext().getSystemService(Context.INPUT_METHOD_SERVICE); 162 if (imm != null) { 163 if (!imm.showSoftInput(editor, InputMethodManager.SHOW_IMPLICIT)) { 164 Log.w(TAG, "Failed to show soft input method."); 165 } 166 } 167 } 168 169 @Override setEnabled(boolean enabled)170 public void setEnabled(boolean enabled) { 171 super.setEnabled(enabled); 172 173 if (mFieldEditTexts != null) { 174 for (int index = 0; index < mFieldEditTexts.length; index++) { 175 mFieldEditTexts[index].setEnabled(!isReadOnly() && enabled && !mIsLegacyField); 176 if (mIsLegacyField) { 177 mFieldEditTexts[index].setFocusable(false); 178 mFieldEditTexts[index].setClickable(false); 179 mFieldEditTexts[index].setLongClickable(false); 180 } 181 } 182 if (mIsLegacyField && mFieldEditTexts.length > 0) { 183 setOnLongClickListenerOnContainer(); 184 } 185 } 186 if (mExpansionView != null) { 187 mExpansionView.setEnabled(!isReadOnly() && enabled); 188 } 189 } 190 191 /** 192 * Attaches OnLongClickLister to fields LinearLayout that allow user copy the EditText text on 193 * long press. 194 */ setOnLongClickListenerOnContainer()195 private void setOnLongClickListenerOnContainer() { 196 mFields.setFocusable(true); 197 mFields.setLongClickable(true); 198 // Current legacy mimetypes support exactly 1 field 199 mFields.setTag(R.id.text_to_copy, mFieldEditTexts[0].getText().toString()); 200 mFields.setOnLongClickListener(mOnLongClickListener); 201 } 202 203 private OnFocusChangeListener mTextFocusChangeListener = new OnFocusChangeListener() { 204 @Override 205 public void onFocusChange(View v, boolean hasFocus) { 206 if (getEditorListener() != null) { 207 getEditorListener().onRequest(EditorListener.EDITOR_FOCUS_CHANGED); 208 } 209 // Rebuild the label spinner using the new colors. 210 rebuildLabel(); 211 212 if (hasFocus) { 213 needInputInitialize = true; 214 } 215 } 216 }; 217 218 /** 219 * Creates or removes the type/label button. Doesn't do anything if already correctly configured 220 */ setupExpansionView(boolean shouldExist, boolean collapsed)221 private void setupExpansionView(boolean shouldExist, boolean collapsed) { 222 final Drawable expandIcon = getContext().getDrawable(collapsed 223 ? R.drawable.quantum_ic_expand_more_vd_theme_24 224 : R.drawable.quantum_ic_expand_less_vd_theme_24); 225 mExpansionView.setImageDrawable(expandIcon); 226 mExpansionView.setContentDescription(collapsed ? mExpandButtonDescription 227 : mCollapseButtonDescription); 228 mExpansionViewContainer.setVisibility(shouldExist ? View.VISIBLE : View.INVISIBLE); 229 } 230 231 @Override requestFocusForFirstEditField()232 protected void requestFocusForFirstEditField() { 233 if (mFieldEditTexts != null && mFieldEditTexts.length != 0) { 234 EditText firstField = null; 235 boolean anyFieldHasFocus = false; 236 for (EditText editText : mFieldEditTexts) { 237 if (firstField == null && editText.getVisibility() == View.VISIBLE) { 238 firstField = editText; 239 } 240 if (editText.hasFocus()) { 241 anyFieldHasFocus = true; 242 break; 243 } 244 } 245 if (!anyFieldHasFocus && firstField != null) { 246 firstField.requestFocus(); 247 } 248 } 249 } 250 setValue(int field, String value)251 public void setValue(int field, String value) { 252 mFieldEditTexts[field].setText(value); 253 } 254 isUnFixed(Editable input)255 private boolean isUnFixed(Editable input) { 256 boolean unfixed = false; 257 Object[] spanned = input.getSpans(0, input.length(), Object.class); 258 if (spanned != null) { 259 for (Object obj : spanned) { 260 if ((input.getSpanFlags(obj) & Spanned.SPAN_COMPOSING) == Spanned.SPAN_COMPOSING) { 261 unfixed = true; 262 } 263 } 264 } 265 return unfixed; 266 } 267 getNameField(String column)268 private String getNameField(String column) { 269 270 EditText editText = null; 271 272 if (StructuredName.FAMILY_NAME.equals(column)) { 273 editText = (EditText) mFields.getChildAt(1); 274 } else if (StructuredName.GIVEN_NAME.equals(column)) { 275 editText = (EditText) mFields.getChildAt(3); 276 } else if (StructuredName.MIDDLE_NAME.equals(column)) { 277 editText = (EditText) mFields.getChildAt(2); 278 } 279 280 if (editText != null) { 281 return editText.getText().toString(); 282 } 283 284 return ""; 285 } 286 287 @Override setValues(DataKind kind, ValuesDelta entry, RawContactDelta state, boolean readOnly, ViewIdGenerator vig)288 public void setValues(DataKind kind, ValuesDelta entry, RawContactDelta state, boolean readOnly, 289 ViewIdGenerator vig) { 290 super.setValues(kind, entry, state, readOnly, vig); 291 // Remove edit texts that we currently have 292 if (mFieldEditTexts != null) { 293 for (EditText fieldEditText : mFieldEditTexts) { 294 mFields.removeView(fieldEditText); 295 } 296 } 297 boolean hidePossible = false; 298 299 int fieldCount = kind.fieldList == null ? 0 : kind.fieldList.size(); 300 mFieldEditTexts = new EditText[fieldCount]; 301 for (int index = 0; index < fieldCount; index++) { 302 final EditField field = kind.fieldList.get(index); 303 final EditText fieldView = new EditText(getContext()); 304 fieldView.setLayoutParams(new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, 305 LayoutParams.WRAP_CONTENT)); 306 fieldView.setTextSize(TypedValue.COMPLEX_UNIT_PX, 307 getResources().getDimension(R.dimen.editor_form_text_size)); 308 fieldView.setHintTextColor(mHintTextColorUnfocused); 309 mFieldEditTexts[index] = fieldView; 310 fieldView.setId(vig.getId(state, kind, entry, index)); 311 if (field.titleRes > 0) { 312 fieldView.setHint(field.titleRes); 313 } 314 int inputType = field.inputType; 315 fieldView.setInputType(inputType); 316 if (inputType == InputType.TYPE_CLASS_PHONE) { 317 PhoneNumberFormatter.setPhoneNumberFormattingTextWatcher( 318 getContext(), fieldView, 319 /* formatAfterWatcherSet =*/ state.isContactInsert()); 320 fieldView.setTextDirection(View.TEXT_DIRECTION_LTR); 321 } 322 fieldView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START); 323 324 // Set either a minimum line requirement or a minimum height (because {@link TextView} 325 // only takes one or the other at a single time). 326 if (field.minLines > 1) { 327 fieldView.setMinLines(field.minLines); 328 } else { 329 // This needs to be called after setInputType. Otherwise, calling setInputType 330 // will unset this value. 331 fieldView.setMinHeight(mMinFieldHeight); 332 } 333 334 // Show the "next" button in IME to navigate between text fields 335 // TODO: Still need to properly navigate to/from sections without text fields, 336 // See Bug: 5713510 337 fieldView.setImeOptions(EditorInfo.IME_ACTION_NEXT | EditorInfo.IME_FLAG_NO_FULLSCREEN); 338 339 // Read current value from state 340 final String column = field.column; 341 final String value = entry.getAsString(column); 342 if (ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE.equals(kind.mimeType)) { 343 fieldView.setText(PhoneNumberUtilsCompat.createTtsSpannable(value)); 344 } else { 345 fieldView.setText(value); 346 } 347 348 // Show the delete button if we have a non-empty value 349 setDeleteButtonVisible(!TextUtils.isEmpty(value)); 350 351 // Prepare listener for writing changes 352 fieldView.addTextChangedListener(new TextWatcher() { 353 private int mStart = 0; 354 @Override 355 public void afterTextChanged(Editable s) { 356 // Trigger event for newly changed value 357 onFieldChanged(column, s.toString()); 358 359 if (!DataKind.PSEUDO_MIME_TYPE_NAME.equals(getKind().mimeType)){ 360 return; 361 } 362 363 String displayNameField = s.toString(); 364 365 int nonFixedLen = displayNameField.length() - mFixedDisplayName.length(); 366 if (isUnFixed(s) || nonFixedLen == 0) { 367 String tmpString = mFixedPhonetic 368 + displayNameField.substring(mStart, displayNameField.length()); 369 370 updatePhonetic(column, tmpString); 371 } else { 372 mFixedPhonetic = getPhonetic(column); 373 mFixedDisplayName = displayNameField; 374 } 375 } 376 377 @Override 378 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 379 if (!DataKind.PSEUDO_MIME_TYPE_NAME.equals(getKind().mimeType)){ 380 return; 381 } 382 if (needInputInitialize) { 383 mFixedPhonetic = getPhonetic(column); 384 mFixedDisplayName = getNameField(column); 385 needInputInitialize = false; 386 } 387 } 388 389 @Override 390 public void onTextChanged(CharSequence s, int start, int before, int count) { 391 mStart = start; 392 if (!ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE.equals( 393 getKind().mimeType) || !(s instanceof Spannable)) { 394 return; 395 } 396 final Spannable spannable = (Spannable) s; 397 final TtsSpan[] spans = spannable.getSpans(0, s.length(), TtsSpan.class); 398 for (int i = 0; i < spans.length; i++) { 399 spannable.removeSpan(spans[i]); 400 } 401 PhoneNumberUtilsCompat.addTtsSpan(spannable, 0, s.length()); 402 } 403 }); 404 405 fieldView.setEnabled(isEnabled() && !readOnly); 406 fieldView.setOnFocusChangeListener(mTextFocusChangeListener); 407 408 if (field.shortForm) { 409 hidePossible = true; 410 mHasShortAndLongForms = true; 411 fieldView.setVisibility(mHideOptional ? View.VISIBLE : View.GONE); 412 } else if (field.longForm) { 413 hidePossible = true; 414 mHasShortAndLongForms = true; 415 fieldView.setVisibility(mHideOptional ? View.GONE : View.VISIBLE); 416 } else { 417 // Hide field when empty and optional value 418 final boolean couldHide = (!ContactsUtils.isGraphic(value) && field.optional); 419 final boolean willHide = (mHideOptional && couldHide); 420 fieldView.setVisibility(willHide ? View.GONE : View.VISIBLE); 421 hidePossible = hidePossible || couldHide; 422 } 423 424 mFields.addView(fieldView); 425 } 426 427 if (mExpansionView != null) { 428 // When hiding fields, place expandable 429 setupExpansionView(hidePossible, mHideOptional); 430 mExpansionView.setEnabled(!readOnly && isEnabled()); 431 } 432 updateEmptiness(); 433 } 434 435 @Override isEmpty()436 public boolean isEmpty() { 437 for (int i = 0; i < mFields.getChildCount(); i++) { 438 EditText editText = (EditText) mFields.getChildAt(i); 439 if (!TextUtils.isEmpty(editText.getText())) { 440 return false; 441 } 442 } 443 return true; 444 } 445 446 /** 447 * Returns true if the editor is currently configured to show optional fields. 448 */ areOptionalFieldsVisible()449 public boolean areOptionalFieldsVisible() { 450 return !mHideOptional; 451 } 452 hasShortAndLongForms()453 public boolean hasShortAndLongForms() { 454 return mHasShortAndLongForms; 455 } 456 457 /** 458 * Populates the bound rectangle with the bounds of the last editor field inside this view. 459 */ acquireEditorBounds(Rect bounds)460 public void acquireEditorBounds(Rect bounds) { 461 if (mFieldEditTexts != null) { 462 for (int i = mFieldEditTexts.length; --i >= 0;) { 463 EditText editText = mFieldEditTexts[i]; 464 if (editText.getVisibility() == View.VISIBLE) { 465 bounds.set(editText.getLeft(), editText.getTop(), editText.getRight(), 466 editText.getBottom()); 467 return; 468 } 469 } 470 } 471 } 472 473 /** 474 * Saves the visibility of the child EditTexts, and mHideOptional. 475 */ 476 @Override onSaveInstanceState()477 protected Parcelable onSaveInstanceState() { 478 Parcelable superState = super.onSaveInstanceState(); 479 SavedState ss = new SavedState(superState); 480 481 ss.mHideOptional = mHideOptional; 482 483 final int numChildren = mFieldEditTexts == null ? 0 : mFieldEditTexts.length; 484 ss.mVisibilities = new int[numChildren]; 485 for (int i = 0; i < numChildren; i++) { 486 ss.mVisibilities[i] = mFieldEditTexts[i].getVisibility(); 487 } 488 489 return ss; 490 } 491 492 /** 493 * Restores the visibility of the child EditTexts, and mHideOptional. 494 */ 495 @Override onRestoreInstanceState(Parcelable state)496 protected void onRestoreInstanceState(Parcelable state) { 497 SavedState ss = (SavedState) state; 498 super.onRestoreInstanceState(ss.getSuperState()); 499 500 mHideOptional = ss.mHideOptional; 501 502 int numChildren = Math.min(mFieldEditTexts == null ? 0 : mFieldEditTexts.length, 503 ss.mVisibilities == null ? 0 : ss.mVisibilities.length); 504 for (int i = 0; i < numChildren; i++) { 505 mFieldEditTexts[i].setVisibility(ss.mVisibilities[i]); 506 } 507 rebuildValues(); 508 } 509 510 private static class SavedState extends BaseSavedState { 511 public boolean mHideOptional; 512 public int[] mVisibilities; 513 SavedState(Parcelable superState)514 SavedState(Parcelable superState) { 515 super(superState); 516 } 517 SavedState(Parcel in)518 private SavedState(Parcel in) { 519 super(in); 520 mVisibilities = new int[in.readInt()]; 521 in.readIntArray(mVisibilities); 522 } 523 524 @Override writeToParcel(Parcel out, int flags)525 public void writeToParcel(Parcel out, int flags) { 526 super.writeToParcel(out, flags); 527 out.writeInt(mVisibilities.length); 528 out.writeIntArray(mVisibilities); 529 } 530 531 @SuppressWarnings({"unused", "hiding" }) 532 public static final Parcelable.Creator<SavedState> CREATOR 533 = new Parcelable.Creator<SavedState>() { 534 @Override 535 public SavedState createFromParcel(Parcel in) { 536 return new SavedState(in); 537 } 538 539 @Override 540 public SavedState[] newArray(int size) { 541 return new SavedState[size]; 542 } 543 }; 544 } 545 546 @Override clearAllFields()547 public void clearAllFields() { 548 if (mFieldEditTexts != null) { 549 for (EditText fieldEditText : mFieldEditTexts) { 550 // Update UI (which will trigger a state change through the {@link TextWatcher}) 551 fieldEditText.setText(""); 552 } 553 } 554 } 555 } 556