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