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