blob: d6f123f9ce10c1d7c35652b29eb3cc89fd7db899 [file] [log] [blame]
// Copyright 2014 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.autofill;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.os.Build;
import android.os.Handler;
import android.support.annotation.IntDef;
import android.support.v4.view.MarginLayoutParamsCompat;
import android.support.v4.view.ViewCompat;
import android.text.Editable;
import android.text.InputFilter;
import android.text.TextWatcher;
import android.view.LayoutInflater;
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.view.inputmethod.InputMethodManager;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.PopupWindow;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import android.widget.TextView;
import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.base.VisibleForTesting;
import org.chromium.base.task.AsyncTask;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.ChromeActivity;
import org.chromium.ui.modaldialog.DialogDismissalCause;
import org.chromium.ui.modaldialog.ModalDialogManager;
import org.chromium.ui.modaldialog.ModalDialogProperties;
import org.chromium.ui.modelutil.PropertyModel;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Calendar;
/**
* A prompt that bugs users to enter their CVC when unmasking a Wallet instrument (credit card).
*/
public class CardUnmaskPrompt
implements TextWatcher, OnClickListener, ModalDialogProperties.Controller {
private static CardUnmaskObserverForTest sObserverForTest;
private final CardUnmaskPromptDelegate mDelegate;
private PropertyModel mDialogModel;
private boolean mShouldRequestExpirationDate;
private final View mMainView;
private final TextView mInstructions;
private final TextView mNoRetryErrorMessage;
private final EditText mCardUnmaskInput;
private final EditText mMonthInput;
private final EditText mYearInput;
private final View mExpirationContainer;
private final TextView mNewCardLink;
private final TextView mErrorMessage;
private final CheckBox mStoreLocallyCheckbox;
private final ImageView mStoreLocallyTooltipIcon;
private PopupWindow mStoreLocallyTooltipPopup;
private final ViewGroup mControlsContainer;
private final View mVerificationOverlay;
private final ProgressBar mVerificationProgressBar;
private final TextView mVerificationView;
private final long mSuccessMessageDurationMilliseconds;
private int mThisYear;
private int mThisMonth;
private boolean mValidationWaitsForCalendarTask;
private ModalDialogManager mModalDialogManager;
private Context mContext;
private String mCvcErrorMessage;
private String mExpirationMonthErrorMessage;
private String mExpirationYearErrorMessage;
private String mExpirationDateErrorMessage;
private String mCvcAndExpirationErrorMessage;
private boolean mDidFocusOnMonth;
private boolean mDidFocusOnYear;
private boolean mDidFocusOnCvc;
private static final int EXPIRATION_FIELDS_LENGTH = 2;
@IntDef({ErrorType.EXPIRATION_MONTH, ErrorType.EXPIRATION_YEAR, ErrorType.EXPIRATION_DATE,
ErrorType.CVC, ErrorType.CVC_AND_EXPIRATION, ErrorType.NOT_ENOUGH_INFO, ErrorType.NONE})
@Retention(RetentionPolicy.SOURCE)
public @interface ErrorType {
int EXPIRATION_MONTH = 1;
int EXPIRATION_YEAR = 2;
int EXPIRATION_DATE = 3;
int CVC = 4;
int CVC_AND_EXPIRATION = 5;
int NOT_ENOUGH_INFO = 6;
int NONE = 7;
}
/**
* An interface to handle the interaction with an CardUnmaskPrompt object.
*/
public interface CardUnmaskPromptDelegate {
/**
* Called when the dialog has been dismissed.
*/
void dismissed();
/**
* Returns whether |userResponse| represents a valid value.
* @param userResponse A CVC entered by the user.
*/
boolean checkUserInputValidity(String userResponse);
/**
* Called when the user has entered a value and pressed "verify".
* @param cvc The value the user entered (a CVC), or an empty string if the user canceled.
* @param month The value the user selected for expiration month, if any.
* @param year The value the user selected for expiration month, if any.
* @param shouldStoreLocally The state of the "Save locally?" checkbox at the time.
*/
void onUserInput(String cvc, String month, String year, boolean shouldStoreLocally);
/**
* Called when the "New card?" link has been clicked.
* The controller will call update() in response.
*/
void onNewCardLinkClicked();
/**
* Returns the expected length of the CVC for the card.
*/
int getExpectedCvcLength();
}
/**
* A test-only observer for the unmasking prompt.
*/
public interface CardUnmaskObserverForTest {
/**
* Called when typing the CVC input is possible.
*/
void onCardUnmaskPromptReadyForInput(CardUnmaskPrompt prompt);
/**
* Called when clicking "Verify" or "Continue" (the positive button) is possible.
*/
void onCardUnmaskPromptReadyToUnmask(CardUnmaskPrompt prompt);
/**
* Called when the input values in the unmask prompt have been validated.
*/
void onCardUnmaskPromptValidationDone(CardUnmaskPrompt prompt);
}
public CardUnmaskPrompt(Context context, CardUnmaskPromptDelegate delegate, String title,
String instructions, String confirmButtonLabel, int drawableId,
boolean shouldRequestExpirationDate, boolean canStoreLocally,
boolean defaultToStoringLocally, long successMessageDurationMilliseconds) {
mDelegate = delegate;
LayoutInflater inflater = LayoutInflater.from(context);
View v = inflater.inflate(R.layout.autofill_card_unmask_prompt, null);
mInstructions = (TextView) v.findViewById(R.id.instructions);
mInstructions.setText(instructions);
mMainView = v;
mNoRetryErrorMessage = (TextView) v.findViewById(R.id.no_retry_error_message);
mCardUnmaskInput = (EditText) v.findViewById(R.id.card_unmask_input);
mMonthInput = (EditText) v.findViewById(R.id.expiration_month);
mYearInput = (EditText) v.findViewById(R.id.expiration_year);
mExpirationContainer = v.findViewById(R.id.expiration_container);
mNewCardLink = (TextView) v.findViewById(R.id.new_card_link);
mNewCardLink.setOnClickListener(this);
mErrorMessage = (TextView) v.findViewById(R.id.error_message);
mStoreLocallyCheckbox = (CheckBox) v.findViewById(R.id.store_locally_checkbox);
mStoreLocallyCheckbox.setChecked(canStoreLocally && defaultToStoringLocally);
mStoreLocallyTooltipIcon = (ImageView) v.findViewById(R.id.store_locally_tooltip_icon);
mStoreLocallyTooltipIcon.setOnClickListener(this);
if (!canStoreLocally) v.findViewById(R.id.store_locally_container).setVisibility(View.GONE);
mControlsContainer = (ViewGroup) v.findViewById(R.id.controls_container);
mVerificationOverlay = v.findViewById(R.id.verification_overlay);
mVerificationProgressBar = (ProgressBar) v.findViewById(R.id.verification_progress_bar);
mVerificationView = (TextView) v.findViewById(R.id.verification_message);
mSuccessMessageDurationMilliseconds = successMessageDurationMilliseconds;
((ImageView) v.findViewById(R.id.cvc_hint_image)).setImageResource(drawableId);
Resources resources = context.getResources();
mDialogModel = new PropertyModel.Builder(ModalDialogProperties.ALL_KEYS)
.with(ModalDialogProperties.CONTROLLER, this)
.with(ModalDialogProperties.TITLE, title)
.with(ModalDialogProperties.CUSTOM_VIEW, v)
.with(ModalDialogProperties.POSITIVE_BUTTON_TEXT, confirmButtonLabel)
.with(ModalDialogProperties.NEGATIVE_BUTTON_TEXT, resources,
R.string.cancel)
.build();
mShouldRequestExpirationDate = shouldRequestExpirationDate;
mThisYear = -1;
mThisMonth = -1;
if (mShouldRequestExpirationDate)
new CalendarTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
// Set the max length of the CVC field.
mCardUnmaskInput.setFilters(
new InputFilter[] {new InputFilter.LengthFilter(mDelegate.getExpectedCvcLength())});
// Hitting the "submit" button on the software keyboard should submit the form if valid.
mCardUnmaskInput.setOnEditorActionListener((v14, actionId, event) -> {
if (actionId == EditorInfo.IME_ACTION_DONE) {
onClick(mDialogModel, ModalDialogProperties.ButtonType.POSITIVE);
return true;
}
return false;
});
// Create the listeners to be notified when the user focuses out the input fields.
mCardUnmaskInput.setOnFocusChangeListener((v13, hasFocus) -> {
mDidFocusOnCvc = true;
validate();
});
mMonthInput.setOnFocusChangeListener((v12, hasFocus) -> {
mDidFocusOnMonth = true;
validate();
});
mYearInput.setOnFocusChangeListener((v1, hasFocus) -> {
mDidFocusOnYear = true;
validate();
});
// Load the error messages to show to the user.
mCvcErrorMessage =
resources.getString(R.string.autofill_card_unmask_prompt_error_try_again_cvc);
mExpirationMonthErrorMessage = resources.getString(
R.string.autofill_card_unmask_prompt_error_try_again_expiration_month);
mExpirationYearErrorMessage = resources.getString(
R.string.autofill_card_unmask_prompt_error_try_again_expiration_year);
mExpirationDateErrorMessage = resources.getString(
R.string.autofill_card_unmask_prompt_error_try_again_expiration_date);
mCvcAndExpirationErrorMessage = resources.getString(
R.string.autofill_card_unmask_prompt_error_try_again_cvc_and_expiration);
}
/**
* Avoids disk reads for timezone when getting the default instance of Calendar.
*/
private class CalendarTask extends AsyncTask<Calendar> {
@Override
protected Calendar doInBackground() {
return Calendar.getInstance();
}
@Override
protected void onPostExecute(Calendar result) {
mThisYear = result.get(Calendar.YEAR);
mThisMonth = result.get(Calendar.MONTH) + 1;
if (mValidationWaitsForCalendarTask) validate();
}
}
/**
* Show the dialog. If activity is null this method will not do anything.
*/
public void show(ChromeActivity activity) {
if (activity == null) return;
mContext = activity;
mModalDialogManager = activity.getModalDialogManager();
mModalDialogManager.showDialog(mDialogModel, ModalDialogManager.ModalDialogType.APP);
showExpirationDateInputsInputs();
// Override the View.OnClickListener so that pressing the positive button doesn't dismiss
// the dialog.
mDialogModel.set(ModalDialogProperties.POSITIVE_BUTTON_DISABLED, true);
mCardUnmaskInput.addTextChangedListener(this);
mCardUnmaskInput.post(() -> setInitialFocus());
}
public void update(String title, String instructions, boolean shouldRequestExpirationDate) {
mDialogModel.set(ModalDialogProperties.TITLE, title);
mInstructions.setText(instructions);
mShouldRequestExpirationDate = shouldRequestExpirationDate;
if (mShouldRequestExpirationDate && (mThisYear == -1 || mThisMonth == -1)) {
new CalendarTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
showExpirationDateInputsInputs();
}
public void dismiss(@DialogDismissalCause int dismissalCause) {
mModalDialogManager.dismissDialog(mDialogModel, dismissalCause);
}
public void disableAndWaitForVerification() {
setInputsEnabled(false);
setOverlayVisibility(View.VISIBLE);
mVerificationProgressBar.setVisibility(View.VISIBLE);
mVerificationView.setText(R.string.autofill_card_unmask_verification_in_progress);
mVerificationView.announceForAccessibility(mVerificationView.getText());
clearInputError();
}
public void verificationFinished(String errorMessage, boolean allowRetry) {
if (errorMessage != null) {
setOverlayVisibility(View.GONE);
if (allowRetry) {
showErrorMessage(errorMessage);
setInputsEnabled(true);
setInitialFocus();
if (!mShouldRequestExpirationDate) mNewCardLink.setVisibility(View.VISIBLE);
} else {
clearInputError();
setNoRetryError(errorMessage);
}
} else {
Runnable dismissRunnable = () -> dismiss(DialogDismissalCause.ACTION_ON_CONTENT);
if (mSuccessMessageDurationMilliseconds > 0) {
mVerificationProgressBar.setVisibility(View.GONE);
mMainView.findViewById(R.id.verification_success).setVisibility(View.VISIBLE);
mVerificationView.setText(R.string.autofill_card_unmask_verification_success);
mVerificationView.announceForAccessibility(mVerificationView.getText());
new Handler().postDelayed(dismissRunnable, mSuccessMessageDurationMilliseconds);
} else {
new Handler().post(dismissRunnable);
}
}
}
@Override
public void afterTextChanged(Editable s) {
validate();
}
/**
* Validates the values of the input fields to determine whether the submit button should be
* enabled. Also displays a detailed error message and highlights the fields for which the value
* is wrong. Finally checks whether the focuse should move to the next field.
*/
private void validate() {
@ErrorType int errorType = getExpirationAndCvcErrorType();
mDialogModel.set(
ModalDialogProperties.POSITIVE_BUTTON_DISABLED, errorType != ErrorType.NONE);
showDetailedErrorMessage(errorType);
moveFocus(errorType);
if (sObserverForTest != null) {
sObserverForTest.onCardUnmaskPromptValidationDone(this);
if (!mDialogModel.get(ModalDialogProperties.POSITIVE_BUTTON_DISABLED)) {
sObserverForTest.onCardUnmaskPromptReadyToUnmask(this);
}
}
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {}
@Override
public void onClick(View v) {
if (v == mStoreLocallyTooltipIcon) {
onTooltipIconClicked();
} else {
assert v == mNewCardLink;
onNewCardLinkClicked();
}
}
private void showExpirationDateInputsInputs() {
if (!mShouldRequestExpirationDate || mExpirationContainer.getVisibility() == View.VISIBLE) {
return;
}
mExpirationContainer.setVisibility(View.VISIBLE);
mCardUnmaskInput.setEms(3);
mMonthInput.addTextChangedListener(this);
mYearInput.addTextChangedListener(this);
}
private void onTooltipIconClicked() {
// Don't show the popup if there's already one showing (or one has been dismissed
// recently). This prevents a tap on the (?) from hiding and then immediately re-showing
// the popup.
if (mStoreLocallyTooltipPopup != null) return;
mStoreLocallyTooltipPopup = new PopupWindow(mContext);
TextView text = new TextView(mContext);
text.setText(R.string.autofill_card_unmask_prompt_storage_tooltip);
// Width is the dialog's width less the margins and padding around the checkbox and
// icon.
text.setWidth(mMainView.getWidth() - ViewCompat.getPaddingStart(mStoreLocallyCheckbox)
- ViewCompat.getPaddingEnd(mStoreLocallyTooltipIcon)
- MarginLayoutParamsCompat.getMarginStart((RelativeLayout.LayoutParams)
mStoreLocallyCheckbox.getLayoutParams())
- MarginLayoutParamsCompat.getMarginEnd((RelativeLayout.LayoutParams)
mStoreLocallyTooltipIcon.getLayoutParams()));
text.setTextColor(Color.WHITE);
Resources resources = mContext.getResources();
int hPadding = resources.getDimensionPixelSize(R.dimen.autofill_tooltip_horizontal_padding);
int vPadding = resources.getDimensionPixelSize(R.dimen.autofill_tooltip_vertical_padding);
text.setPadding(hPadding, vPadding, hPadding, vPadding);
mStoreLocallyTooltipPopup.setContentView(text);
mStoreLocallyTooltipPopup.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
mStoreLocallyTooltipPopup.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
mStoreLocallyTooltipPopup.setOutsideTouchable(true);
mStoreLocallyTooltipPopup.setBackgroundDrawable(ApiCompatibilityUtils.getDrawable(
resources, R.drawable.store_locally_tooltip_background));
mStoreLocallyTooltipPopup.setOnDismissListener(() -> {
Handler h = new Handler();
h.postDelayed(() -> mStoreLocallyTooltipPopup = null, 200);
});
mStoreLocallyTooltipPopup.showAsDropDown(mStoreLocallyCheckbox,
ViewCompat.getPaddingStart(mStoreLocallyCheckbox), 0);
text.announceForAccessibility(text.getText());
}
private void onNewCardLinkClicked() {
mDelegate.onNewCardLinkClicked();
assert mShouldRequestExpirationDate;
mNewCardLink.setVisibility(View.GONE);
mCardUnmaskInput.setText(null);
clearInputError();
mMonthInput.requestFocus();
}
private void setInitialFocus() {
InputMethodManager imm =
(InputMethodManager) mContext.getSystemService(Context.INPUT_METHOD_SERVICE);
View view = mShouldRequestExpirationDate ? mMonthInput : mCardUnmaskInput;
imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT);
view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
if (sObserverForTest != null) {
sObserverForTest.onCardUnmaskPromptReadyForInput(this);
}
}
/**
* Moves the focus to the next field based on the value of the fields and the specified type of
* error found for the unmask field(s).
*
* @param errorType The type of error detected.
*/
private void moveFocus(@ErrorType int errorType) {
if (errorType == ErrorType.NOT_ENOUGH_INFO) {
if (mMonthInput.isFocused()
&& mMonthInput.getText().length() == EXPIRATION_FIELDS_LENGTH) {
// The user just finished typing in the month field and there are no validation
// errors.
if (mYearInput.getText().length() == EXPIRATION_FIELDS_LENGTH) {
// Year was already filled, move focus to CVC field.
mCardUnmaskInput.requestFocus();
mDidFocusOnCvc = true;
} else {
// Year was not filled, move focus there.
mYearInput.requestFocus();
mDidFocusOnYear = true;
}
} else if (mYearInput.isFocused()
&& mYearInput.getText().length() == EXPIRATION_FIELDS_LENGTH) {
// The user just finished typing in the year field and there are no validation
// errors. Move focus to CVC field.
mCardUnmaskInput.requestFocus();
mDidFocusOnCvc = true;
}
}
}
/**
* Shows (or removes) the appropriate error message and apply the error filter to the
* appropriate fields depending on the error type.
*
* @param errorType The type of error detected.
*/
private void showDetailedErrorMessage(@ErrorType int errorType) {
switch (errorType) {
case ErrorType.EXPIRATION_MONTH:
showErrorMessage(mExpirationMonthErrorMessage);
break;
case ErrorType.EXPIRATION_YEAR:
showErrorMessage(mExpirationYearErrorMessage);
break;
case ErrorType.EXPIRATION_DATE:
showErrorMessage(mExpirationDateErrorMessage);
break;
case ErrorType.CVC:
showErrorMessage(mCvcErrorMessage);
break;
case ErrorType.CVC_AND_EXPIRATION:
showErrorMessage(mCvcAndExpirationErrorMessage);
break;
case ErrorType.NONE:
case ErrorType.NOT_ENOUGH_INFO:
default:
clearInputError();
return;
}
updateColorForInputs(errorType);
}
/**
* Applies the error filter to the invalid fields based on the errorType.
*
* @param errorType The ErrorType value representing the type of error found for the unmask
* fields.
*/
private void updateColorForInputs(@ErrorType int errorType) {
// The rest of this code makes L-specific assumptions about the background being used to
// draw the TextInput.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) return;
ColorFilter filter =
new PorterDuffColorFilter(ApiCompatibilityUtils.getColor(mContext.getResources(),
R.color.input_underline_error_color),
PorterDuff.Mode.SRC_IN);
// Decide on what field(s) to apply the filter.
boolean filterMonth = errorType == ErrorType.EXPIRATION_MONTH
|| errorType == ErrorType.EXPIRATION_DATE
|| errorType == ErrorType.CVC_AND_EXPIRATION;
boolean filterYear = errorType == ErrorType.EXPIRATION_YEAR
|| errorType == ErrorType.EXPIRATION_DATE
|| errorType == ErrorType.CVC_AND_EXPIRATION;
boolean filterCvc = errorType == ErrorType.CVC || errorType == ErrorType.CVC_AND_EXPIRATION;
updateColorForInput(mMonthInput, filterMonth ? filter : null);
updateColorForInput(mYearInput, filterYear ? filter : null);
updateColorForInput(mCardUnmaskInput, filterCvc ? filter : null);
}
/**
* Determines what type of error, if any, is present in the cvc and expiration date fields of
* the prompt.
*
* @return The ErrorType value representing the type of error found for the unmask fields.
*/
@ErrorType private int getExpirationAndCvcErrorType() {
@ErrorType
int errorType = ErrorType.NONE;
if (mShouldRequestExpirationDate) errorType = getExpirationDateErrorType();
// If the CVC is valid, return the error type determined so far.
if (isCvcValid()) return errorType;
if (mDidFocusOnCvc && !mCardUnmaskInput.isFocused()) {
// The CVC is invalid and the user has typed in the CVC field, but is not focused on it
// now. Add the CVC error to the current error.
if (errorType == ErrorType.NONE || errorType == ErrorType.NOT_ENOUGH_INFO) {
errorType = ErrorType.CVC;
} else {
errorType = ErrorType.CVC_AND_EXPIRATION;
}
} else {
// The CVC is invalid but the user is not done with the field.
// If no other errors were detected, set that there is not enough information.
if (errorType == ErrorType.NONE) errorType = ErrorType.NOT_ENOUGH_INFO;
}
return errorType;
}
/**
* Determines what type of error, if any, is present in the expiration date fields of the
* prompt.
*
* @return The ErrorType value representing the type of error found for the expiration date
* unmask fields.
*/
@ErrorType private int getExpirationDateErrorType() {
if (mThisYear == -1 || mThisMonth == -1) {
mValidationWaitsForCalendarTask = true;
return ErrorType.NOT_ENOUGH_INFO;
}
int month = getMonth();
if (month < 1 || month > 12) {
if (mMonthInput.getText().length() == EXPIRATION_FIELDS_LENGTH
|| (!mMonthInput.isFocused() && mDidFocusOnMonth)) {
// mFinishedTypingMonth = true;
return ErrorType.EXPIRATION_MONTH;
}
return ErrorType.NOT_ENOUGH_INFO;
}
int year = getFourDigitYear();
if (year < mThisYear || year > mThisYear + 10) {
if (mYearInput.getText().length() == EXPIRATION_FIELDS_LENGTH
|| (!mYearInput.isFocused() && mDidFocusOnYear)) {
// mFinishedTypingYear = true;
return ErrorType.EXPIRATION_YEAR;
}
return ErrorType.NOT_ENOUGH_INFO;
}
if (year == mThisYear && month < mThisMonth) {
return ErrorType.EXPIRATION_DATE;
}
return ErrorType.NONE;
}
/**
* Makes a call to the native code to determine if the value in the CVC input field is valid.
*
* @return Whether the CVC is valid.
*/
private boolean isCvcValid() {
return mDelegate.checkUserInputValidity(mCardUnmaskInput.getText().toString());
}
/**
* Sets the enabled state of the main contents, and hides or shows the verification overlay.
* @param enabled True if the inputs should be useable, false if the verification overlay
* obscures them.
*/
private void setInputsEnabled(boolean enabled) {
mCardUnmaskInput.setEnabled(enabled);
mMonthInput.setEnabled(enabled);
mYearInput.setEnabled(enabled);
mStoreLocallyCheckbox.setEnabled(enabled);
mDialogModel.set(ModalDialogProperties.POSITIVE_BUTTON_DISABLED, !enabled);
}
/**
* Updates the verification overlay and main contents such that the overlay has |visibility|.
* @param visibility A View visibility enumeration value.
*/
private void setOverlayVisibility(int visibility) {
mVerificationOverlay.setVisibility(visibility);
mControlsContainer.setAlpha(1f);
boolean contentsShowing = visibility == View.GONE;
if (!contentsShowing) {
int durationMs = 250;
mVerificationOverlay.setAlpha(0f);
mVerificationOverlay.animate().alpha(1f).setDuration(durationMs);
mControlsContainer.animate().alpha(0f).setDuration(durationMs);
}
ViewCompat.setImportantForAccessibility(mControlsContainer,
contentsShowing ? View.IMPORTANT_FOR_ACCESSIBILITY_AUTO
: View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
mControlsContainer.setDescendantFocusability(
contentsShowing ? ViewGroup.FOCUS_BEFORE_DESCENDANTS
: ViewGroup.FOCUS_BLOCK_DESCENDANTS);
}
/**
* Sets the error message on the inputs.
* @param message The error message to show.
*/
private void showErrorMessage(String message) {
assert message != null;
// Set the message to display;
mErrorMessage.setText(message);
mErrorMessage.setVisibility(View.VISIBLE);
// A null message is passed in during card verification, which also makes an announcement.
// Announcing twice in a row may cancel the first announcement.
mErrorMessage.announceForAccessibility(message);
}
/**
* Removes the error message on the inputs.
*/
private void clearInputError() {
mErrorMessage.setText(null);
mErrorMessage.setVisibility(View.GONE);
// The rest of this code makes L-specific assumptions about the background being used to
// draw the TextInput.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) return;
// Remove the highlight on the input fields.
updateColorForInput(mMonthInput, null);
updateColorForInput(mYearInput, null);
updateColorForInput(mCardUnmaskInput, null);
}
/**
* Displays an error that indicates the user can't retry.
*/
private void setNoRetryError(String message) {
mNoRetryErrorMessage.setText(message);
mNoRetryErrorMessage.setVisibility(View.VISIBLE);
mNoRetryErrorMessage.announceForAccessibility(message);
}
/**
* Sets the stroke color for the given input.
* @param input The input to modify.
* @param filter The color filter to apply to the background.
*/
private void updateColorForInput(EditText input, ColorFilter filter) {
input.getBackground().mutate().setColorFilter(filter);
}
/**
* @return The expiration year the user entered.
* Two digit values (such as 17) will be converted to 4 digit years (such as 2017).
* Returns -1 if the input is empty or otherwise not a valid year.
*/
private int getFourDigitYear() {
try {
int year = Integer.parseInt(mYearInput.getText().toString());
if (year < 0) return -1;
if (year < 100) year += mThisYear - mThisYear % 100;
return year;
} catch (NumberFormatException e) {
return -1;
}
}
/**
* @return The expiration month the user entered.
* Returns -1 if the input is empty or not a number.
*/
private int getMonth() {
try {
return Integer.parseInt(mMonthInput.getText().toString());
} catch (NumberFormatException e) {
return -1;
}
}
@Override
public void onClick(PropertyModel model, int buttonType) {
if (buttonType == ModalDialogProperties.ButtonType.POSITIVE) {
mDelegate.onUserInput(mCardUnmaskInput.getText().toString(),
mMonthInput.getText().toString(), Integer.toString(getFourDigitYear()),
mStoreLocallyCheckbox != null && mStoreLocallyCheckbox.isChecked());
} else if (buttonType == ModalDialogProperties.ButtonType.NEGATIVE) {
mModalDialogManager.dismissDialog(model, DialogDismissalCause.NEGATIVE_BUTTON_CLICKED);
}
}
@Override
public void onDismiss(PropertyModel model, int dismissalCause) {
mDelegate.dismissed();
mDialogModel = null;
}
@VisibleForTesting
public static void setObserverForTest(CardUnmaskObserverForTest observerForTest) {
sObserverForTest = observerForTest;
}
@VisibleForTesting
public PropertyModel getDialogForTest() {
return mDialogModel;
}
@VisibleForTesting
public String getErrorMessage() {
return mErrorMessage.getText().toString();
}
}