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