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