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