• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
GenericEditorView(Context context)89     public GenericEditorView(Context context) {
90         super(context);
91     }
92 
GenericEditorView(Context context, AttributeSet attrs)93     public GenericEditorView(Context context, AttributeSet attrs) {
94         super(context, attrs);
95     }
96 
97     /** {@inheritDoc} */
98     @Override
onFinishInflate()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 
setEditorListener(EditorListener listener)120     public void setEditorListener(EditorListener listener) {
121         mListener = listener;
122     }
123 
setDeletable(boolean deletable)124     public void setDeletable(boolean deletable) {
125         mDelete.setVisibility(deletable ? View.VISIBLE : View.INVISIBLE);
126     }
127 
128     @Override
setEnabled(boolean enabled)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      */
rebuildLabel()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} */
onFieldChanged(String column, String value)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 
isAnyFieldFilledOut()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 
rebuildValues()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      */
setValues(DataKind kind, ValuesDelta entry, EntityDelta state, boolean readOnly, ViewIdGenerator vig)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      */
createCustomDialog()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      */
createLabelDialog()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} */
onClick(View v)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 
SavedState(Parcelable superState)409         SavedState(Parcelable superState) {
410             super(superState);
411         }
412 
SavedState(Parcel in)413         private SavedState(Parcel in) {
414             super(in);
415             mVisibilities = new int[in.readInt()];
416             in.readIntArray(mVisibilities);
417         }
418 
419         @Override
writeToParcel(Parcel out, int flags)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
onSaveInstanceState()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
onRestoreInstanceState(Parcelable state)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