chromium/chrome/android/java/src/org/chromium/chrome/browser/payments/ContactEditor.java

// Copyright 2016 The Chromium Authors
// 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;

import static org.chromium.chrome.browser.autofill.editors.EditorProperties.ALLOW_DELETE;
import static org.chromium.chrome.browser.autofill.editors.EditorProperties.ALL_KEYS;
import static org.chromium.chrome.browser.autofill.editors.EditorProperties.CANCEL_RUNNABLE;
import static org.chromium.chrome.browser.autofill.editors.EditorProperties.DONE_RUNNABLE;
import static org.chromium.chrome.browser.autofill.editors.EditorProperties.EDITOR_FIELDS;
import static org.chromium.chrome.browser.autofill.editors.EditorProperties.EDITOR_TITLE;
import static org.chromium.chrome.browser.autofill.editors.EditorProperties.FieldProperties.IS_REQUIRED;
import static org.chromium.chrome.browser.autofill.editors.EditorProperties.FieldProperties.LABEL;
import static org.chromium.chrome.browser.autofill.editors.EditorProperties.FieldProperties.VALIDATOR;
import static org.chromium.chrome.browser.autofill.editors.EditorProperties.FieldProperties.VALUE;
import static org.chromium.chrome.browser.autofill.editors.EditorProperties.ItemType.TEXT_INPUT;
import static org.chromium.chrome.browser.autofill.editors.EditorProperties.SHOW_REQUIRED_INDICATOR;
import static org.chromium.chrome.browser.autofill.editors.EditorProperties.TextFieldProperties.TEXT_ALL_KEYS;
import static org.chromium.chrome.browser.autofill.editors.EditorProperties.TextFieldProperties.TEXT_FIELD_TYPE;
import static org.chromium.chrome.browser.autofill.editors.EditorProperties.TextFieldProperties.TEXT_FORMATTER;
import static org.chromium.chrome.browser.autofill.editors.EditorProperties.TextFieldProperties.TEXT_SUGGESTIONS;
import static org.chromium.chrome.browser.autofill.editors.EditorProperties.VALIDATE_ON_SHOW;
import static org.chromium.chrome.browser.autofill.editors.EditorProperties.VISIBLE;
import static org.chromium.chrome.browser.autofill.editors.EditorProperties.scrollToFieldWithErrorMessage;
import static org.chromium.chrome.browser.autofill.editors.EditorProperties.validateForm;

import android.telephony.PhoneNumberUtils;
import android.text.TextUtils;
import android.util.Patterns;

import androidx.annotation.Nullable;

import org.chromium.base.Callback;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.autofill.PersonalDataManager;
import org.chromium.chrome.browser.autofill.PhoneNumberUtil;
import org.chromium.chrome.browser.autofill.editors.EditorBase;
import org.chromium.chrome.browser.autofill.editors.EditorDialogViewBinder;
import org.chromium.chrome.browser.autofill.editors.EditorFieldValidator;
import org.chromium.chrome.browser.autofill.editors.EditorProperties.FieldItem;
import org.chromium.components.autofill.AutofillProfile;
import org.chromium.components.autofill.FieldType;
import org.chromium.payments.mojom.PayerErrors;
import org.chromium.ui.modelutil.ListModel;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.modelutil.PropertyModelChangeProcessor;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;

/** Contact information editor. */
public class ContactEditor extends EditorBase<AutofillContact> {
    // Bit field values are identical to ProfileFields in payments_profile_comparator.h.
    // Please also modify payments_profile_comparator.h after changing these bits.
    public @interface CompletionStatus {}

    /** Can be sent to the merchant as-is without editing first. */
    public static final int COMPLETE = 0;

    /** The contact name is missing. */
    public static final int INVALID_NAME = 1 << 0;

    /** The contact phone number is invalid or missing. */
    public static final int INVALID_PHONE_NUMBER = 1 << 1;

    /** The contact email is invalid or missing. */
    public static final int INVALID_EMAIL = 1 << 2;

    private final boolean mRequestPayerName;
    private final boolean mRequestPayerPhone;
    private final boolean mRequestPayerEmail;
    private final boolean mSaveToDisk;
    private final PersonalDataManager mPersonalDataManager;
    private final Set<String> mPayerNames;
    private final Set<String> mPhoneNumbers;
    private final Set<String> mEmailAddresses;
    @Nullable private PayerErrors mPayerErrors;
    @Nullable private EditorFieldValidator mEmailValidator;
    private boolean mContactNew;
    private AutofillContact mContact;
    private Optional<PropertyModel> mNameField;
    private Optional<PropertyModel> mPhoneField;
    private Optional<PropertyModel> mEmailField;
    private Callback<AutofillContact> mDoneCallback;
    private Callback<AutofillContact> mCancelCallback;

    /**
     * Builds a contact information editor.
     *
     * @param requestPayerName Whether to request the user's name.
     * @param requestPayerPhone Whether to request the user's phone number.
     * @param requestPayerEmail Whether to request the user's email address.
     * @param saveToDisk Whether to save changes to disk.
     * @param personalDataManager The context appropriate PersonalDataManager reference.
     */
    public ContactEditor(
            boolean requestPayerName,
            boolean requestPayerPhone,
            boolean requestPayerEmail,
            boolean saveToDisk,
            PersonalDataManager personalDataManager) {
        assert requestPayerName || requestPayerPhone || requestPayerEmail;
        mRequestPayerName = requestPayerName;
        mRequestPayerPhone = requestPayerPhone;
        mRequestPayerEmail = requestPayerEmail;
        mSaveToDisk = saveToDisk;
        mPersonalDataManager = personalDataManager;
        mPayerNames = new HashSet<>();
        mPhoneNumbers = new HashSet<>();
        mEmailAddresses = new HashSet<>();
    }

    /**
     * @return Whether this editor requires the payer name.
     */
    public boolean getRequestPayerName() {
        return mRequestPayerName;
    }

    /**
     * @return Whether this editor requires the payer phone.
     */
    public boolean getRequestPayerPhone() {
        return mRequestPayerPhone;
    }

    /**
     * @return Whether this editor requires the payer email.
     */
    public boolean getRequestPayerEmail() {
        return mRequestPayerEmail;
    }

    /**
     * Returns the contact completion status with the given name, phone and email.
     *
     * @param name  The payer name to check.
     * @param phone The phone number to check.
     * @param email The email address to check.
     * @return The completion status.
     */
    public @CompletionStatus int checkContactCompletionStatus(
            @Nullable String name, @Nullable String phone, @Nullable String email) {
        int completionStatus = COMPLETE;

        if (mRequestPayerName && TextUtils.isEmpty(name)) {
            completionStatus |= INVALID_NAME;
        }

        if (mRequestPayerPhone && !isPhoneValid(phone)) {
            completionStatus |= INVALID_PHONE_NUMBER;
        }

        if (mRequestPayerEmail && !isEmailValid(email)) {
            completionStatus |= INVALID_EMAIL;
        }

        return completionStatus;
    }

    /**
     * Adds the given payer name to the autocomplete set, if it's valid.
     *
     * @param payerName The payer name to possibly add.
     */
    public void addPayerNameIfValid(@Nullable String payerName) {
        if (!TextUtils.isEmpty(payerName)) mPayerNames.add(payerName);
    }

    /**
     * Adds the given phone number to the autocomplete set, if it's valid.
     *
     * @param phoneNumber The phone number to possibly add.
     */
    public void addPhoneNumberIfValid(@Nullable String phoneNumber) {
        if (isPhoneValid(phoneNumber)) mPhoneNumbers.add(phoneNumber);
    }

    /**
     * Adds the given email address to the autocomplete set, if it's valid.
     *
     * @param emailAddress The email address to possibly add.
     */
    public void addEmailAddressIfValid(@Nullable String emailAddress) {
        if (isEmailValid(emailAddress)) mEmailAddresses.add(emailAddress);
    }

    /**
     * Sets the payer errors to indicate error messages from merchant's retry() call.
     *
     * @param errors The payer errors from merchant's retry() call.
     */
    public void setPayerErrors(@Nullable PayerErrors errors) {
        mPayerErrors = errors;
    }

    /**
     * Allows calling |edit| with a single callback used for both 'done' and 'cancel'.
     * @see #edit(AutofillContact, Callback, Callback)
     */
    public void edit(
            @Nullable final AutofillContact toEdit, final Callback<AutofillContact> callback) {
        edit(toEdit, callback, callback);
    }

    @Override
    public void edit(
            @Nullable final AutofillContact toEdit,
            final Callback<AutofillContact> doneCallback,
            final Callback<AutofillContact> cancelCallback) {
        super.edit(toEdit, doneCallback, cancelCallback);
        mDoneCallback = doneCallback;
        mCancelCallback = cancelCallback;

        boolean contactNew = toEdit == null;
        mContactNew = contactNew;
        var context = mContext;
        AutofillContact contact =
                contactNew
                        ? new AutofillContact(
                                context,
                                AutofillProfile.builder().build(),
                                null,
                                null,
                                null,
                                INVALID_NAME | INVALID_PHONE_NUMBER | INVALID_EMAIL,
                                mRequestPayerName,
                                mRequestPayerPhone,
                                mRequestPayerEmail)
                        : toEdit;
        mContact = contact;

        final String nameCustomErrorMessage = mPayerErrors != null ? mPayerErrors.name : null;
        PropertyModel nameField = null;
        if (mRequestPayerName) {
            String label = context.getString(R.string.payments_name_field_in_contact_details);
            String errorMessage =
                    context.getString(R.string.pref_edit_dialog_field_required_validation_message);
            nameField =
                    new PropertyModel.Builder(TEXT_ALL_KEYS)
                            .with(TEXT_FIELD_TYPE, FieldType.NAME_FULL)
                            .with(LABEL, label)
                            .with(TEXT_SUGGESTIONS, new ArrayList<>(mPayerNames))
                            .with(IS_REQUIRED, true)
                            .with(
                                    VALIDATOR,
                                    EditorFieldValidator.builder()
                                            .withRequiredErrorMessage(errorMessage)
                                            .withInitialErrorMessage(nameCustomErrorMessage)
                                            .build())
                            .with(VALUE, contact.getPayerName())
                            .build();
        }
        mNameField = Optional.ofNullable(nameField);

        PropertyModel phoneField = null;
        if (mRequestPayerPhone) {
            String label = context.getString(R.string.autofill_profile_editor_phone_number);
            phoneField =
                    new PropertyModel.Builder(TEXT_ALL_KEYS)
                            .with(TEXT_FIELD_TYPE, FieldType.PHONE_HOME_WHOLE_NUMBER)
                            .with(LABEL, label)
                            .with(TEXT_SUGGESTIONS, new ArrayList<>(mPhoneNumbers))
                            .with(
                                    TEXT_FORMATTER,
                                    new PhoneNumberUtil.CountryAwareFormatTextWatcher())
                            .with(IS_REQUIRED, true)
                            .with(VALIDATOR, getPhoneValidator())
                            .with(VALUE, contact.getPayerPhone())
                            .build();
        }
        mPhoneField = Optional.ofNullable(phoneField);

        PropertyModel emailField = null;
        if (mRequestPayerEmail) {
            String label = context.getString(R.string.autofill_profile_editor_email_address);
            emailField =
                    new PropertyModel.Builder(TEXT_ALL_KEYS)
                            .with(TEXT_FIELD_TYPE, FieldType.EMAIL_ADDRESS)
                            .with(LABEL, label)
                            .with(TEXT_SUGGESTIONS, new ArrayList<>(mEmailAddresses))
                            .with(IS_REQUIRED, true)
                            .with(VALIDATOR, getEmailValidator())
                            .with(VALUE, contact.getPayerEmail())
                            .build();
        }
        mEmailField = Optional.ofNullable(emailField);

        final String editorTitle =
                toEdit == null
                        ? context.getString(R.string.payments_add_contact_details_label)
                        : toEdit.getEditTitle();

        ListModel<FieldItem> editorFields = new ListModel<>();
        if (mNameField.isPresent()) {
            editorFields.add(new FieldItem(TEXT_INPUT, mNameField.get(), /* isFullLine= */ true));
        }
        if (mPhoneField.isPresent()) {
            editorFields.add(new FieldItem(TEXT_INPUT, mPhoneField.get(), /* isFullLine= */ true));
        }
        if (mEmailField.isPresent()) {
            editorFields.add(new FieldItem(TEXT_INPUT, mEmailField.get(), /* isFullLine= */ true));
        }

        mEditorModel =
                new PropertyModel.Builder(ALL_KEYS)
                        .with(EDITOR_TITLE, editorTitle)
                        .with(SHOW_REQUIRED_INDICATOR, true)
                        .with(EDITOR_FIELDS, editorFields)
                        .with(DONE_RUNNABLE, this::onDone)
                        .with(CANCEL_RUNNABLE, this::onCancel)
                        .with(ALLOW_DELETE, false)
                        // Form validation must be performed only for non-empty address profiles.
                        .with(VALIDATE_ON_SHOW, !contactNew)
                        .build();

        mEditorMCP =
                PropertyModelChangeProcessor.create(
                        mEditorModel, mEditorDialog, EditorDialogViewBinder::bindEditorDialogView);
        mEditorModel.set(VISIBLE, true);
    }

    private void onDone() {
        if (!validateForm(mEditorModel)) {
            scrollToFieldWithErrorMessage(mEditorModel);
            return;
        }
        mEditorModel.set(VISIBLE, false);

        String name = null;
        String phone = null;
        String email = null;
        AutofillProfile profile = mContact.getProfile();

        if (mNameField.isPresent()) {
            name = mNameField.get().get(VALUE);
            profile.setFullName(name);
        }

        if (mPhoneField.isPresent()) {
            phone = mPhoneField.get().get(VALUE);
            profile.setPhoneNumber(phone);
        }

        if (mEmailField.isPresent()) {
            email = mEmailField.get().get(VALUE);
            profile.setEmailAddress(email);
        }

        if (mSaveToDisk) {
            profile.setGUID(mPersonalDataManager.setProfileToLocal(profile));
        }

        if (profile.getGUID().isEmpty()) {
            assert !mSaveToDisk;

            // Set a fake guid for a new temp AutofillProfile.
            profile.setGUID(UUID.randomUUID().toString());
        }

        mContact.completeContact(profile.getGUID(), name, phone, email);
        mDoneCallback.onResult(mContact);

        // Clean up the state of this editor.
        reset();
    }

    private void onCancel() {
        mEditorModel.set(VISIBLE, false);

        mCancelCallback.onResult(mContactNew ? null : mContact);

        // Clean up the state of this editor.
        reset();
    }

    private static boolean isEmailValid(@Nullable String email) {
        return email != null && Patterns.EMAIL_ADDRESS.matcher(email).matches();
    }

    private static boolean isPhoneValid(@Nullable String phone) {
        // TODO(crbug.com/41479087): PhoneNumberUtils internally trigger
        // disk reads for certain devices/configurations.
        return phone != null
                && PhoneNumberUtils.isGlobalPhoneNumber(PhoneNumberUtils.stripSeparators(phone));
    }

    private EditorFieldValidator getEmailValidator() {
        var context = mContext;
        return EditorFieldValidator.builder()
                .withRequiredErrorMessage(
                        context.getString(
                                R.string.pref_edit_dialog_field_required_validation_message))
                .withInitialErrorMessage(mPayerErrors != null ? mPayerErrors.email : null)
                .withValidationPredicate(
                        ContactEditor::isEmailValid,
                        context.getString(R.string.payments_email_invalid_validation_message))
                .build();
    }

    private EditorFieldValidator getPhoneValidator() {
        var context = mContext;
        return EditorFieldValidator.builder()
                .withRequiredErrorMessage(
                        context.getString(
                                R.string.pref_edit_dialog_field_required_validation_message))
                .withInitialErrorMessage(mPayerErrors != null ? mPayerErrors.phone : null)
                .withValidationPredicate(
                        ContactEditor::isPhoneValid,
                        context.getString(R.string.payments_phone_invalid_validation_message))
                .build();
    }
}