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