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