chromium/chrome/browser/autofill/android/java/src/org/chromium/chrome/browser/autofill/AutofillAddress.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.autofill;

import android.content.Context;
import android.telephony.PhoneNumberUtils;
import android.text.TextUtils;
import android.util.Pair;

import androidx.annotation.IntDef;
import androidx.annotation.Nullable;

import org.chromium.components.autofill.AutofillProfile;
import org.chromium.components.autofill.EditableOption;
import org.chromium.components.autofill.FieldType;
import org.chromium.payments.mojom.PaymentAddress;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.List;
import java.util.regex.Pattern;

/** The locally stored autofill address. */
public class AutofillAddress extends EditableOption {
    /** The pattern for a valid region code. */
    private static final String REGION_CODE_PATTERN = "^[A-Z]{2}$";

    // Bit field values are identical to ProfileFields in payments_profile_comparator.h.
    @IntDef({
        CompletionStatus.COMPLETE,
        CompletionStatus.INVALID_RECIPIENT,
        CompletionStatus.INVALID_PHONE_NUMBER,
        CompletionStatus.INVALID_ADDRESS
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface CompletionStatus {
        /** Can be sent to the merchant as-is without editing first. */
        int COMPLETE = 0;

        /** The recipient is missing. */
        int INVALID_RECIPIENT = 1 << 0;

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

        /**
         * The address is invalid. For example, missing state or city name. To have consistent bit
         * field values between Android and Desktop 1 << 2 is reserved for email address.
         */
        int INVALID_ADDRESS = 1 << 3;

        int MAX_VALUE = 1 << 4;
    }

    @Nullable private static Pattern sRegionCodePattern;

    private final Context mContext;
    private final PersonalDataManager mPersonalDataManager;
    private AutofillProfile mProfile;
    @Nullable private String mShippingLabelWithCountry;
    @Nullable private String mShippingLabelWithoutCountry;
    @Nullable private String mBillingLabel;

    /**
     * Builds the autofill address.
     *
     * @param context The context where this address was created.
     * @param profile The autofill profile containing the address information.
     * @param personalDataManager
     */
    public AutofillAddress(
            Context context, AutofillProfile profile, PersonalDataManager personalDataManager) {
        super(
                profile.getGUID(),
                profile.getFullName(),
                profile.getLabel(),
                profile.getPhoneNumber(),
                null);
        mContext = context;
        mProfile = profile;
        mPersonalDataManager = personalDataManager;
        mIsEditable = true;
        checkAndUpdateAddressCompleteness();
    }

    /** @return The autofill profile where this address data lives. */
    public AutofillProfile getProfile() {
        return mProfile;
    }

    /**
     * Updates the address and marks it "complete".
     * Called after the user has edited this address.
     * Updates the identifier and labels.
     *
     * @param profile The new profile to use.
     */
    public void completeAddress(AutofillProfile profile) {
        updateAddress(profile);
        assert mIsComplete;
    }

    /**
     * Updates the address and its completeness if needed.
     * Called after the user has edited this address.
     * Updates the identifier and labels.
     *
     * @param profile The new profile to use.
     */
    public void updateAddress(AutofillProfile profile) {
        // Since the profile changed, our cached labels are now out of date. Set them to null so the
        // labels are recomputed next time they are needed.
        mShippingLabelWithCountry = null;
        mShippingLabelWithoutCountry = null;
        mBillingLabel = null;

        mProfile = profile;
        updateIdentifierAndLabels(
                mProfile.getGUID(),
                mProfile.getFullName(),
                mProfile.getLabel(),
                mProfile.getPhoneNumber());
        checkAndUpdateAddressCompleteness();
    }

    /**
     * Gets the shipping address label which includes the country for the profile associated with
     * this address and sets it as sublabel for this EditableOption.
     */
    public void setShippingAddressLabelWithCountry() {
        assert mProfile != null;

        if (mShippingLabelWithCountry == null) {
            mShippingLabelWithCountry =
                    mPersonalDataManager.getShippingAddressLabelWithCountryForPaymentRequest(
                            mProfile);
        }

        mProfile.setLabel(mShippingLabelWithCountry);
        updateSublabel(mProfile.getLabel());
    }

    /**
     * Gets the shipping address label which does not include the country for the profile associated
     * with this address and sets it as sublabel for this EditableOption.
     */
    public void setShippingAddressLabelWithoutCountry() {
        assert mProfile != null;

        if (mShippingLabelWithoutCountry == null) {
            mShippingLabelWithoutCountry =
                    mPersonalDataManager.getShippingAddressLabelWithoutCountryForPaymentRequest(
                            mProfile);
        }

        mProfile.setLabel(mShippingLabelWithoutCountry);
        updateSublabel(mProfile.getLabel());
    }

    /**
     * Checks whether this address is complete and updates edit message, edit title and complete
     * status.
     */
    private void checkAndUpdateAddressCompleteness() {
        Pair<Integer, Integer> messageResIds =
                getEditMessageAndTitleResIds(
                        checkAddressCompletionStatus(mProfile, mPersonalDataManager));

        mEditMessage =
                messageResIds.first.intValue() == 0
                        ? null
                        : mContext.getString(messageResIds.first);
        mEditTitle =
                messageResIds.second.intValue() == 0
                        ? null
                        : mContext.getString(messageResIds.second);
        mIsComplete = mEditMessage == null;
        mCompletenessScore = calculateCompletenessScore();
    }

    /**
     * Gets the edit message and title resource Ids for the completion status.
     *
     * @param  completionStatus The completion status.
     * @return The resource Ids. The first is the edit message resource Id. The second is the
     *         correspond editor title resource Id.
     */
    public static Pair<Integer, Integer> getEditMessageAndTitleResIds(
            @CompletionStatus int completionStatus) {
        int editMessageResId = 0;
        int editTitleResId = 0;

        switch (completionStatus) {
            case CompletionStatus.COMPLETE:
                editTitleResId = R.string.payments_edit_address;
                break;
            case CompletionStatus.INVALID_RECIPIENT:
                editMessageResId = R.string.payments_recipient_required;
                editTitleResId = R.string.payments_add_recipient;
                break;
            case CompletionStatus.INVALID_PHONE_NUMBER:
                editMessageResId = R.string.payments_phone_number_required;
                editTitleResId = R.string.payments_add_phone_number;
                break;
            case CompletionStatus.INVALID_ADDRESS:
                editMessageResId = R.string.payments_invalid_address;
                editTitleResId = R.string.payments_add_valid_address;
                break;
            default:
                // Multiple bits are set.
                assert completionStatus < CompletionStatus.MAX_VALUE : "Invalid completion status";
                editMessageResId = R.string.payments_more_information_required;
                editTitleResId = R.string.payments_add_more_information;
        }

        return new Pair<Integer, Integer>(editMessageResId, editTitleResId);
    }

    /**
     * Checks address completion status in the given profile.
     *
     * <p>If the country code is not set or invalid, but all fields for the default locale's country
     * code are present, then the profile is deemed "complete." AutoflllAddress.toPaymentAddress()
     * will use the default locale to fill in a blank country code before sending the address to the
     * renderer.
     *
     * @param profile The autofill profile containing the address information.
     * @param personalDataManager
     * @return int The completion status.
     */
    public static @CompletionStatus int checkAddressCompletionStatus(
            AutofillProfile profile, PersonalDataManager personalDataManager) {
        @CompletionStatus int completionStatus = CompletionStatus.COMPLETE;

        if (TextUtils.isEmpty(profile.getFullName())) {
            completionStatus |= CompletionStatus.INVALID_RECIPIENT;
        }

        if (!PhoneNumberUtils.isGlobalPhoneNumber(
                PhoneNumberUtils.stripSeparators(profile.getPhoneNumber().toString()))) {
            completionStatus |= CompletionStatus.INVALID_PHONE_NUMBER;
        }

        List<Integer> requiredFields =
                AutofillProfileBridge.getRequiredAddressFields(
                        AutofillAddress.getCountryCode(profile, personalDataManager));
        for (int fieldId : requiredFields) {
            if (fieldId == FieldType.NAME_FULL || fieldId == FieldType.ADDRESS_HOME_COUNTRY) {
                continue;
            }
            if (!TextUtils.isEmpty(profile.getInfo(fieldId))) continue;
            completionStatus |= CompletionStatus.INVALID_ADDRESS;
            break;
        }

        return completionStatus;
    }

    /**
     * @return The country code to use, e.g., when constructing an editor for this address.
     */
    public static String getCountryCode(
            @Nullable AutofillProfile profile, PersonalDataManager personalDataManager) {
        if (sRegionCodePattern == null) {
            sRegionCodePattern = Pattern.compile(REGION_CODE_PATTERN);
        }
        if (profile == null) {
            return personalDataManager.getDefaultCountryCodeForNewAddress();
        }
        final String countryCode = profile.getInfo(FieldType.ADDRESS_HOME_COUNTRY);
        return TextUtils.isEmpty(countryCode) || !sRegionCodePattern.matcher(countryCode).matches()
                ? personalDataManager.getDefaultCountryCodeForNewAddress()
                : countryCode;
    }

    /** @return The address for the merchant. */
    public PaymentAddress toPaymentAddress() {
        assert mIsComplete;
        PaymentAddress result = new PaymentAddress();

        result.country = getCountryCode(mProfile, mPersonalDataManager);
        result.addressLine = mProfile.getStreetAddress().split("\n");
        result.region = mProfile.getRegion();
        result.city = mProfile.getLocality();
        result.dependentLocality = mProfile.getDependentLocality();
        result.postalCode = mProfile.getPostalCode();
        result.sortingCode = mProfile.getSortingCode();
        result.organization = mProfile.getCompanyName();
        result.recipient = mProfile.getFullName();
        result.phone = mProfile.getPhoneNumber();

        return result;
    }

    private int calculateCompletenessScore() {
        int missingFields = checkAddressCompletionStatus(mProfile, mPersonalDataManager);

        // Count how many are set. The completeness of the address is weighted so as
        // to dominate the other fields.
        int completenessScore = 0;
        if ((missingFields & CompletionStatus.INVALID_RECIPIENT) == 0) completenessScore++;
        if ((missingFields & CompletionStatus.INVALID_PHONE_NUMBER) == 0) completenessScore++;
        if ((missingFields & CompletionStatus.INVALID_ADDRESS) == 0) completenessScore += 10;
        return completenessScore;
    }
}