Index: chrome/android/java/src/org/chromium/chrome/browser/payments/ui/EditorView.java |
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/payments/ui/EditorView.java b/chrome/android/java/src/org/chromium/chrome/browser/payments/ui/EditorView.java |
index dd3ab51f24561ef5d675491b7420371986e75677..5618878d850083e9b138dff569935798870be581 100644 |
--- a/chrome/android/java/src/org/chromium/chrome/browser/payments/ui/EditorView.java |
+++ b/chrome/android/java/src/org/chromium/chrome/browser/payments/ui/EditorView.java |
@@ -12,17 +12,22 @@ import android.os.AsyncTask; |
import android.os.Handler; |
import android.support.v7.widget.Toolbar.OnMenuItemClickListener; |
import android.telephony.PhoneNumberFormattingTextWatcher; |
+import android.text.InputFilter; |
+import android.text.Spanned; |
+import android.text.TextWatcher; |
import android.view.KeyEvent; |
import android.view.LayoutInflater; |
import android.view.MenuItem; |
import android.view.View; |
import android.view.View.OnClickListener; |
import android.view.ViewGroup; |
-import android.view.accessibility.AccessibilityEvent; |
import android.view.inputmethod.EditorInfo; |
-import android.widget.AutoCompleteTextView; |
import android.widget.Button; |
+import android.widget.CheckBox; |
+import android.widget.CompoundButton; |
+import android.widget.EditText; |
import android.widget.LinearLayout; |
+import android.widget.Spinner; |
import android.widget.TextView; |
import org.chromium.base.ApiCompatibilityUtils; |
@@ -30,6 +35,7 @@ import org.chromium.base.VisibleForTesting; |
import org.chromium.chrome.R; |
import org.chromium.chrome.browser.EmbedContentViewActivity; |
import org.chromium.chrome.browser.payments.ui.PaymentRequestUI.PaymentRequestObserverForTest; |
+import org.chromium.chrome.browser.preferences.autofill.CreditCardNumberFormattingTextWatcher; |
import org.chromium.chrome.browser.widget.AlwaysDismissedDialog; |
import org.chromium.chrome.browser.widget.DualControlLayout; |
import org.chromium.chrome.browser.widget.FadingShadow; |
@@ -37,10 +43,7 @@ import org.chromium.chrome.browser.widget.FadingShadowView; |
import java.util.ArrayList; |
import java.util.List; |
-import java.util.concurrent.CancellationException; |
-import java.util.concurrent.ExecutionException; |
-import java.util.concurrent.TimeUnit; |
-import java.util.concurrent.TimeoutException; |
+import java.util.regex.Pattern; |
import javax.annotation.Nullable; |
@@ -50,6 +53,8 @@ import javax.annotation.Nullable; |
*/ |
public class EditorView extends AlwaysDismissedDialog |
implements OnClickListener, DialogInterface.OnDismissListener { |
+ /** The indicator for input fields that are required. */ |
+ public static final String REQUIRED_FIELD_INDICATOR = "*"; |
/** Help page that the user is directed to when asking for help. */ |
private static final String HELP_URL = "https://support.google.com/chrome/answer/142893?hl=en"; |
@@ -57,17 +62,22 @@ public class EditorView extends AlwaysDismissedDialog |
private final Context mContext; |
private final PaymentRequestObserverForTest mObserverForTest; |
private final Handler mHandler; |
- private final AsyncTask<Void, Void, PhoneNumberFormattingTextWatcher> mPhoneFormatterTask; |
private final TextView.OnEditorActionListener mEditorActionListener; |
private final int mHalfRowMargin; |
- private final List<EditorTextField> mEditorTextFields; |
+ private final List<Validatable> mCheckableFields; |
+ private final List<EditText> mEditableTextFields; |
+ private final List<Spinner> mDropdownFields; |
+ private final InputFilter mCardNumberInputFilter; |
+ private final TextWatcher mCardNumberFormatter; |
+ @Nullable private TextWatcher mPhoneFormatter; |
private View mLayout; |
private EditorModel mEditorModel; |
private Button mDoneButton; |
private ViewGroup mDataView; |
private View mFooter; |
- @Nullable private AutoCompleteTextView mPhoneInput; |
+ @Nullable private TextView mCardInput; |
+ @Nullable private TextView mPhoneInput; |
/** |
* Builds the editor view. |
@@ -80,13 +90,6 @@ public class EditorView extends AlwaysDismissedDialog |
mContext = activity; |
mObserverForTest = observerForTest; |
mHandler = new Handler(); |
- mPhoneFormatterTask = new AsyncTask<Void, Void, PhoneNumberFormattingTextWatcher>() { |
- @Override |
- protected PhoneNumberFormattingTextWatcher doInBackground(Void... unused) { |
- return new PhoneNumberFormattingTextWatcher(); |
- } |
- }.execute(); |
- |
mEditorActionListener = new TextView.OnEditorActionListener() { |
@Override |
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { |
@@ -95,8 +98,8 @@ public class EditorView extends AlwaysDismissedDialog |
return true; |
} else if (actionId == EditorInfo.IME_ACTION_NEXT) { |
View next = v.focusSearch(View.FOCUS_FORWARD); |
- if (next != null && next instanceof AutoCompleteTextView) { |
- focusInputField(next); |
+ if (next != null) { |
+ next.requestFocus(); |
return true; |
} |
} |
@@ -106,7 +109,43 @@ public class EditorView extends AlwaysDismissedDialog |
mHalfRowMargin = activity.getResources().getDimensionPixelSize( |
R.dimen.payments_section_large_spacing); |
- mEditorTextFields = new ArrayList<>(); |
+ mCheckableFields = new ArrayList<>(); |
+ mEditableTextFields = new ArrayList<>(); |
+ mDropdownFields = new ArrayList<>(); |
+ |
+ final Pattern cardNumberPattern = Pattern.compile("^[\\d- ]*$"); |
+ mCardNumberInputFilter = new InputFilter() { |
+ @Override |
+ public CharSequence filter( |
+ CharSequence source, int start, int end, Spanned dest, int dstart, int dend) { |
+ // Accept deletions. |
+ if (start == end) return null; |
+ |
+ // Accept digits, "-", and spaces. |
+ if (cardNumberPattern.matcher(source.subSequence(start, end)).matches()) { |
+ return null; |
+ } |
+ |
+ // Reject everything else. |
+ return ""; |
+ } |
+ }; |
+ |
+ mCardNumberFormatter = new CreditCardNumberFormattingTextWatcher(); |
+ new AsyncTask<Void, Void, PhoneNumberFormattingTextWatcher>() { |
+ @Override |
+ protected PhoneNumberFormattingTextWatcher doInBackground(Void... unused) { |
+ return new PhoneNumberFormattingTextWatcher(); |
+ } |
+ |
+ @Override |
+ protected void onPostExecute(PhoneNumberFormattingTextWatcher result) { |
+ mPhoneFormatter = result; |
+ if (mPhoneInput != null) { |
+ mPhoneInput.addTextChangedListener(mPhoneFormatter); |
+ } |
+ } |
+ }.execute(); |
} |
/** Launches the Autofill help page on top of the current Context. */ |
@@ -158,27 +197,49 @@ public class EditorView extends AlwaysDismissedDialog |
} |
/** |
- * Checks if all of the fields in the form are valid and updates the displayed errors. |
+ * Checks if all of the fields in the form are valid and updates the displayed errors. If there |
+ * are any invalid fields, makes sure that one of them is focused. Called when user taps [SAVE]. |
* |
* @return Whether all fields contain valid information. |
*/ |
private boolean validateForm() { |
- final List<EditorTextField> invalidViews = getViewsWithInvalidInformation(); |
- |
- // Focus the first field that's invalid. |
- if (!invalidViews.isEmpty() && !invalidViews.contains(getCurrentFocus())) { |
- focusInputField(invalidViews.get(0)); |
- } |
+ final List<Validatable> invalidViews = getViewsWithInvalidInformation(true); |
// Iterate over all the fields to update what errors are displayed, which is necessary to |
// to clear existing errors on any newly valid fields. |
- for (int i = 0; i < mEditorTextFields.size(); i++) { |
- EditorTextField fieldView = mEditorTextFields.get(i); |
+ for (int i = 0; i < mCheckableFields.size(); i++) { |
+ Validatable fieldView = mCheckableFields.get(i); |
fieldView.updateDisplayedError(invalidViews.contains(fieldView)); |
} |
+ |
+ if (!invalidViews.isEmpty()) { |
+ // Make sure that focus is on an invalid field. |
+ Validatable focusedField = getValidatable(getCurrentFocus()); |
+ if (invalidViews.contains(focusedField)) { |
+ // The focused field is invalid, but it may be scrolled off screen. Scroll to it. |
+ focusedField.scrollToAndFocus(); |
+ } else { |
+ // Some fields are invalid, but none of the are focused. Scroll to the first invalid |
+ // field and focus it. |
+ invalidViews.get(0).scrollToAndFocus(); |
+ } |
+ } |
+ |
return invalidViews.isEmpty(); |
} |
+ /** @return The validatable item for the given view. */ |
+ private Validatable getValidatable(View v) { |
+ if (v instanceof TextView && v.getParent() != null |
+ && v.getParent() instanceof EditorTextField) { |
+ return (EditorTextField) v.getParent(); |
+ } else if (v instanceof Spinner && v.getTag() != null) { |
+ return (Validatable) v.getTag(); |
+ } else { |
+ return null; |
+ } |
+ } |
+ |
@Override |
public void onClick(View view) { |
if (view.getId() == R.id.payments_edit_done_button) { |
@@ -219,12 +280,13 @@ public class EditorView extends AlwaysDismissedDialog |
* much more human-parsable with inefficient LinearLayouts for half-width controls sharing rows. |
*/ |
private void prepareEditor() { |
- removeTextChangedListenerFromPhoneInputField(); |
- |
// Ensure the layout is empty. |
+ removeTextChangedListenersAndInputFilters(); |
mDataView = (ViewGroup) mLayout.findViewById(R.id.contents); |
mDataView.removeAllViews(); |
- mEditorTextFields.clear(); |
+ mCheckableFields.clear(); |
+ mEditableTextFields.clear(); |
+ mDropdownFields.clear(); |
// Add Views for each of the {@link EditorFields}. |
for (int i = 0; i < mEditorModel.getFields().size(); i++) { |
@@ -267,10 +329,14 @@ public class EditorView extends AlwaysDismissedDialog |
mDataView.addView(mFooter); |
} |
- private View addFieldViewToEditor(ViewGroup parent, EditorFieldModel fieldModel) { |
+ private View addFieldViewToEditor(ViewGroup parent, final EditorFieldModel fieldModel) { |
View childView = null; |
- if (fieldModel.getInputTypeHint() == EditorFieldModel.INPUT_TYPE_HINT_DROPDOWN) { |
+ if (fieldModel.getInputTypeHint() == EditorFieldModel.INPUT_TYPE_HINT_ICONS) { |
+ childView = new EditorIconsField(mContext, parent, fieldModel).getLayout(); |
+ } else if (fieldModel.getInputTypeHint() == EditorFieldModel.INPUT_TYPE_HINT_LABEL) { |
+ childView = new EditorLabelField(mContext, parent, fieldModel).getLayout(); |
+ } else if (fieldModel.getInputTypeHint() == EditorFieldModel.INPUT_TYPE_HINT_DROPDOWN) { |
Runnable prepareEditorRunnable = new Runnable() { |
@Override |
public void run() { |
@@ -280,16 +346,46 @@ public class EditorView extends AlwaysDismissedDialog |
} |
}; |
EditorDropdownField dropdownView = |
- new EditorDropdownField(mContext, fieldModel, prepareEditorRunnable); |
+ new EditorDropdownField(mContext, parent, fieldModel, prepareEditorRunnable); |
+ mCheckableFields.add(dropdownView); |
+ mDropdownFields.add(dropdownView.getDropdown()); |
childView = dropdownView.getLayout(); |
+ } else if (fieldModel.getInputTypeHint() == EditorFieldModel.INPUT_TYPE_HINT_CHECKBOX) { |
+ final CheckBox checkbox = new CheckBox(mLayout.getContext()); |
+ checkbox.setId(R.id.payments_edit_checkbox); |
+ checkbox.setText(fieldModel.getLabel()); |
+ checkbox.setChecked(fieldModel.isChecked()); |
+ checkbox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { |
+ @Override |
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { |
+ fieldModel.setIsChecked(isChecked); |
+ if (mObserverForTest != null) mObserverForTest.onPaymentRequestReadyToEdit(); |
+ } |
+ }); |
+ |
+ childView = checkbox; |
} else { |
- EditorTextField inputLayout = new EditorTextField(mLayout.getContext(), fieldModel, |
- mEditorActionListener, getPhoneFormatter(), mObserverForTest); |
- mEditorTextFields.add(inputLayout); |
+ InputFilter filter = null; |
+ TextWatcher formatter = null; |
+ if (fieldModel.getInputTypeHint() == EditorFieldModel.INPUT_TYPE_HINT_CREDIT_CARD) { |
+ filter = mCardNumberInputFilter; |
+ formatter = mCardNumberFormatter; |
+ } else if (fieldModel.getInputTypeHint() == EditorFieldModel.INPUT_TYPE_HINT_PHONE) { |
+ formatter = mPhoneFormatter; |
+ } |
- final AutoCompleteTextView input = inputLayout.getEditText(); |
- if (fieldModel.getInputTypeHint() == EditorFieldModel.INPUT_TYPE_HINT_PHONE) { |
+ EditorTextField inputLayout = new EditorTextField(mContext, fieldModel, |
+ mEditorActionListener, filter, formatter, mObserverForTest); |
+ mCheckableFields.add(inputLayout); |
+ |
+ EditText input = inputLayout.getEditText(); |
+ mEditableTextFields.add(input); |
+ |
+ if (fieldModel.getInputTypeHint() == EditorFieldModel.INPUT_TYPE_HINT_CREDIT_CARD) { |
+ assert mCardInput == null; |
+ mCardInput = input; |
+ } else if (fieldModel.getInputTypeHint() == EditorFieldModel.INPUT_TYPE_HINT_PHONE) { |
assert mPhoneInput == null; |
mPhoneInput = input; |
} |
@@ -322,12 +418,13 @@ public class EditorView extends AlwaysDismissedDialog |
show(); |
// Immediately focus the first invalid field to make it faster to edit. |
- final List<EditorTextField> invalidViews = getViewsWithInvalidInformation(); |
+ final List<Validatable> invalidViews = getViewsWithInvalidInformation(false); |
if (!invalidViews.isEmpty()) { |
mHandler.post(new Runnable() { |
@Override |
public void run() { |
- focusInputField(invalidViews.get(0)); |
+ invalidViews.get(0).scrollToAndFocus(); |
+ if (mObserverForTest != null) mObserverForTest.onPaymentRequestReadyToEdit(); |
} |
}); |
} |
@@ -335,44 +432,45 @@ public class EditorView extends AlwaysDismissedDialog |
@Override |
public void onDismiss(DialogInterface dialog) { |
- removeTextChangedListenerFromPhoneInputField(); |
+ removeTextChangedListenersAndInputFilters(); |
mEditorModel.cancel(); |
if (mObserverForTest != null) mObserverForTest.onPaymentRequestEditorDismissed(); |
} |
- /** @return All the EditorTextFields that exist in the EditorView. */ |
- @VisibleForTesting |
- public List<EditorTextField> getEditorTextFields() { |
- return mEditorTextFields; |
- } |
- |
- private void removeTextChangedListenerFromPhoneInputField() { |
- if (mPhoneInput != null) mPhoneInput.removeTextChangedListener(getPhoneFormatter()); |
- mPhoneInput = null; |
- } |
+ private void removeTextChangedListenersAndInputFilters() { |
+ if (mCardInput != null) { |
+ mCardInput.removeTextChangedListener(mCardNumberFormatter); |
+ mCardInput.setFilters(new InputFilter[0]); // Null is not allowed. |
+ mCardInput = null; |
+ } |
- /** Immediately returns the phone formatter or null if it has not initialized yet. */ |
- private PhoneNumberFormattingTextWatcher getPhoneFormatter() { |
- try { |
- return mPhoneFormatterTask.get(0, TimeUnit.MILLISECONDS); |
- } catch (CancellationException | ExecutionException | InterruptedException |
- | TimeoutException e) { |
- return null; |
+ if (mPhoneInput != null) { |
+ mPhoneInput.removeTextChangedListener(mPhoneFormatter); |
+ mPhoneInput = null; |
} |
} |
- private List<EditorTextField> getViewsWithInvalidInformation() { |
- List<EditorTextField> invalidViews = new ArrayList<>(); |
- for (int i = 0; i < mEditorTextFields.size(); i++) { |
- EditorTextField fieldView = mEditorTextFields.get(i); |
- if (!fieldView.getFieldModel().isValid()) invalidViews.add(fieldView); |
+ private List<Validatable> getViewsWithInvalidInformation(boolean findAll) { |
+ List<Validatable> invalidViews = new ArrayList<>(); |
+ for (int i = 0; i < mCheckableFields.size(); i++) { |
+ Validatable fieldView = mCheckableFields.get(i); |
+ if (!fieldView.isValid()) { |
+ invalidViews.add(fieldView); |
+ if (!findAll) break; |
+ } |
} |
return invalidViews; |
} |
- private void focusInputField(View view) { |
- view.requestFocus(); |
- view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED); |
- if (mObserverForTest != null) mObserverForTest.onPaymentRequestReadyToEdit(); |
+ /** @return All editable text fields in the editor. Used only for tests. */ |
+ @VisibleForTesting |
+ public List<EditText> getEditableTextFieldsForTest() { |
+ return mEditableTextFields; |
+ } |
+ |
+ /** @return All dropdown fields in the editor. Used only for tests. */ |
+ @VisibleForTesting |
+ public List<Spinner> getDropdownFieldsForTest() { |
+ return mDropdownFields; |
} |
} |