/* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.contacts.editor; import android.app.AlertDialog; import android.app.Dialog; import android.content.Context; import android.content.DialogInterface; import android.content.DialogInterface.OnShowListener; import android.os.Bundle; import android.os.Handler; import android.text.Editable; import android.text.TextUtils; import android.text.TextWatcher; import android.util.AttributeSet; import android.util.TypedValue; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; import android.view.inputmethod.EditorInfo; import android.widget.AdapterView; import android.widget.AdapterView.OnItemSelectedListener; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.CheckedTextView; import android.widget.EditText; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.Spinner; import android.widget.TextView; import com.android.contacts.ContactsUtils; import com.android.contacts.R; import com.android.contacts.model.RawContactDelta; import com.android.contacts.model.RawContactModifier; import com.android.contacts.model.ValuesDelta; import com.android.contacts.model.account.AccountType.EditType; import com.android.contacts.model.dataitem.DataKind; import com.android.contacts.util.DialogManager; import com.android.contacts.util.DialogManager.DialogShowingView; import java.util.List; /** * Base class for editors that handles labels and values. Uses * {@link ValuesDelta} to read any existing {@link RawContact} values, and to * correctly write any changes values. */ public abstract class LabeledEditorView extends LinearLayout implements Editor, DialogShowingView { protected static final String DIALOG_ID_KEY = "dialog_id"; private static final int DIALOG_ID_CUSTOM = 1; private static final int INPUT_TYPE_CUSTOM = EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS; private Spinner mLabel; private EditTypeAdapter mEditTypeAdapter; protected View mDeleteContainer; private ImageView mDelete; private DataKind mKind; private ValuesDelta mEntry; private RawContactDelta mState; private boolean mReadOnly; private boolean mWasEmpty = true; private boolean mIsDeletable = true; private boolean mIsAttachedToWindow; private EditType mType; private ViewIdGenerator mViewIdGenerator; private DialogManager mDialogManager = null; private EditorListener mListener; protected int mMinLineItemHeight; private int mSelectedLabelIndex; /** * A marker in the spinner adapter of the currently selected custom type. */ public static final EditType CUSTOM_SELECTION = new EditType(0, 0); private OnItemSelectedListener mSpinnerListener = new OnItemSelectedListener() { @Override public void onItemSelected( AdapterView> parent, View view, int position, long id) { onTypeSelectionChange(position); } @Override public void onNothingSelected(AdapterView> parent) { } }; public LabeledEditorView(Context context) { super(context); init(context); } public LabeledEditorView(Context context, AttributeSet attrs) { super(context, attrs); init(context); } public LabeledEditorView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(context); } public Long getRawContactId() { return mState == null ? null : mState.getRawContactId(); } private void init(Context context) { mMinLineItemHeight = context.getResources().getDimensionPixelSize( R.dimen.editor_min_line_item_height); } /** {@inheritDoc} */ @Override protected void onFinishInflate() { mLabel = (Spinner) findViewById(R.id.spinner); // Turn off the Spinner's own state management. We do this ourselves on rotation mLabel.setId(View.NO_ID); mLabel.setOnItemSelectedListener(mSpinnerListener); ViewSelectedFilter.suppressViewSelectedEvent(mLabel); mDelete = (ImageView) findViewById(R.id.delete_button); mDeleteContainer = findViewById(R.id.delete_button_container); mDeleteContainer.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { // defer removal of this button so that the pressed state is visible shortly new Handler().post(new Runnable() { @Override public void run() { // Don't do anything if the view is no longer attached to the window // (This check is needed because when this {@link Runnable} is executed, // we can't guarantee the view is still valid. if (!mIsAttachedToWindow) { return; } // Send the delete request to the listener (which will in turn call // deleteEditor() on this view if the deletion is valid - i.e. this is not // the last {@link Editor} in the section). if (mListener != null) { mListener.onDeleteRequested(LabeledEditorView.this); } } }); } }); setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(), (int) getResources().getDimension(R.dimen.editor_padding_between_editor_views)); } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); // Keep track of when the view is attached or detached from the window, so we know it's // safe to remove views (in case the user requests to delete this editor). mIsAttachedToWindow = true; } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); mIsAttachedToWindow = false; } @Override public void markDeleted() { // Keep around in model, but mark as deleted mEntry.markDeleted(); } @Override public void deleteEditor() { markDeleted(); // Remove the view EditorAnimator.getInstance().removeEditorView(this); } public boolean isReadOnly() { return mReadOnly; } public int getBaseline(int row) { if (row == 0 && mLabel != null) { return mLabel.getBaseline(); } return -1; } /** * Configures the visibility of the type label button and enables or disables it properly. */ private void setupLabelButton(boolean shouldExist) { if (shouldExist) { mLabel.setEnabled(!mReadOnly && isEnabled()); mLabel.setVisibility(View.VISIBLE); } else { mLabel.setVisibility(View.GONE); } } /** * Configures the visibility of the "delete" button and enables or disables it properly. */ private void setupDeleteButton() { if (mIsDeletable) { mDeleteContainer.setVisibility(View.VISIBLE); mDelete.setEnabled(!mReadOnly && isEnabled()); } else { mDeleteContainer.setVisibility(View.INVISIBLE); } } public void setDeleteButtonVisible(boolean visible) { if (mIsDeletable) { mDeleteContainer.setVisibility(visible ? View.VISIBLE : View.INVISIBLE); } } protected void onOptionalFieldVisibilityChange() { if (mListener != null) { mListener.onRequest(EditorListener.EDITOR_FORM_CHANGED); } } @Override public void setEditorListener(EditorListener listener) { mListener = listener; } protected EditorListener getEditorListener(){ return mListener; } @Override public void setDeletable(boolean deletable) { mIsDeletable = deletable; setupDeleteButton(); } @Override public void setEnabled(boolean enabled) { super.setEnabled(enabled); mLabel.setEnabled(!mReadOnly && enabled); mDelete.setEnabled(!mReadOnly && enabled); } public Spinner getLabel() { return mLabel; } public ImageView getDelete() { return mDelete; } protected DataKind getKind() { return mKind; } protected ValuesDelta getEntry() { return mEntry; } protected EditType getType() { return mType; } /** * Build the current label state based on selected {@link EditType} and * possible custom label string. */ public void rebuildLabel() { mEditTypeAdapter = new EditTypeAdapter(getContext()); mEditTypeAdapter.setSelectedIndex(mSelectedLabelIndex); mLabel.setAdapter(mEditTypeAdapter); if (mEditTypeAdapter.hasCustomSelection()) { mLabel.setSelection(mEditTypeAdapter.getPosition(CUSTOM_SELECTION)); mDeleteContainer.setContentDescription( getContext().getString(R.string.editor_delete_view_description, mEntry.getAsString(mType.customColumn), getContext().getString(mKind.titleRes))); } else { if (mType != null && mType.labelRes > 0 && mKind.titleRes > 0) { mLabel.setSelection(mEditTypeAdapter.getPosition(mType)); mDeleteContainer.setContentDescription( getContext().getString(R.string.editor_delete_view_description, getContext().getString(mType.labelRes), getContext().getString(mKind.titleRes))); } else if (mKind.titleRes > 0) { mDeleteContainer.setContentDescription( getContext().getString(R.string.editor_delete_view_description_short, getContext().getString(mKind.titleRes))); } } } @Override public void onFieldChanged(String column, String value) { if (!isFieldChanged(column, value)) { return; } // Field changes are saved directly saveValue(column, value); // Notify listener if applicable notifyEditorListener(); } protected void saveValue(String column, String value) { mEntry.put(column, value); } /** * Sub classes should call this at the end of {@link #setValues} once they finish changing * isEmpty(). This is needed to fix b/18194655. */ protected final void updateEmptiness() { mWasEmpty = isEmpty(); } protected void notifyEditorListener() { if (mListener != null) { mListener.onRequest(EditorListener.FIELD_CHANGED); } boolean isEmpty = isEmpty(); if (mWasEmpty != isEmpty) { if (isEmpty) { if (mListener != null) { mListener.onRequest(EditorListener.FIELD_TURNED_EMPTY); } if (mIsDeletable) mDeleteContainer.setVisibility(View.INVISIBLE); } else { if (mListener != null) { mListener.onRequest(EditorListener.FIELD_TURNED_NON_EMPTY); } if (mIsDeletable) mDeleteContainer.setVisibility(View.VISIBLE); } mWasEmpty = isEmpty; // Update the label text color if (mEditTypeAdapter != null) { mEditTypeAdapter.notifyDataSetChanged(); } } } protected boolean isFieldChanged(String column, String value) { final String dbValue = mEntry.getAsString(column); // nullable fields (e.g. Middle Name) are usually represented as empty columns, // so lets treat null and empty space equivalently here final String dbValueNoNull = dbValue == null ? "" : dbValue; final String valueNoNull = value == null ? "" : value; return !TextUtils.equals(dbValueNoNull, valueNoNull); } protected void rebuildValues() { setValues(mKind, mEntry, mState, mReadOnly, mViewIdGenerator); } /** * Prepare this editor using the given {@link DataKind} for defining structure and * {@link ValuesDelta} describing the content to edit. When overriding this, be careful * to call {@link #updateEmptiness} at the end. */ @Override public void setValues(DataKind kind, ValuesDelta entry, RawContactDelta state, boolean readOnly, ViewIdGenerator vig) { mKind = kind; mEntry = entry; mState = state; mReadOnly = readOnly; mViewIdGenerator = vig; setId(vig.getId(state, kind, entry, ViewIdGenerator.NO_VIEW_INDEX)); if (!entry.isVisible()) { // Hide ourselves entirely if deleted setVisibility(View.GONE); return; } setVisibility(View.VISIBLE); // Display label selector if multiple types available final boolean hasTypes = RawContactModifier.hasEditTypes(kind); setupLabelButton(hasTypes); mLabel.setEnabled(!readOnly && isEnabled()); if (mKind.titleRes > 0) { mLabel.setContentDescription(getContext().getResources().getString(mKind.titleRes)); } mType = RawContactModifier.getCurrentType(entry, kind); rebuildLabel(); } public ValuesDelta getValues() { return mEntry; } /** * Prepare dialog for entering a custom label. The input value is trimmed: white spaces before * and after the input text is removed. *
* If the final value is empty, this change request is ignored;
* no empty text is allowed in any custom label.
*/
private Dialog createCustomDialog() {
final AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
final LayoutInflater layoutInflater = LayoutInflater.from(builder.getContext());
builder.setTitle(R.string.customLabelPickerTitle);
final View view = layoutInflater.inflate(R.layout.contact_editor_label_name_dialog, null);
final EditText editText = (EditText) view.findViewById(R.id.custom_dialog_content);
editText.setInputType(INPUT_TYPE_CUSTOM);
editText.setSaveEnabled(true);
builder.setView(view);
editText.requestFocus();
builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
final String customText = editText.getText().toString().trim();
if (ContactsUtils.isGraphic(customText)) {
final List