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