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