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