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