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