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