// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.payments.ui;
import android.content.Context;
import android.text.Editable;
import android.text.InputFilter;
import android.text.InputType;
import android.text.TextWatcher;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
import android.widget.ArrayAdapter;
import android.widget.AutoCompleteTextView;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView.OnEditorActionListener;
import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.base.VisibleForTesting;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.payments.ui.PaymentRequestUI.PaymentRequestObserverForTest;
import org.chromium.chrome.browser.widget.CompatibilityTextInputLayout;
import org.chromium.chrome.browser.widget.TintedDrawable;
import javax.annotation.Nullable;
/** Handles validation and display of one field from the {@link EditorFieldModel}. */
@VisibleForTesting
public class EditorTextField extends FrameLayout implements EditorFieldView, View.OnClickListener {
private EditorFieldModel mEditorFieldModel;
private CompatibilityTextInputLayout mInputLayout;
private AutoCompleteTextView mInput;
private ImageView mActionIcon;
private boolean mHasFocusedAtLeastOnce;
@Nullable private PaymentRequestObserverForTest mObserverForTest;
public EditorTextField(Context context, final EditorFieldModel fieldModel,
OnEditorActionListener actionlistener, @Nullable InputFilter filter,
@Nullable TextWatcher formatter, @Nullable PaymentRequestObserverForTest observer) {
super(context);
assert fieldModel.getInputTypeHint() != EditorFieldModel.INPUT_TYPE_HINT_DROPDOWN;
mEditorFieldModel = fieldModel;
mObserverForTest = observer;
LayoutInflater.from(context).inflate(R.layout.payments_request_editor_textview, this, true);
mInputLayout = (CompatibilityTextInputLayout) findViewById(R.id.text_input_layout);
// Build up the label. Required fields are indicated by appending a '*'.
CharSequence label = fieldModel.getLabel();
if (fieldModel.isRequired()) label = label + EditorView.REQUIRED_FIELD_INDICATOR;
mInputLayout.setHint(label);
mInput = (AutoCompleteTextView) mInputLayout.findViewById(R.id.text_view);
mInput.setText(fieldModel.getValue());
mInput.setContentDescription(label);
mInput.setOnEditorActionListener(actionlistener);
if (fieldModel.getIconAction() != null) {
mActionIcon = (ImageView) findViewById(R.id.action_icon);
mActionIcon.setImageDrawable(
TintedDrawable.constructTintedDrawable(context.getResources(),
fieldModel.getActionIconResourceId(), R.color.light_active_color));
mActionIcon.setContentDescription(context.getResources().getString(
fieldModel.getActionDescriptionForAccessibility()));
mActionIcon.setOnClickListener(this);
mActionIcon.setVisibility(VISIBLE);
}
// Validate the field when the user de-focuses it.
mInput.setOnFocusChangeListener(new OnFocusChangeListener() {
@Override
public void onFocusChange(View v, boolean hasFocus) {
if (hasFocus) {
mHasFocusedAtLeastOnce = true;
} else if (mHasFocusedAtLeastOnce) {
// Show no errors until the user has already tried to edit the field once.
updateDisplayedError(!mEditorFieldModel.isValid());
}
}
});
// Update the model as the user edits the field.
mInput.addTextChangedListener(new TextWatcher() {
@Override
public void afterTextChanged(Editable s) {
fieldModel.setValue(s.toString());
updateDisplayedError(false);
if (mObserverForTest != null) {
mObserverForTest.onPaymentRequestEditorTextUpdate();
}
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {}
});
// Display any autofill suggestions.
if (fieldModel.getSuggestions() != null && !fieldModel.getSuggestions().isEmpty()) {
mInput.setAdapter(new ArrayAdapter<CharSequence>(getContext(),
android.R.layout.simple_spinner_dropdown_item,
fieldModel.getSuggestions()));
mInput.setThreshold(0);
}
if (filter != null) mInput.setFilters(new InputFilter[] {filter});
if (formatter != null) mInput.addTextChangedListener(formatter);
switch (fieldModel.getInputTypeHint()) {
case EditorFieldModel.INPUT_TYPE_HINT_CREDIT_CARD:
// Intentionally fall through.
//
// There's no keyboard that allows numbers, spaces, and "-" only, so use the phone
// keyboard instead. The phone keyboard has more symbols than necessary. A filter
// should be used to prevent input of phone number symbols that are not relevant for
// credit card numbers, e.g., "+", "*", and "#".
//
// The number keyboard is not suitable, because it filters out everything except
// digits.
case EditorFieldModel.INPUT_TYPE_HINT_PHONE:
// Show the keyboard with numbers and phone-related symbols.
mInput.setInputType(InputType.TYPE_CLASS_PHONE);
break;
case EditorFieldModel.INPUT_TYPE_HINT_EMAIL:
mInput.setInputType(InputType.TYPE_CLASS_TEXT
| InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS);
break;
case EditorFieldModel.INPUT_TYPE_HINT_STREET_LINES:
// TODO(rouslan): Provide a hint to the keyboard that the street lines are
// likely to have numbers.
mInput.setInputType(InputType.TYPE_CLASS_TEXT
| InputType.TYPE_TEXT_FLAG_CAP_WORDS
| InputType.TYPE_TEXT_FLAG_MULTI_LINE
| InputType.TYPE_TEXT_VARIATION_POSTAL_ADDRESS);
break;
case EditorFieldModel.INPUT_TYPE_HINT_PERSON_NAME:
mInput.setInputType(InputType.TYPE_CLASS_TEXT
| InputType.TYPE_TEXT_FLAG_CAP_WORDS
| InputType.TYPE_TEXT_VARIATION_PERSON_NAME);
break;
case EditorFieldModel.INPUT_TYPE_HINT_ALPHA_NUMERIC:
// Intentionally fall through.
// TODO(rouslan): Provide a hint to the keyboard that postal code and sorting
// code are likely to have numbers.
case EditorFieldModel.INPUT_TYPE_HINT_REGION:
mInput.setInputType(InputType.TYPE_CLASS_TEXT
| InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS
| InputType.TYPE_TEXT_VARIATION_POSTAL_ADDRESS);
break;
default:
mInput.setInputType(InputType.TYPE_CLASS_TEXT
| InputType.TYPE_TEXT_FLAG_CAP_WORDS
| InputType.TYPE_TEXT_VARIATION_POSTAL_ADDRESS);
break;
}
}
@Override
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mActionIcon != null) {
if (mActionIcon.getMeasuredWidth() == 0) {
mActionIcon.measure(widthMeasureSpec, heightMeasureSpec);
}
// Padding at the end of mInput to preserve space for mActionIcon.
ApiCompatibilityUtils.setPaddingRelative(mInput,
ApiCompatibilityUtils.getPaddingStart(mInput), mInput.getPaddingTop(),
mActionIcon.getWidth(), mInput.getPaddingBottom());
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
public void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (changed && mActionIcon != null) {
// Align the bottom of mActionIcon to the bottom of mInput (mActionIcon overlaps
// mInput).
// Note one: mActionIcon can not be put inside mInputLayout to display on top of
// mInput since mInputLayout is LinearLayout in essential.
// Note two: mActionIcon and mInput can not be put in ViewGroup to display over each
// other inside mInputLayout since mInputLayout must contain an instance of EditText
// child view.
// Note three: mInputLayout's bottom changes when displaying error.
float offset = mInputLayout.getY() + mInput.getY() + (float) mInput.getHeight()
- (float) mActionIcon.getHeight() - mActionIcon.getTop();
mActionIcon.setTranslationY(offset);
}
}
@Override
public void onClick(View v) {
mEditorFieldModel.getIconAction().run();
}
/** @return The EditorFieldModel that the TextView represents. */
public EditorFieldModel getFieldModel() {
return mEditorFieldModel;
}
/** @return The AutoCompleteTextView this field associates*/
public AutoCompleteTextView getEditText() {
return mInput;
}
@Override
public boolean isValid() {
return mEditorFieldModel.isValid();
}
@Override
public void updateDisplayedError(boolean showError) {
mInputLayout.setError(showError ? mEditorFieldModel.getErrorMessage() : null);
}
@Override
public void scrollToAndFocus() {
ViewGroup parent = (ViewGroup) getParent();
if (parent != null) parent.requestChildFocus(this, this);
requestFocus();
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
}
@Override
public void update() {
mInput.setText(mEditorFieldModel.getValue());
}
}