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.content.Context;
20import android.graphics.Rect;
21import android.os.Parcel;
22import android.os.Parcelable;
23import android.text.Editable;
24import android.text.InputType;
25import android.text.TextUtils;
26import android.text.TextWatcher;
27import android.util.AttributeSet;
28import android.util.Log;
29import android.util.TypedValue;
30import android.view.View;
31import android.view.ViewGroup;
32import android.view.inputmethod.EditorInfo;
33import android.view.inputmethod.InputMethodManager;
34import android.widget.EditText;
35import android.widget.ImageView;
36import android.widget.LinearLayout;
37
38import com.android.contacts.R;
39import com.android.contacts.common.model.RawContactDelta;
40import com.android.contacts.common.ContactsUtils;
41import com.android.contacts.common.model.ValuesDelta;
42import com.android.contacts.common.model.account.AccountType.EditField;
43import com.android.contacts.common.model.dataitem.DataKind;
44import com.android.contacts.common.util.PhoneNumberFormatter;
45
46/**
47 * Simple editor that handles labels and any {@link EditField} defined for the
48 * entry. Uses {@link ValuesDelta} to read any existing {@link RawContact} values,
49 * and to correctly write any changes values.
50 */
51public class TextFieldsEditorView extends LabeledEditorView {
52    private static final String TAG = TextFieldsEditorView.class.getSimpleName();
53
54    private EditText[] mFieldEditTexts = null;
55    private ViewGroup mFields = null;
56    private View mExpansionViewContainer;
57    private ImageView mExpansionView;
58    private boolean mHideOptional = true;
59    private boolean mHasShortAndLongForms;
60    private int mMinFieldHeight;
61    private int mPreviousViewHeight;
62    private int mHintTextColorUnfocused;
63
64    public TextFieldsEditorView(Context context) {
65        super(context);
66    }
67
68    public TextFieldsEditorView(Context context, AttributeSet attrs) {
69        super(context, attrs);
70    }
71
72    public TextFieldsEditorView(Context context, AttributeSet attrs, int defStyle) {
73        super(context, attrs, defStyle);
74    }
75
76    /** {@inheritDoc} */
77    @Override
78    protected void onFinishInflate() {
79        super.onFinishInflate();
80
81        setDrawingCacheEnabled(true);
82        setAlwaysDrawnWithCacheEnabled(true);
83
84        mMinFieldHeight = getContext().getResources().getDimensionPixelSize(
85                R.dimen.editor_min_line_item_height);
86        mFields = (ViewGroup) findViewById(R.id.editors);
87        mHintTextColorUnfocused = getResources().getColor(R.color.editor_disabled_text_color);
88        mExpansionView = (ImageView) findViewById(R.id.expansion_view);
89        mExpansionViewContainer = findViewById(R.id.expansion_view_container);
90        if (mExpansionViewContainer != null) {
91            mExpansionViewContainer.setOnClickListener(new OnClickListener() {
92                @Override
93                public void onClick(View v) {
94                    mPreviousViewHeight = mFields.getHeight();
95
96                    // Save focus
97                    final View focusedChild = getFocusedChild();
98                    final int focusedViewId = focusedChild == null ? -1 : focusedChild.getId();
99
100                    // Reconfigure GUI
101                    mHideOptional = !mHideOptional;
102                    onOptionalFieldVisibilityChange();
103                    rebuildValues();
104
105                    // Restore focus
106                    View newFocusView = findViewById(focusedViewId);
107                    if (newFocusView == null || newFocusView.getVisibility() == GONE) {
108                        // find first visible child
109                        newFocusView = TextFieldsEditorView.this;
110                    }
111                    newFocusView.requestFocus();
112
113                    EditorAnimator.getInstance().slideAndFadeIn(mFields, mPreviousViewHeight);
114                }
115            });
116        }
117    }
118
119    @Override
120    public void editNewlyAddedField() {
121        // Some editors may have multiple fields (eg: first-name/last-name), but since the user
122        // has not selected a particular one, it is reasonable to simply pick the first.
123        final View editor = mFields.getChildAt(0);
124
125        // Show the soft-keyboard.
126        InputMethodManager imm =
127                (InputMethodManager)getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
128        if (imm != null) {
129            if (!imm.showSoftInput(editor, InputMethodManager.SHOW_IMPLICIT)) {
130                Log.w(TAG, "Failed to show soft input method.");
131            }
132        }
133    }
134
135    @Override
136    public void setEnabled(boolean enabled) {
137        super.setEnabled(enabled);
138
139        if (mFieldEditTexts != null) {
140            for (int index = 0; index < mFieldEditTexts.length; index++) {
141                mFieldEditTexts[index].setEnabled(!isReadOnly() && enabled);
142            }
143        }
144        if (mExpansionView != null) {
145            mExpansionView.setEnabled(!isReadOnly() && enabled);
146        }
147    }
148
149    private OnFocusChangeListener mTextFocusChangeListener = new OnFocusChangeListener() {
150        @Override
151        public void onFocusChange(View v, boolean hasFocus) {
152            if (getEditorListener() != null) {
153                getEditorListener().onRequest(EditorListener.EDITOR_FOCUS_CHANGED);
154            }
155            // Check whether this field contains focus by calling findFocus() instead of
156            // hasFocus(). The hasFocus() value is not necessarily up to date.
157            final boolean foundFocus = TextFieldsEditorView.this.findFocus() != null;
158            if (foundFocus && !isTypeVisible()) {
159                // We just got focus and the types are not visible
160                showType();
161            } else if (isEmpty()) {
162                // We just lost focus and the field is empty
163                hideType();
164            }
165            // Rebuild the label spinner using the new colors.
166            rebuildLabel();
167        }
168    };
169
170    /**
171     * Creates or removes the type/label button. Doesn't do anything if already correctly configured
172     */
173    private void setupExpansionView(boolean shouldExist, boolean collapsed) {
174        mExpansionView.setImageResource(collapsed
175                ? R.drawable.ic_menu_expander_minimized_holo_light
176                : R.drawable.ic_menu_expander_maximized_holo_light);
177        mExpansionViewContainer.setVisibility(shouldExist ? View.VISIBLE : View.INVISIBLE);
178    }
179
180    @Override
181    protected void requestFocusForFirstEditField() {
182        if (mFieldEditTexts != null && mFieldEditTexts.length != 0) {
183            EditText firstField = null;
184            boolean anyFieldHasFocus = false;
185            for (EditText editText : mFieldEditTexts) {
186                if (firstField == null && editText.getVisibility() == View.VISIBLE) {
187                    firstField = editText;
188                }
189                if (editText.hasFocus()) {
190                    anyFieldHasFocus = true;
191                    break;
192                }
193            }
194            if (!anyFieldHasFocus && firstField != null) {
195                firstField.requestFocus();
196            }
197        }
198    }
199
200    public void setValue(int field, String value) {
201        mFieldEditTexts[field].setText(value);
202    }
203
204    @Override
205    public void setValues(DataKind kind, ValuesDelta entry, RawContactDelta state, boolean readOnly,
206            ViewIdGenerator vig) {
207        super.setValues(kind, entry, state, readOnly, vig);
208        // Remove edit texts that we currently have
209        if (mFieldEditTexts != null) {
210            for (EditText fieldEditText : mFieldEditTexts) {
211                mFields.removeView(fieldEditText);
212            }
213        }
214        boolean hidePossible = false;
215
216        int fieldCount = kind.fieldList.size();
217        mFieldEditTexts = new EditText[fieldCount];
218        for (int index = 0; index < fieldCount; index++) {
219            final EditField field = kind.fieldList.get(index);
220            final EditText fieldView = new EditText(getContext());
221            fieldView.setLayoutParams(new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT,
222                    LayoutParams.WRAP_CONTENT));
223            fieldView.setTextSize(TypedValue.COMPLEX_UNIT_PX,
224                    getResources().getDimension(R.dimen.editor_form_text_size));
225            fieldView.setHintTextColor(mHintTextColorUnfocused);
226            mFieldEditTexts[index] = fieldView;
227            fieldView.setId(vig.getId(state, kind, entry, index));
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                PhoneNumberFormatter.setPhoneNumberFormattingTextWatcher(getContext(), fieldView);
235                fieldView.setTextDirection(View.TEXT_DIRECTION_LTR);
236            }
237
238            // Set either a minimum line requirement or a minimum height (because {@link TextView}
239            // only takes one or the other at a single time).
240            if (field.minLines > 1) {
241                fieldView.setMinLines(field.minLines);
242            } else {
243                // This needs to be called after setInputType. Otherwise, calling setInputType
244                // will unset this value.
245                fieldView.setMinHeight(mMinFieldHeight);
246            }
247
248            // Show the "next" button in IME to navigate between text fields
249            // TODO: Still need to properly navigate to/from sections without text fields,
250            // See Bug: 5713510
251            fieldView.setImeOptions(EditorInfo.IME_ACTION_NEXT);
252
253            // Read current value from state
254            final String column = field.column;
255            final String value = entry.getAsString(column);
256            fieldView.setText(value);
257
258            // Show the type drop down if we have a non-empty value.
259            if (!isTypeVisible() && !TextUtils.isEmpty(value)) {
260                showType();
261            }
262
263            // Show the delete button if we have a non-null value
264            setDeleteButtonVisible(value != null);
265
266            // Prepare listener for writing changes
267            fieldView.addTextChangedListener(new TextWatcher() {
268                @Override
269                public void afterTextChanged(Editable s) {
270                    // Trigger event for newly changed value
271                    onFieldChanged(column, s.toString());
272                }
273
274                @Override
275                public void beforeTextChanged(CharSequence s, int start, int count, int after) {
276                }
277
278                @Override
279                public void onTextChanged(CharSequence s, int start, int before, int count) {
280                }
281            });
282
283            fieldView.setEnabled(isEnabled() && !readOnly);
284            fieldView.setOnFocusChangeListener(mTextFocusChangeListener);
285
286            if (field.shortForm) {
287                hidePossible = true;
288                mHasShortAndLongForms = true;
289                fieldView.setVisibility(mHideOptional ? View.VISIBLE : View.GONE);
290            } else if (field.longForm) {
291                hidePossible = true;
292                mHasShortAndLongForms = true;
293                fieldView.setVisibility(mHideOptional ? View.GONE : View.VISIBLE);
294            } else {
295                // Hide field when empty and optional value
296                final boolean couldHide = (!ContactsUtils.isGraphic(value) && field.optional);
297                final boolean willHide = (mHideOptional && couldHide);
298                fieldView.setVisibility(willHide ? View.GONE : View.VISIBLE);
299                hidePossible = hidePossible || couldHide;
300            }
301
302            mFields.addView(fieldView);
303        }
304
305        if (mExpansionView != null) {
306            // When hiding fields, place expandable
307            setupExpansionView(hidePossible, mHideOptional);
308            mExpansionView.setEnabled(!readOnly && isEnabled());
309        }
310        updateEmptiness();
311    }
312
313    @Override
314    public boolean isEmpty() {
315        for (int i = 0; i < mFields.getChildCount(); i++) {
316            EditText editText = (EditText) mFields.getChildAt(i);
317            if (!TextUtils.isEmpty(editText.getText())) {
318                return false;
319            }
320        }
321        return true;
322    }
323
324    /**
325     * Returns true if the editor is currently configured to show optional fields.
326     */
327    public boolean areOptionalFieldsVisible() {
328        return !mHideOptional;
329    }
330
331    public boolean hasShortAndLongForms() {
332        return mHasShortAndLongForms;
333    }
334
335    /**
336     * Populates the bound rectangle with the bounds of the last editor field inside this view.
337     */
338    public void acquireEditorBounds(Rect bounds) {
339        if (mFieldEditTexts != null) {
340            for (int i = mFieldEditTexts.length; --i >= 0;) {
341                EditText editText = mFieldEditTexts[i];
342                if (editText.getVisibility() == View.VISIBLE) {
343                    bounds.set(editText.getLeft(), editText.getTop(), editText.getRight(),
344                            editText.getBottom());
345                    return;
346                }
347            }
348        }
349    }
350
351    /**
352     * Saves the visibility of the child EditTexts, and mHideOptional.
353     */
354    @Override
355    protected Parcelable onSaveInstanceState() {
356        Parcelable superState = super.onSaveInstanceState();
357        SavedState ss = new SavedState(superState);
358
359        ss.mHideOptional = mHideOptional;
360
361        final int numChildren = mFieldEditTexts == null ? 0 : mFieldEditTexts.length;
362        ss.mVisibilities = new int[numChildren];
363        for (int i = 0; i < numChildren; i++) {
364            ss.mVisibilities[i] = mFieldEditTexts[i].getVisibility();
365        }
366
367        return ss;
368    }
369
370    /**
371     * Restores the visibility of the child EditTexts, and mHideOptional.
372     */
373    @Override
374    protected void onRestoreInstanceState(Parcelable state) {
375        SavedState ss = (SavedState) state;
376        super.onRestoreInstanceState(ss.getSuperState());
377
378        mHideOptional = ss.mHideOptional;
379
380        int numChildren = Math.min(mFieldEditTexts == null ? 0 : mFieldEditTexts.length,
381                ss.mVisibilities == null ? 0 : ss.mVisibilities.length);
382        for (int i = 0; i < numChildren; i++) {
383            mFieldEditTexts[i].setVisibility(ss.mVisibilities[i]);
384        }
385    }
386
387    private static class SavedState extends BaseSavedState {
388        public boolean mHideOptional;
389        public int[] mVisibilities;
390
391        SavedState(Parcelable superState) {
392            super(superState);
393        }
394
395        private SavedState(Parcel in) {
396            super(in);
397            mVisibilities = new int[in.readInt()];
398            in.readIntArray(mVisibilities);
399        }
400
401        @Override
402        public void writeToParcel(Parcel out, int flags) {
403            super.writeToParcel(out, flags);
404            out.writeInt(mVisibilities.length);
405            out.writeIntArray(mVisibilities);
406        }
407
408        @SuppressWarnings({"unused", "hiding" })
409        public static final Parcelable.Creator<SavedState> CREATOR
410                = new Parcelable.Creator<SavedState>() {
411            @Override
412            public SavedState createFromParcel(Parcel in) {
413                return new SavedState(in);
414            }
415
416            @Override
417            public SavedState[] newArray(int size) {
418                return new SavedState[size];
419            }
420        };
421    }
422
423    @Override
424    public void clearAllFields() {
425        if (mFieldEditTexts != null) {
426            for (EditText fieldEditText : mFieldEditTexts) {
427                // Update UI (which will trigger a state change through the {@link TextWatcher})
428                fieldEditText.setText("");
429            }
430        }
431    }
432}
433