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