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