Home | History | Annotate | Download | only in editor
      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 
     79     public TextFieldsEditorView(Context context) {
     80         super(context);
     81     }
     82 
     83     public TextFieldsEditorView(Context context, AttributeSet attrs) {
     84         super(context, attrs);
     85     }
     86 
     87     public TextFieldsEditorView(Context context, AttributeSet attrs, int defStyle) {
     88         super(context, attrs, defStyle);
     89     }
     90 
     91     /** {@inheritDoc} */
     92     @Override
     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
    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
    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      */
    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
    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 
    223     public void setValue(int field, String value) {
    224         mFieldEditTexts[field].setText(value);
    225     }
    226 
    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 
    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
    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
    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      */
    421     public boolean areOptionalFieldsVisible() {
    422         return !mHideOptional;
    423     }
    424 
    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      */
    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
    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
    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 
    486         SavedState(Parcelable superState) {
    487             super(superState);
    488         }
    489 
    490         private SavedState(Parcel in) {
    491             super(in);
    492             mVisibilities = new int[in.readInt()];
    493             in.readIntArray(mVisibilities);
    494         }
    495 
    496         @Override
    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
    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