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.app.AlertDialog;
     20 import android.app.Dialog;
     21 import android.content.Context;
     22 import android.content.DialogInterface;
     23 import android.content.DialogInterface.OnShowListener;
     24 import android.os.Bundle;
     25 import android.os.Handler;
     26 import android.text.Editable;
     27 import android.text.TextUtils;
     28 import android.text.TextWatcher;
     29 import android.util.AttributeSet;
     30 import android.util.TypedValue;
     31 import android.view.LayoutInflater;
     32 import android.view.View;
     33 import android.view.ViewGroup;
     34 import android.view.WindowManager;
     35 import android.view.inputmethod.EditorInfo;
     36 import android.widget.AdapterView;
     37 import android.widget.AdapterView.OnItemSelectedListener;
     38 import android.widget.ArrayAdapter;
     39 import android.widget.Button;
     40 import android.widget.EditText;
     41 import android.widget.ImageView;
     42 import android.widget.LinearLayout;
     43 import android.widget.Spinner;
     44 import android.widget.TextView;
     45 
     46 import com.android.contacts.R;
     47 import com.android.contacts.common.model.RawContactDelta;
     48 import com.android.contacts.common.ContactsUtils;
     49 import com.android.contacts.common.model.ValuesDelta;
     50 import com.android.contacts.common.model.RawContactModifier;
     51 import com.android.contacts.common.model.account.AccountType.EditType;
     52 import com.android.contacts.common.model.dataitem.DataKind;
     53 import com.android.contacts.util.DialogManager;
     54 import com.android.contacts.util.DialogManager.DialogShowingView;
     55 
     56 import java.util.List;
     57 
     58 /**
     59  * Base class for editors that handles labels and values. Uses
     60  * {@link ValuesDelta} to read any existing {@link RawContact} values, and to
     61  * correctly write any changes values.
     62  */
     63 public abstract class LabeledEditorView extends LinearLayout implements Editor, DialogShowingView {
     64     protected static final String DIALOG_ID_KEY = "dialog_id";
     65     private static final int DIALOG_ID_CUSTOM = 1;
     66 
     67     private static final int INPUT_TYPE_CUSTOM = EditorInfo.TYPE_CLASS_TEXT
     68             | EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS;
     69 
     70     private Spinner mLabel;
     71     private EditTypeAdapter mEditTypeAdapter;
     72     private View mDeleteContainer;
     73     private ImageView mDelete;
     74 
     75     private DataKind mKind;
     76     private ValuesDelta mEntry;
     77     private RawContactDelta mState;
     78     private boolean mReadOnly;
     79     private boolean mWasEmpty = true;
     80     private boolean mIsDeletable = true;
     81     private boolean mIsAttachedToWindow;
     82 
     83     private EditType mType;
     84 
     85     private ViewIdGenerator mViewIdGenerator;
     86     private DialogManager mDialogManager = null;
     87     private EditorListener mListener;
     88     protected int mMinLineItemHeight;
     89 
     90     /**
     91      * A marker in the spinner adapter of the currently selected custom type.
     92      */
     93     public static final EditType CUSTOM_SELECTION = new EditType(0, 0);
     94 
     95     private OnItemSelectedListener mSpinnerListener = new OnItemSelectedListener() {
     96 
     97         @Override
     98         public void onItemSelected(
     99                 AdapterView<?> parent, View view, int position, long id) {
    100             onTypeSelectionChange(position);
    101         }
    102 
    103         @Override
    104         public void onNothingSelected(AdapterView<?> parent) {
    105         }
    106     };
    107 
    108     public LabeledEditorView(Context context) {
    109         super(context);
    110         init(context);
    111     }
    112 
    113     public LabeledEditorView(Context context, AttributeSet attrs) {
    114         super(context, attrs);
    115         init(context);
    116     }
    117 
    118     public LabeledEditorView(Context context, AttributeSet attrs, int defStyle) {
    119         super(context, attrs, defStyle);
    120         init(context);
    121     }
    122 
    123     private void init(Context context) {
    124         mMinLineItemHeight = context.getResources().getDimensionPixelSize(
    125                 R.dimen.editor_min_line_item_height);
    126     }
    127 
    128     /** {@inheritDoc} */
    129     @Override
    130     protected void onFinishInflate() {
    131 
    132         mLabel = (Spinner) findViewById(R.id.spinner);
    133         // Turn off the Spinner's own state management. We do this ourselves on rotation
    134         mLabel.setId(View.NO_ID);
    135         mLabel.setOnItemSelectedListener(mSpinnerListener);
    136 
    137         mDelete = (ImageView) findViewById(R.id.delete_button);
    138         mDeleteContainer = findViewById(R.id.delete_button_container);
    139         mDeleteContainer.setOnClickListener(new OnClickListener() {
    140             @Override
    141             public void onClick(View v) {
    142                 // defer removal of this button so that the pressed state is visible shortly
    143                 new Handler().post(new Runnable() {
    144                     @Override
    145                     public void run() {
    146                         // Don't do anything if the view is no longer attached to the window
    147                         // (This check is needed because when this {@link Runnable} is executed,
    148                         // we can't guarantee the view is still valid.
    149                         if (!mIsAttachedToWindow) {
    150                             return;
    151                         }
    152                         // Send the delete request to the listener (which will in turn call
    153                         // deleteEditor() on this view if the deletion is valid - i.e. this is not
    154                         // the last {@link Editor} in the section).
    155                         if (mListener != null) {
    156                             mListener.onDeleteRequested(LabeledEditorView.this);
    157                         }
    158                     }
    159                 });
    160             }
    161         });
    162 
    163         setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(),
    164                 (int) getResources().getDimension(R.dimen.editor_padding_between_editor_views));
    165     }
    166 
    167     @Override
    168     protected void onAttachedToWindow() {
    169         super.onAttachedToWindow();
    170         // Keep track of when the view is attached or detached from the window, so we know it's
    171         // safe to remove views (in case the user requests to delete this editor).
    172         mIsAttachedToWindow = true;
    173     }
    174 
    175     @Override
    176     protected void onDetachedFromWindow() {
    177         super.onDetachedFromWindow();
    178         mIsAttachedToWindow = false;
    179     }
    180 
    181     @Override
    182     public void deleteEditor() {
    183         // Keep around in model, but mark as deleted
    184         mEntry.markDeleted();
    185 
    186         // Remove the view
    187         EditorAnimator.getInstance().removeEditorView(this);
    188     }
    189 
    190     public boolean isReadOnly() {
    191         return mReadOnly;
    192     }
    193 
    194     public int getBaseline(int row) {
    195         if (row == 0 && mLabel != null) {
    196             return mLabel.getBaseline();
    197         }
    198         return -1;
    199     }
    200 
    201     /**
    202      * Configures the visibility of the type label button and enables or disables it properly.
    203      */
    204     private void setupLabelButton(boolean shouldExist) {
    205         if (shouldExist) {
    206             mLabel.setEnabled(!mReadOnly && isEnabled());
    207             mLabel.setVisibility(View.VISIBLE);
    208         } else {
    209             mLabel.setVisibility(View.GONE);
    210         }
    211     }
    212 
    213     /**
    214      * Configures the visibility of the "delete" button and enables or disables it properly.
    215      */
    216     private void setupDeleteButton() {
    217         if (mIsDeletable) {
    218             mDeleteContainer.setVisibility(View.VISIBLE);
    219             mDelete.setEnabled(!mReadOnly && isEnabled());
    220         } else {
    221             mDeleteContainer.setVisibility(View.GONE);
    222         }
    223     }
    224 
    225     public void setDeleteButtonVisible(boolean visible) {
    226         if (mIsDeletable) {
    227             mDeleteContainer.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
    228         }
    229     }
    230 
    231     protected void onOptionalFieldVisibilityChange() {
    232         if (mListener != null) {
    233             mListener.onRequest(EditorListener.EDITOR_FORM_CHANGED);
    234         }
    235     }
    236 
    237     @Override
    238     public void setEditorListener(EditorListener listener) {
    239         mListener = listener;
    240     }
    241 
    242     protected EditorListener getEditorListener(){
    243         return mListener;
    244     }
    245 
    246     @Override
    247     public void setDeletable(boolean deletable) {
    248         mIsDeletable = deletable;
    249         setupDeleteButton();
    250     }
    251 
    252     @Override
    253     public void setEnabled(boolean enabled) {
    254         super.setEnabled(enabled);
    255         mLabel.setEnabled(!mReadOnly && enabled);
    256         mDelete.setEnabled(!mReadOnly && enabled);
    257     }
    258 
    259     public Spinner getLabel() {
    260         return mLabel;
    261     }
    262 
    263     public ImageView getDelete() {
    264         return mDelete;
    265     }
    266 
    267     protected DataKind getKind() {
    268         return mKind;
    269     }
    270 
    271     protected ValuesDelta getEntry() {
    272         return mEntry;
    273     }
    274 
    275     protected EditType getType() {
    276         return mType;
    277     }
    278 
    279     /**
    280      * Build the current label state based on selected {@link EditType} and
    281      * possible custom label string.
    282      */
    283     public void rebuildLabel() {
    284         mEditTypeAdapter = new EditTypeAdapter(mContext);
    285         mLabel.setAdapter(mEditTypeAdapter);
    286         if (mEditTypeAdapter.hasCustomSelection()) {
    287             mLabel.setSelection(mEditTypeAdapter.getPosition(CUSTOM_SELECTION));
    288         } else {
    289             mLabel.setSelection(mEditTypeAdapter.getPosition(mType));
    290         }
    291     }
    292 
    293     @Override
    294     public void onFieldChanged(String column, String value) {
    295         if (!isFieldChanged(column, value)) {
    296             return;
    297         }
    298 
    299         // Field changes are saved directly
    300         saveValue(column, value);
    301 
    302         // Notify listener if applicable
    303         notifyEditorListener();
    304 
    305         rebuildLabel();
    306     }
    307 
    308     protected void saveValue(String column, String value) {
    309         mEntry.put(column, value);
    310     }
    311 
    312     /**
    313      * Sub classes should call this at the end of {@link #setValues} once they finish changing
    314      * isEmpty(). This is needed to fix b/18194655.
    315      */
    316     protected final void updateEmptiness() {
    317         mWasEmpty = isEmpty();
    318     }
    319 
    320     protected void notifyEditorListener() {
    321         if (mListener != null) {
    322             mListener.onRequest(EditorListener.FIELD_CHANGED);
    323         }
    324 
    325         boolean isEmpty = isEmpty();
    326         if (mWasEmpty != isEmpty) {
    327             if (isEmpty) {
    328                 if (mListener != null) {
    329                     mListener.onRequest(EditorListener.FIELD_TURNED_EMPTY);
    330                 }
    331                 if (mIsDeletable) mDeleteContainer.setVisibility(View.INVISIBLE);
    332             } else {
    333                 if (mListener != null) {
    334                     mListener.onRequest(EditorListener.FIELD_TURNED_NON_EMPTY);
    335                 }
    336                 if (mIsDeletable) mDeleteContainer.setVisibility(View.VISIBLE);
    337             }
    338             mWasEmpty = isEmpty;
    339         }
    340     }
    341 
    342     protected boolean isFieldChanged(String column, String value) {
    343         final String dbValue = mEntry.getAsString(column);
    344         // nullable fields (e.g. Middle Name) are usually represented as empty columns,
    345         // so lets treat null and empty space equivalently here
    346         final String dbValueNoNull = dbValue == null ? "" : dbValue;
    347         final String valueNoNull = value == null ? "" : value;
    348         return !TextUtils.equals(dbValueNoNull, valueNoNull);
    349     }
    350 
    351     protected void rebuildValues() {
    352         setValues(mKind, mEntry, mState, mReadOnly, mViewIdGenerator);
    353     }
    354 
    355     /**
    356      * Prepare this editor using the given {@link DataKind} for defining structure and
    357      * {@link ValuesDelta} describing the content to edit. When overriding this, be careful
    358      * to call {@link #updateEmptiness} at the end.
    359      */
    360     @Override
    361     public void setValues(DataKind kind, ValuesDelta entry, RawContactDelta state, boolean readOnly,
    362             ViewIdGenerator vig) {
    363         mKind = kind;
    364         mEntry = entry;
    365         mState = state;
    366         mReadOnly = readOnly;
    367         mViewIdGenerator = vig;
    368         setId(vig.getId(state, kind, entry, ViewIdGenerator.NO_VIEW_INDEX));
    369 
    370         if (!entry.isVisible()) {
    371             // Hide ourselves entirely if deleted
    372             setVisibility(View.GONE);
    373             return;
    374         }
    375         setVisibility(View.VISIBLE);
    376 
    377         // Display label selector if multiple types available
    378         final boolean hasTypes = RawContactModifier.hasEditTypes(kind);
    379         setupLabelButton(hasTypes);
    380         mLabel.setEnabled(!readOnly && isEnabled());
    381         if (hasTypes) {
    382             mType = RawContactModifier.getCurrentType(entry, kind);
    383             rebuildLabel();
    384         }
    385     }
    386 
    387     public ValuesDelta getValues() {
    388         return mEntry;
    389     }
    390 
    391     /**
    392      * Prepare dialog for entering a custom label. The input value is trimmed: white spaces before
    393      * and after the input text is removed.
    394      * <p>
    395      * If the final value is empty, this change request is ignored;
    396      * no empty text is allowed in any custom label.
    397      */
    398     private Dialog createCustomDialog() {
    399         final AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
    400         final LayoutInflater layoutInflater = LayoutInflater.from(builder.getContext());
    401         builder.setTitle(R.string.customLabelPickerTitle);
    402 
    403         final View view = layoutInflater.inflate(R.layout.contact_editor_label_name_dialog, null);
    404         final EditText editText = (EditText) view.findViewById(R.id.custom_dialog_content);
    405         editText.setInputType(INPUT_TYPE_CUSTOM);
    406         editText.setSaveEnabled(true);
    407 
    408         builder.setView(view);
    409         editText.requestFocus();
    410 
    411         builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
    412             @Override
    413             public void onClick(DialogInterface dialog, int which) {
    414                 final String customText = editText.getText().toString().trim();
    415                 if (ContactsUtils.isGraphic(customText)) {
    416                     final List<EditType> allTypes =
    417                             RawContactModifier.getValidTypes(mState, mKind, null);
    418                     mType = null;
    419                     for (EditType editType : allTypes) {
    420                         if (editType.customColumn != null) {
    421                             mType = editType;
    422                             break;
    423                         }
    424                     }
    425                     if (mType == null) return;
    426 
    427                     mEntry.put(mKind.typeColumn, mType.rawValue);
    428                     mEntry.put(mType.customColumn, customText);
    429                     rebuildLabel();
    430                     requestFocusForFirstEditField();
    431                     onLabelRebuilt();
    432                 }
    433             }
    434         });
    435 
    436         builder.setNegativeButton(android.R.string.cancel, null);
    437 
    438         final AlertDialog dialog = builder.create();
    439         dialog.setOnShowListener(new OnShowListener() {
    440             @Override
    441             public void onShow(DialogInterface dialogInterface) {
    442                 updateCustomDialogOkButtonState(dialog, editText);
    443             }
    444         });
    445         editText.addTextChangedListener(new TextWatcher() {
    446             @Override
    447             public void onTextChanged(CharSequence s, int start, int before, int count) {
    448             }
    449 
    450             @Override
    451             public void beforeTextChanged(CharSequence s, int start, int count, int after) {
    452             }
    453 
    454             @Override
    455             public void afterTextChanged(Editable s) {
    456                 updateCustomDialogOkButtonState(dialog, editText);
    457             }
    458         });
    459         dialog.getWindow().setSoftInputMode(
    460                 WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
    461 
    462         return dialog;
    463     }
    464 
    465     /* package */ void updateCustomDialogOkButtonState(AlertDialog dialog, EditText editText) {
    466         final Button okButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
    467         okButton.setEnabled(!TextUtils.isEmpty(editText.getText().toString().trim()));
    468     }
    469 
    470     /**
    471      * Called after the label has changed (either chosen from the list or entered in the Dialog)
    472      */
    473     protected void onLabelRebuilt() {
    474     }
    475 
    476     protected void onTypeSelectionChange(int position) {
    477         EditType selected = mEditTypeAdapter.getItem(position);
    478         // See if the selection has in fact changed
    479         if (mEditTypeAdapter.hasCustomSelection() && selected == CUSTOM_SELECTION) {
    480             return;
    481         }
    482 
    483         if (mType == selected && mType.customColumn == null) {
    484             return;
    485         }
    486 
    487         if (selected.customColumn != null) {
    488             showDialog(DIALOG_ID_CUSTOM);
    489         } else {
    490             // User picked type, and we're sure it's ok to actually write the entry.
    491             mType = selected;
    492             mEntry.put(mKind.typeColumn, mType.rawValue);
    493             rebuildLabel();
    494             requestFocusForFirstEditField();
    495             onLabelRebuilt();
    496         }
    497     }
    498 
    499     /* package */
    500     void showDialog(int bundleDialogId) {
    501         Bundle bundle = new Bundle();
    502         bundle.putInt(DIALOG_ID_KEY, bundleDialogId);
    503         getDialogManager().showDialogInView(this, bundle);
    504     }
    505 
    506     private DialogManager getDialogManager() {
    507         if (mDialogManager == null) {
    508             Context context = getContext();
    509             if (!(context instanceof DialogManager.DialogShowingViewActivity)) {
    510                 throw new IllegalStateException(
    511                         "View must be hosted in an Activity that implements " +
    512                         "DialogManager.DialogShowingViewActivity");
    513             }
    514             mDialogManager = ((DialogManager.DialogShowingViewActivity)context).getDialogManager();
    515         }
    516         return mDialogManager;
    517     }
    518 
    519     @Override
    520     public Dialog createDialog(Bundle bundle) {
    521         if (bundle == null) throw new IllegalArgumentException("bundle must not be null");
    522         int dialogId = bundle.getInt(DIALOG_ID_KEY);
    523         switch (dialogId) {
    524             case DIALOG_ID_CUSTOM:
    525                 return createCustomDialog();
    526             default:
    527                 throw new IllegalArgumentException("Invalid dialogId: " + dialogId);
    528         }
    529     }
    530 
    531     protected abstract void requestFocusForFirstEditField();
    532 
    533     private class EditTypeAdapter extends ArrayAdapter<EditType> {
    534         private final LayoutInflater mInflater;
    535         private boolean mHasCustomSelection;
    536         private int mTextColorHintUnfocused;
    537         private int mTextColorDark;
    538         private int mTextColorSecondary;
    539 
    540         public EditTypeAdapter(Context context) {
    541             super(context, 0);
    542             mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    543             mTextColorHintUnfocused = context.getResources().getColor(
    544                     R.color.editor_disabled_text_color);
    545             mTextColorSecondary = context.getResources().getColor(R.color.secondary_text_color);
    546             mTextColorDark = context.getResources().getColor(R.color.primary_text_color);
    547 
    548 
    549             if (mType != null && mType.customColumn != null) {
    550 
    551                 // Use custom label string when present
    552                 final String customText = mEntry.getAsString(mType.customColumn);
    553                 if (customText != null) {
    554                     add(CUSTOM_SELECTION);
    555                     mHasCustomSelection = true;
    556                 }
    557             }
    558 
    559             addAll(RawContactModifier.getValidTypes(mState, mKind, mType));
    560         }
    561 
    562         public boolean hasCustomSelection() {
    563             return mHasCustomSelection;
    564         }
    565 
    566         @Override
    567         public View getView(int position, View convertView, ViewGroup parent) {
    568             final TextView view = createViewFromResource(
    569                     position, convertView, parent, R.layout.edit_simple_spinner_item);
    570             // We don't want any background on this view. The background would obscure
    571             // the spinner's background.
    572             view.setBackground(null);
    573             // The text color should be a very light hint color when unfocused and empty. When
    574             // focused and empty, use a less light hint color. When non-empty, use a dark non-hint
    575             // color.
    576             if (!LabeledEditorView.this.isEmpty()) {
    577                 view.setTextColor(mTextColorDark);
    578             } else if (LabeledEditorView.this.hasFocus()) {
    579                 view.setTextColor(mTextColorSecondary);
    580             } else {
    581                 view.setTextColor(mTextColorHintUnfocused);
    582             }
    583             return view;
    584         }
    585 
    586         @Override
    587         public View getDropDownView(int position, View convertView, ViewGroup parent) {
    588             return createViewFromResource(
    589                     position, convertView, parent, android.R.layout.simple_spinner_dropdown_item);
    590         }
    591 
    592         private TextView createViewFromResource(int position, View convertView, ViewGroup parent,
    593                 int resource) {
    594             TextView textView;
    595 
    596             if (convertView == null) {
    597                 textView = (TextView) mInflater.inflate(resource, parent, false);
    598                 textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, getResources().getDimension(
    599                         R.dimen.editor_form_text_size));
    600                 textView.setTextColor(mTextColorDark);
    601             } else {
    602                 textView = (TextView) convertView;
    603             }
    604 
    605             EditType type = getItem(position);
    606             String text;
    607             if (type == CUSTOM_SELECTION) {
    608                 text = mEntry.getAsString(mType.customColumn);
    609             } else {
    610                 text = getContext().getString(type.labelRes);
    611             }
    612             textView.setText(text);
    613             return textView;
    614         }
    615     }
    616 }
    617