Home | History | Annotate | Download | only in widget
      1 /*
      2  * Copyright (C) 2009 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.ui.widget;
     18 
     19 import com.android.contacts.ContactsUtils;
     20 import com.android.contacts.R;
     21 import com.android.contacts.model.Editor;
     22 import com.android.contacts.model.EntityDelta;
     23 import com.android.contacts.model.EntityModifier;
     24 import com.android.contacts.model.ContactsSource.DataKind;
     25 import com.android.contacts.model.ContactsSource.EditField;
     26 import com.android.contacts.model.ContactsSource.EditType;
     27 import com.android.contacts.model.EntityDelta.ValuesDelta;
     28 import com.android.contacts.ui.ViewIdGenerator;
     29 
     30 import android.app.AlertDialog;
     31 import android.app.Dialog;
     32 import android.content.Context;
     33 import android.content.DialogInterface;
     34 import android.content.Entity;
     35 import android.os.Parcel;
     36 import android.os.Parcelable;
     37 import android.telephony.PhoneNumberFormattingTextWatcher;
     38 import android.text.Editable;
     39 import android.text.InputType;
     40 import android.text.TextUtils;
     41 import android.text.TextWatcher;
     42 import android.util.AttributeSet;
     43 import android.view.ContextThemeWrapper;
     44 import android.view.LayoutInflater;
     45 import android.view.View;
     46 import android.view.ViewGroup;
     47 import android.view.inputmethod.EditorInfo;
     48 import android.widget.ArrayAdapter;
     49 import android.widget.EditText;
     50 import android.widget.ListAdapter;
     51 import android.widget.RelativeLayout;
     52 import android.widget.TextView;
     53 
     54 import java.util.List;
     55 
     56 /**
     57  * Simple editor that handles labels and any {@link EditField} defined for
     58  * the entry. Uses {@link ValuesDelta} to read any existing
     59  * {@link Entity} values, and to correctly write any changes values.
     60  */
     61 public class GenericEditorView extends RelativeLayout implements Editor, View.OnClickListener {
     62     protected static final int RES_FIELD = R.layout.item_editor_field;
     63     protected static final int RES_LABEL_ITEM = android.R.layout.simple_list_item_1;
     64 
     65     protected LayoutInflater mInflater;
     66 
     67     protected static final int INPUT_TYPE_CUSTOM = EditorInfo.TYPE_CLASS_TEXT
     68             | EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS;
     69 
     70     protected TextView mLabel;
     71     protected ViewGroup mFields;
     72     protected View mDelete;
     73     protected View mMore;
     74     protected View mLess;
     75 
     76     protected DataKind mKind;
     77     protected ValuesDelta mEntry;
     78     protected EntityDelta mState;
     79     protected boolean mReadOnly;
     80 
     81     protected boolean mHideOptional = true;
     82 
     83     protected EditType mType;
     84     // Used only when a user tries to use custom label.
     85     private EditType mPendingType;
     86 
     87     private ViewIdGenerator mViewIdGenerator;
     88 
     89     public GenericEditorView(Context context) {
     90         super(context);
     91     }
     92 
     93     public GenericEditorView(Context context, AttributeSet attrs) {
     94         super(context, attrs);
     95     }
     96 
     97     /** {@inheritDoc} */
     98     @Override
     99     protected void onFinishInflate() {
    100         mInflater = (LayoutInflater)getContext().getSystemService(
    101                 Context.LAYOUT_INFLATER_SERVICE);
    102 
    103         mLabel = (TextView)findViewById(R.id.edit_label);
    104         mLabel.setOnClickListener(this);
    105 
    106         mFields = (ViewGroup)findViewById(R.id.edit_fields);
    107 
    108         mDelete = findViewById(R.id.edit_delete);
    109         mDelete.setOnClickListener(this);
    110 
    111         mMore = findViewById(R.id.edit_more);
    112         mMore.setOnClickListener(this);
    113 
    114         mLess = findViewById(R.id.edit_less);
    115         mLess.setOnClickListener(this);
    116     }
    117 
    118     protected EditorListener mListener;
    119 
    120     public void setEditorListener(EditorListener listener) {
    121         mListener = listener;
    122     }
    123 
    124     public void setDeletable(boolean deletable) {
    125         mDelete.setVisibility(deletable ? View.VISIBLE : View.INVISIBLE);
    126     }
    127 
    128     @Override
    129     public void setEnabled(boolean enabled) {
    130         mLabel.setEnabled(enabled);
    131         final int count = mFields.getChildCount();
    132         for (int pos = 0; pos < count; pos++) {
    133             final View v = mFields.getChildAt(pos);
    134             v.setEnabled(enabled);
    135         }
    136         mMore.setEnabled(enabled);
    137         mLess.setEnabled(enabled);
    138     }
    139 
    140     /**
    141      * Build the current label state based on selected {@link EditType} and
    142      * possible custom label string.
    143      */
    144     private void rebuildLabel() {
    145         // Handle undetected types
    146         if (mType == null) {
    147             mLabel.setText(R.string.unknown);
    148             return;
    149         }
    150 
    151         if (mType.customColumn != null) {
    152             // Use custom label string when present
    153             final String customText = mEntry.getAsString(mType.customColumn);
    154             if (customText != null) {
    155                 mLabel.setText(customText);
    156                 return;
    157             }
    158         }
    159 
    160         // Otherwise fall back to using default label
    161         mLabel.setText(mType.labelRes);
    162     }
    163 
    164     /** {@inheritDoc} */
    165     public void onFieldChanged(String column, String value) {
    166         // Field changes are saved directly
    167         mEntry.put(column, value);
    168         if (mListener != null) {
    169             mListener.onRequest(EditorListener.FIELD_CHANGED);
    170         }
    171     }
    172 
    173     public boolean isAnyFieldFilledOut() {
    174         int childCount = mFields.getChildCount();
    175         for (int i = 0; i < childCount; i++) {
    176             EditText editorView = (EditText) mFields.getChildAt(i);
    177             if (!TextUtils.isEmpty(editorView.getText())) {
    178                 return true;
    179             }
    180         }
    181         return false;
    182     }
    183 
    184     private void rebuildValues() {
    185         setValues(mKind, mEntry, mState, mReadOnly, mViewIdGenerator);
    186     }
    187 
    188     /**
    189      * Prepare this editor using the given {@link DataKind} for defining
    190      * structure and {@link ValuesDelta} describing the content to edit.
    191      */
    192     public void setValues(DataKind kind, ValuesDelta entry, EntityDelta state, boolean readOnly,
    193             ViewIdGenerator vig) {
    194         mKind = kind;
    195         mEntry = entry;
    196         mState = state;
    197         mReadOnly = readOnly;
    198         mViewIdGenerator = vig;
    199         setId(vig.getId(state, kind, entry, ViewIdGenerator.NO_VIEW_INDEX));
    200 
    201         final boolean enabled = !readOnly;
    202 
    203         if (!entry.isVisible()) {
    204             // Hide ourselves entirely if deleted
    205             setVisibility(View.GONE);
    206             return;
    207         } else {
    208             setVisibility(View.VISIBLE);
    209         }
    210 
    211         // Display label selector if multiple types available
    212         final boolean hasTypes = EntityModifier.hasEditTypes(kind);
    213         mLabel.setVisibility(hasTypes ? View.VISIBLE : View.GONE);
    214         mLabel.setEnabled(enabled);
    215         if (hasTypes) {
    216             mType = EntityModifier.getCurrentType(entry, kind);
    217             rebuildLabel();
    218         }
    219 
    220         // Build out set of fields
    221         mFields.removeAllViews();
    222         boolean hidePossible = false;
    223         int n = 0;
    224         for (EditField field : kind.fieldList) {
    225             // Inflate field from definition
    226             EditText fieldView = (EditText)mInflater.inflate(RES_FIELD, mFields, false);
    227             fieldView.setId(vig.getId(state, kind, entry, n++));
    228             if (field.titleRes > 0) {
    229                 fieldView.setHint(field.titleRes);
    230             }
    231             int inputType = field.inputType;
    232             fieldView.setInputType(inputType);
    233             if (inputType == InputType.TYPE_CLASS_PHONE) {
    234                 fieldView.addTextChangedListener(new PhoneNumberFormattingTextWatcher());
    235             }
    236             fieldView.setMinLines(field.minLines);
    237 
    238             // Read current value from state
    239             final String column = field.column;
    240             final String value = entry.getAsString(column);
    241             fieldView.setText(value);
    242 
    243             // Prepare listener for writing changes
    244             fieldView.addTextChangedListener(new TextWatcher() {
    245                 public void afterTextChanged(Editable s) {
    246                     // Trigger event for newly changed value
    247                     onFieldChanged(column, s.toString());
    248                 }
    249 
    250                 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
    251                 }
    252 
    253                 public void onTextChanged(CharSequence s, int start, int before, int count) {
    254                 }
    255             });
    256 
    257             // Hide field when empty and optional value
    258             final boolean couldHide = (!ContactsUtils.isGraphic(value) && field.optional);
    259             final boolean willHide = (mHideOptional && couldHide);
    260             fieldView.setVisibility(willHide ? View.GONE : View.VISIBLE);
    261             fieldView.setEnabled(enabled);
    262             hidePossible = hidePossible || couldHide;
    263 
    264             mFields.addView(fieldView);
    265         }
    266 
    267         // When hiding fields, place expandable
    268         if (hidePossible) {
    269             mMore.setVisibility(mHideOptional ? View.VISIBLE : View.GONE);
    270             mLess.setVisibility(mHideOptional ? View.GONE : View.VISIBLE);
    271         } else {
    272             mMore.setVisibility(View.GONE);
    273             mLess.setVisibility(View.GONE);
    274         }
    275         mMore.setEnabled(enabled);
    276         mLess.setEnabled(enabled);
    277     }
    278 
    279     /**
    280      * Prepare dialog for entering a custom label. The input value is trimmed: white spaces before
    281      * and after the input text is removed.
    282      * <p>
    283      * If the final value is empty, this change request is ignored;
    284      * no empty text is allowed in any custom label.
    285      */
    286     private Dialog createCustomDialog() {
    287         final EditText customType = new EditText(mContext);
    288         customType.setInputType(INPUT_TYPE_CUSTOM);
    289         customType.requestFocus();
    290 
    291         final AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
    292         builder.setTitle(R.string.customLabelPickerTitle);
    293         builder.setView(customType);
    294 
    295         builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
    296             public void onClick(DialogInterface dialog, int which) {
    297                 final String customText = customType.getText().toString().trim();
    298                 if (ContactsUtils.isGraphic(customText)) {
    299                     // Now we're sure it's ok to actually change the type value.
    300                     mType = mPendingType;
    301                     mPendingType = null;
    302                     mEntry.put(mKind.typeColumn, mType.rawValue);
    303                     mEntry.put(mType.customColumn, customText);
    304                     rebuildLabel();
    305                     if (!mFields.hasFocus())
    306                         mFields.requestFocus();
    307                 }
    308             }
    309         });
    310 
    311         builder.setNegativeButton(android.R.string.cancel, null);
    312 
    313         return builder.create();
    314     }
    315 
    316     /**
    317      * Prepare dialog for picking a new {@link EditType} or entering a
    318      * custom label. This dialog is limited to the valid types as determined
    319      * by {@link EntityModifier}.
    320      */
    321     public Dialog createLabelDialog() {
    322         // Build list of valid types, including the current value
    323         final List<EditType> validTypes = EntityModifier.getValidTypes(mState, mKind, mType);
    324 
    325         // Wrap our context to inflate list items using correct theme
    326         final Context dialogContext = new ContextThemeWrapper(mContext,
    327                 android.R.style.Theme_Light);
    328         final LayoutInflater dialogInflater = mInflater.cloneInContext(dialogContext);
    329 
    330         final ListAdapter typeAdapter = new ArrayAdapter<EditType>(mContext, RES_LABEL_ITEM,
    331                 validTypes) {
    332             @Override
    333             public View getView(int position, View convertView, ViewGroup parent) {
    334                 if (convertView == null) {
    335                     convertView = dialogInflater.inflate(RES_LABEL_ITEM, parent, false);
    336                 }
    337 
    338                 final EditType type = this.getItem(position);
    339                 final TextView textView = (TextView)convertView;
    340                 textView.setText(type.labelRes);
    341                 return textView;
    342             }
    343         };
    344 
    345         final DialogInterface.OnClickListener clickListener =
    346                 new DialogInterface.OnClickListener() {
    347             public void onClick(DialogInterface dialog, int which) {
    348                 dialog.dismiss();
    349 
    350                 final EditType selected = validTypes.get(which);
    351                 if (selected.customColumn != null) {
    352                     // Show custom label dialog if requested by type.
    353                     //
    354                     // Only when the custum value input in the next step is correct one.
    355                     // this method also set the type value to what the user requested here.
    356                     mPendingType = selected;
    357                     createCustomDialog().show();
    358                 } else {
    359                     // User picked type, and we're sure it's ok to actually write the entry.
    360                     mType = selected;
    361                     mEntry.put(mKind.typeColumn, mType.rawValue);
    362                     rebuildLabel();
    363                     if (!mFields.hasFocus())
    364                         mFields.requestFocus();
    365                 }
    366             }
    367         };
    368 
    369         final AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
    370         builder.setTitle(R.string.selectLabel);
    371         builder.setSingleChoiceItems(typeAdapter, 0, clickListener);
    372         return builder.create();
    373     }
    374 
    375     /** {@inheritDoc} */
    376     public void onClick(View v) {
    377         switch (v.getId()) {
    378             case R.id.edit_label: {
    379                 createLabelDialog().show();
    380                 break;
    381             }
    382             case R.id.edit_delete: {
    383                 // Keep around in model, but mark as deleted
    384                 mEntry.markDeleted();
    385 
    386                 // Remove editor from parent view
    387                 final ViewGroup parent = (ViewGroup)getParent();
    388                 parent.removeView(this);
    389 
    390                 if (mListener != null) {
    391                     // Notify listener when present
    392                     mListener.onDeleted(this);
    393                 }
    394                 break;
    395             }
    396             case R.id.edit_more:
    397             case R.id.edit_less: {
    398                 mHideOptional = !mHideOptional;
    399                 rebuildValues();
    400                 break;
    401             }
    402         }
    403     }
    404 
    405     private static class SavedState extends BaseSavedState {
    406         public boolean mHideOptional;
    407         public int[] mVisibilities;
    408 
    409         SavedState(Parcelable superState) {
    410             super(superState);
    411         }
    412 
    413         private SavedState(Parcel in) {
    414             super(in);
    415             mVisibilities = new int[in.readInt()];
    416             in.readIntArray(mVisibilities);
    417         }
    418 
    419         @Override
    420         public void writeToParcel(Parcel out, int flags) {
    421             super.writeToParcel(out, flags);
    422             out.writeInt(mVisibilities.length);
    423             out.writeIntArray(mVisibilities);
    424         }
    425 
    426         public static final Parcelable.Creator<SavedState> CREATOR
    427                 = new Parcelable.Creator<SavedState>() {
    428             public SavedState createFromParcel(Parcel in) {
    429                 return new SavedState(in);
    430             }
    431 
    432             public SavedState[] newArray(int size) {
    433                 return new SavedState[size];
    434             }
    435         };
    436     }
    437 
    438     /**
    439      * Saves the visibility of the child EditTexts, and mHideOptional.
    440      */
    441     @Override
    442     protected Parcelable onSaveInstanceState() {
    443         Parcelable superState = super.onSaveInstanceState();
    444         SavedState ss = new SavedState(superState);
    445 
    446         ss.mHideOptional = mHideOptional;
    447 
    448         final int numChildren = mFields.getChildCount();
    449         ss.mVisibilities = new int[numChildren];
    450         for (int i = 0; i < numChildren; i++) {
    451             ss.mVisibilities[i] = mFields.getChildAt(i).getVisibility();
    452         }
    453 
    454         return ss;
    455     }
    456 
    457     /**
    458      * Restores the visibility of the child EditTexts, and mHideOptional.
    459      */
    460     @Override
    461     protected void onRestoreInstanceState(Parcelable state) {
    462         SavedState ss = (SavedState) state;
    463         super.onRestoreInstanceState(ss.getSuperState());
    464 
    465         mHideOptional = ss.mHideOptional;
    466 
    467         int numChildren = Math.min(mFields.getChildCount(), ss.mVisibilities.length);
    468         for (int i = 0; i < numChildren; i++) {
    469             mFields.getChildAt(i).setVisibility(ss.mVisibilities[i]);
    470         }
    471     }
    472 }
    473