/*
Copyright © 2013-2014, Silent Circle, LLC.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Any redistribution, use, or modification is done solely for personal
benefit and not for any commercial purpose or for monetary gain
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name Silent Circle nor the names of its contributors may
be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL SILENT CIRCLE, LLC BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/*
* This implementation is edited version of original Android sources.
*/
/*
* 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.silentcircle.contacts.editor;
import android.annotation.TargetApi;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnShowListener;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextUtils.TruncateAt;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.view.Gravity;
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.EditText;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.Spinner;
import android.widget.TextView;
import com.silentcircle.contacts.model.RawContactDelta;
import com.silentcircle.contacts.model.account.AccountType;
import com.silentcircle.contacts.model.dataitem.DataKind;
import com.silentcircle.contacts.ContactsUtils;
import com.silentcircle.contacts.R;
import com.silentcircle.contacts.model.RawContactModifier;
import com.silentcircle.contacts.utils.DialogManager;
import com.silentcircle.contacts.utils.DialogManager.DialogShowingView;
import java.util.List;
/**
* Base class for editors that handles labels and values. Uses
* {@link com.silentcircle.contacts.model.RawContactDelta.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;
private View mDeleteContainer;
private ImageView mDelete;
private DataKind mKind;
private RawContactDelta.ValuesDelta mEntry;
private RawContactDelta mState;
private boolean mReadOnly;
private boolean mWasEmpty = true;
private boolean mIsDeletable = true;
private boolean mIsAttachedToWindow;
private AccountType.EditType mType;
private ViewIdGenerator mViewIdGenerator;
private DialogManager mDialogManager = null;
private EditorListener mListener;
protected int mMinLineItemHeight;
private Context mContext;
/**
* A marker in the spinner adapter of the currently selected custom type.
*/
public static final AccountType.EditType CUSTOM_SELECTION = new AccountType.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);
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public LabeledEditorView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context);
}
private void init(Context context) {
mContext = 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);
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);
}
}
});
}
});
}
@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 deleteEditor() {
// Keep around in model, but mark as deleted
mEntry.markDeleted();
final ViewGroup victimParent = (ViewGroup)this.getParent();
if (victimParent != null) {
victimParent.removeView(this);
}
// Remove the view
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH)
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.GONE);
}
}
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;
}
@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 RawContactDelta.ValuesDelta getEntry() {
return mEntry;
}
protected AccountType.EditType getType() {
return mType;
}
/**
* Build the current label state based on selected {@link com.silentcircle.contacts.model.account.AccountType.EditType} and
* possible custom label string.
*/
private void rebuildLabel() {
if (mEditTypeAdapter == null) {
mEditTypeAdapter = new EditTypeAdapter(mContext);
mLabel.setAdapter(mEditTypeAdapter);
}
if (mEditTypeAdapter.hasCustomSelection()) {
mLabel.setSelection(mEditTypeAdapter.getPosition(CUSTOM_SELECTION));
}
else {
mLabel.setSelection(mEditTypeAdapter.getPosition(mType));
}
}
@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);
}
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;
}
}
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 com.silentcircle.contacts.model.RawContactDelta.ValuesDelta} describing the content to edit.
*/
@Override
public void setValues(DataKind kind, RawContactDelta.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 (hasTypes) {
mType = RawContactModifier.getCurrentType(entry, kind);
rebuildLabel();
}
}
public RawContactDelta.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.
* <p>
* 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(mContext);
final LayoutInflater layoutInflater = LayoutInflater.from(mContext /*builder.getContext() check this */ );
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<AccountType.EditType> allTypes = RawContactModifier.getValidTypes(mState, mKind, null);
mType = null;
for (AccountType.EditType editType : allTypes) {
if (editType.customColumn != null) {
mType = editType;
break;
}
}
if (mType == null)
return;
mEntry.put(mKind.typeColumn, mType.rawValue);
mEntry.put(mType.customColumn, customText);
rebuildLabel();
// see comment below requestFocusForFirstEditField();
onLabelRebuilt();
}
}
});
builder.setNegativeButton(android.R.string.cancel, null);
final AlertDialog dialog = builder.create();
dialog.setOnShowListener(new OnShowListener() {
@Override
public void onShow(DialogInterface dialogInterface) {
updateCustomDialogOkButtonState(dialog, editText);
}
});
editText.addTextChangedListener(new TextWatcher() {
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void afterTextChanged(Editable s) {
updateCustomDialogOkButtonState(dialog, editText);
}
});
dialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
return dialog;
}
void updateCustomDialogOkButtonState(AlertDialog dialog, EditText editText) {
final Button okButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
okButton.setEnabled(!TextUtils.isEmpty(editText.getText().toString().trim()));
}
/**
* Called after the label has changed (either chosen from the list or entered in the Dialog)
*/
protected void onLabelRebuilt() {
}
protected void onTypeSelectionChange(int position) {
AccountType.EditType selected = mEditTypeAdapter.getItem(position);
// See if the selection has in fact changed
if (mEditTypeAdapter.hasCustomSelection() && selected == CUSTOM_SELECTION) {
return;
}
if (mType == selected && mType.customColumn == null) {
return;
}
if (selected.customColumn != null) {
showDialog(DIALOG_ID_CUSTOM);
}
else {
// User picked type, and we're sure it's ok to actually write the entry.
mType = selected;
mEntry.put(mKind.typeColumn, mType.rawValue);
rebuildLabel();
// don't do - text not shown anymore. Don't know why: requestFocusForFirstEditField();
onLabelRebuilt();
}
}
void showDialog(int bundleDialogId) {
Bundle bundle = new Bundle();
bundle.putInt(DIALOG_ID_KEY, bundleDialogId);
getDialogManager().showDialogInView(this, bundle);
}
private DialogManager getDialogManager() {
if (mDialogManager == null) {
Context context = getContext();
if (!(context instanceof DialogManager.DialogShowingViewActivity)) {
throw new IllegalStateException(
"View must be hosted in an Activity that implements DialogManager.DialogShowingViewActivity");
}
mDialogManager = ((DialogManager.DialogShowingViewActivity)context).getDialogManager();
}
return mDialogManager;
}
@Override
public Dialog createDialog(Bundle bundle) {
if (bundle == null) throw new IllegalArgumentException("bundle must not be null");
int dialogId = bundle.getInt(DIALOG_ID_KEY);
switch (dialogId) {
case DIALOG_ID_CUSTOM:
return createCustomDialog();
default:
throw new IllegalArgumentException("Invalid dialogId: " + dialogId);
}
}
protected abstract void requestFocusForFirstEditField();
private class EditTypeAdapter extends ArrayAdapter<AccountType.EditType> {
private final LayoutInflater mInflater;
private boolean mHasCustomSelection;
private int mTextColor;
public EditTypeAdapter(Context context) {
super(context, 0);
mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
mTextColor = context.getResources().getColor(R.color.secondary_text_color);
if (mType != null && mType.customColumn != null) {
// Use custom label string when present
final String customText = mEntry.getAsString(mType.customColumn);
if (customText != null) {
add(CUSTOM_SELECTION);
mHasCustomSelection = true;
}
}
for (AccountType.EditType type : RawContactModifier.getValidTypes(mState, mKind, mType)) {
add(type);
}
}
public boolean hasCustomSelection() {
return mHasCustomSelection;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
return createViewFromResource(position, convertView, parent, android.R.layout.simple_spinner_item);
}
@Override
public View getDropDownView(int position, View convertView, ViewGroup parent) {
return createViewFromResource(position, convertView, parent, android.R.layout.simple_spinner_dropdown_item);
}
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
private View createViewFromResource(int position, View convertView, ViewGroup parent, int resource) {
TextView textView;
if (convertView == null) {
textView = (TextView) mInflater.inflate(resource, parent, false);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH)
textView.setAllCaps(true);
textView.setGravity(Gravity.RIGHT | Gravity.CENTER_VERTICAL);
textView.setTextAppearance(mContext, android.R.style.TextAppearance_Small);
textView.setTextColor(mTextColor);
textView.setBackgroundColor(mContext.getResources().getColor(R.color.background_primary));
textView.setEllipsize(TruncateAt.MIDDLE);
}
else {
textView = (TextView) convertView;
}
AccountType.EditType type = getItem(position);
String text;
if (type == CUSTOM_SELECTION) {
text = mEntry.getAsString(mType.customColumn);
}
else {
text = getContext().getString(type.labelRes);
}
textView.setText(text);
return textView;
}
}
}