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