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