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