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