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