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