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