| Index: chrome/android/java/src/org/chromium/chrome/browser/payments/AddressEditor.java
|
| diff --git a/chrome/android/java/src/org/chromium/chrome/browser/payments/AddressEditor.java b/chrome/android/java/src/org/chromium/chrome/browser/payments/AddressEditor.java
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..3d8949980c8bbf25dab5e5ee536ee4dd8c111ebe
|
| --- /dev/null
|
| +++ b/chrome/android/java/src/org/chromium/chrome/browser/payments/AddressEditor.java
|
| @@ -0,0 +1,377 @@
|
| +// 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;
|
| +
|
| +import android.os.Handler;
|
| +import android.telephony.PhoneNumberUtils;
|
| +import android.text.TextUtils;
|
| +import android.util.Pair;
|
| +
|
| +import org.chromium.base.Callback;
|
| +import org.chromium.chrome.R;
|
| +import org.chromium.chrome.browser.autofill.PersonalDataManager;
|
| +import org.chromium.chrome.browser.autofill.PersonalDataManager.AutofillProfile;
|
| +import org.chromium.chrome.browser.payments.ui.EditorFieldModel;
|
| +import org.chromium.chrome.browser.payments.ui.EditorFieldModel.EditorFieldValidator;
|
| +import org.chromium.chrome.browser.payments.ui.EditorModel;
|
| +import org.chromium.chrome.browser.preferences.autofill.AutofillProfileBridge;
|
| +import org.chromium.chrome.browser.preferences.autofill.AutofillProfileBridge.AddressField;
|
| +import org.chromium.chrome.browser.preferences.autofill.AutofillProfileBridge.AddressUiComponent;
|
| +
|
| +import java.util.ArrayList;
|
| +import java.util.HashMap;
|
| +import java.util.HashSet;
|
| +import java.util.List;
|
| +import java.util.Locale;
|
| +import java.util.Map;
|
| +import java.util.Set;
|
| +
|
| +import javax.annotation.Nullable;
|
| +
|
| +/**
|
| + * An address editor. Can be used for either shipping or billing address editing.
|
| + */
|
| +public class AddressEditor extends EditorBase<AutofillAddress> {
|
| + private final Handler mHandler;
|
| + private final Map<Integer, EditorFieldModel> mAddressFields;
|
| + private final List<CharSequence> mPhoneNumbers;
|
| + @Nullable private AutofillProfileBridge mAutofillProfileBridge;
|
| + @Nullable private EditorFieldModel mCountryField;
|
| + @Nullable private EditorFieldModel mPhoneField;
|
| + @Nullable private EditorFieldValidator mPhoneValidator;
|
| + @Nullable private List<AddressUiComponent> mAddressUiComponents;
|
| +
|
| + /**
|
| + * Builds an address editor.
|
| + */
|
| + public AddressEditor() {
|
| + mHandler = new Handler();
|
| + mAddressFields = new HashMap<>();
|
| + mPhoneNumbers = new ArrayList<>();
|
| + }
|
| +
|
| + /**
|
| + * Returns whether the given profile can be sent to the merchant as-is without editing first. 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 profile to check.
|
| + * @return Whether the profile is complete.
|
| + */
|
| + public boolean isProfileComplete(@Nullable AutofillProfile profile) {
|
| + if (profile == null || TextUtils.isEmpty(profile.getFullName())
|
| + || !getPhoneValidator().isValid(profile.getPhoneNumber())) {
|
| + return false;
|
| + }
|
| +
|
| + List<Integer> requiredFields = AutofillProfileBridge.getRequiredAddressFields(
|
| + AutofillAddress.getCountryCode(profile));
|
| + for (int i = 0; i < requiredFields.size(); i++) {
|
| + if (TextUtils.isEmpty(getProfileField(profile, requiredFields.get(i)))) return false;
|
| + }
|
| +
|
| + return true;
|
| + }
|
| +
|
| + /**
|
| + * Adds the given phone number to the autocomplete list, if it's valid.
|
| + *
|
| + * @param phoneNumber The phone number to possibly add.
|
| + */
|
| + public void addPhoneNumberIfValid(@Nullable CharSequence phoneNumber) {
|
| + if (getPhoneValidator().isValid(phoneNumber)) mPhoneNumbers.add(phoneNumber);
|
| + }
|
| +
|
| + /**
|
| + * Builds and shows an editor model with the following fields.
|
| + *
|
| + * [ country dropdown ] <----- country dropdown is always present.
|
| + * [ an address field ] \
|
| + * [ an address field ] \
|
| + * ... <-- field order, presence, required, and labels depend on country.
|
| + * [ an address field ] /
|
| + * [ an address field ] /
|
| + * [ phone number field ] <----- phone is always present and required.
|
| + */
|
| + @Override
|
| + public void edit(@Nullable AutofillAddress toEdit, final Callback<AutofillAddress> callback) {
|
| + super.edit(toEdit, callback);
|
| +
|
| + if (mAutofillProfileBridge == null) mAutofillProfileBridge = new AutofillProfileBridge();
|
| +
|
| + // Ensure that |address| and |profile| are always not null. If |toEdit| is null, we're
|
| + // creating a new autofill profile with the country code of the default locale on this
|
| + // device.
|
| + final AutofillAddress address = toEdit == null
|
| + ? new AutofillAddress(new AutofillProfile(), false) : toEdit;
|
| + final AutofillProfile profile = address.getProfile();
|
| +
|
| + // The title of the editor depends on whether we're adding a new address (toEdit is null) or
|
| + // editing an existing address (toEdit is not null).
|
| + final EditorModel editor = new EditorModel(
|
| + mContext.getString(toEdit == null ? R.string.payments_add_address_label
|
| + : R.string.payments_edit_address_label));
|
| +
|
| + // The country dropdown is always present on the editor.
|
| + if (mCountryField == null) {
|
| + mCountryField = new EditorFieldModel(
|
| + mContext.getString(R.string.autofill_profile_editor_country),
|
| + AutofillProfileBridge.getSupportedCountries());
|
| + }
|
| +
|
| + // Country dropdown is cached, so the selected item needs to be updated for every new
|
| + // profile that's being edited.
|
| + mCountryField.setValue(AutofillAddress.getCountryCode(profile));
|
| +
|
| + // Changing the country will update which fields are in the model. The actual fields are not
|
| + // discarded, so their contents are preserved.
|
| + mCountryField.setDropdownCallback(new Callback<Pair<String, Runnable>>() {
|
| + @Override
|
| + public void onResult(Pair<String, Runnable> eventData) {
|
| + editor.removeAllFields();
|
| + editor.addField(mCountryField);
|
| + addAddressTextFieldsToEditor(editor, eventData.first,
|
| + Locale.getDefault().getLanguage());
|
| + editor.addField(mPhoneField);
|
| +
|
| + // Notify EditorView that the fields in the model have changed. EditorView should
|
| + // re-read the model and update the UI accordingly.
|
| + mHandler.post(eventData.second);
|
| + }
|
| + });
|
| + editor.addField(mCountryField);
|
| +
|
| + // There's a finite number of fields for address editing. Changing the country will re-order
|
| + // and relabel the fields. The meaning of each field remains the same.
|
| + if (mAddressFields.isEmpty()) {
|
| + // City, dependent locality, and organization don't have any special formatting hints.
|
| + mAddressFields.put(AddressField.LOCALITY, new EditorFieldModel(0));
|
| + mAddressFields.put(AddressField.DEPENDENT_LOCALITY, new EditorFieldModel(0));
|
| + mAddressFields.put(AddressField.ORGANIZATION, new EditorFieldModel(0));
|
| +
|
| + // State should be formatted in all capitals.
|
| + mAddressFields.put(AddressField.ADMIN_AREA,
|
| + new EditorFieldModel(EditorFieldModel.INPUT_TYPE_HINT_REGION));
|
| +
|
| + // Sorting code and postal code (a.k.a. ZIP code) should show both letters and digits on
|
| + // the keyboard, if possible.
|
| + mAddressFields.put(AddressField.SORTING_CODE,
|
| + new EditorFieldModel(EditorFieldModel.INPUT_TYPE_HINT_ALPHA_NUMERIC));
|
| + mAddressFields.put(AddressField.POSTAL_CODE,
|
| + new EditorFieldModel(EditorFieldModel.INPUT_TYPE_HINT_ALPHA_NUMERIC));
|
| +
|
| + // Street line field can contain \n to indicate line breaks.
|
| + mAddressFields.put(AddressField.STREET_ADDRESS,
|
| + new EditorFieldModel(EditorFieldModel.INPUT_TYPE_HINT_STREET_LINES));
|
| +
|
| + // Android has special formatting rules for names.
|
| + mAddressFields.put(AddressField.RECIPIENT,
|
| + new EditorFieldModel(EditorFieldModel.INPUT_TYPE_HINT_PERSON_NAME));
|
| + }
|
| +
|
| + // Address fields are cached, so their values need to be updated for every new profile
|
| + // that's being edited.
|
| + for (Map.Entry<Integer, EditorFieldModel> entry : mAddressFields.entrySet()) {
|
| + entry.getValue().setValue(getProfileField(profile, entry.getKey()));
|
| + }
|
| +
|
| + // Both country code and language code dictate which fields should be added to the editor.
|
| + // For example, "US" will not add dependent locality to the editor. A "JP" address will
|
| + // start with a person's full name or a with a prefecture name, depending on whether the
|
| + // language code is "ja-Latn" or "ja".
|
| + addAddressTextFieldsToEditor(editor, profile.getCountryCode(), profile.getLanguageCode());
|
| +
|
| + // Phone number is present and required for all countries.
|
| + if (mPhoneField == null) {
|
| + mPhoneField = new EditorFieldModel(EditorFieldModel.INPUT_TYPE_HINT_PHONE,
|
| + mContext.getString(R.string.autofill_profile_editor_phone_number),
|
| + mPhoneNumbers, getPhoneValidator(),
|
| + mContext.getString(R.string.payments_address_field_required_validation_message),
|
| + mContext.getString(R.string.payments_phone_invalid_validation_message), null);
|
| + }
|
| +
|
| + // Phone number field is cached, so its value needs to be updated for every new profile
|
| + // that's being edited.
|
| + mPhoneField.setValue(profile.getPhoneNumber());
|
| + editor.addField(mPhoneField);
|
| +
|
| + // If the user clicks [Cancel], send a null address back to the caller.
|
| + editor.setCancelCallback(new Runnable() {
|
| + @Override
|
| + public void run() {
|
| + callback.onResult(null);
|
| + }
|
| + });
|
| +
|
| + // If the user clicks [Done], save changes on disk, mark the address "complete," and send it
|
| + // back to the caller.
|
| + editor.setDoneCallback(new Runnable() {
|
| + @Override
|
| + public void run() {
|
| + commitChanges(profile);
|
| + address.completeAddress(profile);
|
| + callback.onResult(address);
|
| + }
|
| + });
|
| +
|
| + mEditorView.show(editor);
|
| + }
|
| +
|
| + /** Saves the edited profile on disk. */
|
| + private void commitChanges(AutofillProfile profile) {
|
| + // Country code and phone number are always required and are always collected from the
|
| + // editor model.
|
| + profile.setCountryCode(mCountryField.getValue().toString());
|
| + profile.setPhoneNumber(mPhoneField.getValue().toString());
|
| +
|
| + // Autofill profile bridge normalizes the language code for the autofill profile.
|
| + profile.setLanguageCode(mAutofillProfileBridge.getCurrentBestLanguageCode());
|
| +
|
| + // Collect data from all visible fields and store it in the autofill profile.
|
| + Set<Integer> visibleFields = new HashSet<>();
|
| + for (int i = 0; i < mAddressUiComponents.size(); i++) {
|
| + AddressUiComponent component = mAddressUiComponents.get(i);
|
| + visibleFields.add(component.id);
|
| + if (component.id != AddressField.COUNTRY) {
|
| + setProfileField(profile, component.id, mAddressFields.get(component.id).getValue());
|
| + }
|
| + }
|
| +
|
| + // Clear the fields that are hidden from the user interface, so
|
| + // AutofillAddress.toPaymentAddress() will send them to the renderer as empty strings.
|
| + for (Map.Entry<Integer, EditorFieldModel> entry : mAddressFields.entrySet()) {
|
| + if (!visibleFields.contains(entry.getKey())) {
|
| + setProfileField(profile, entry.getKey(), "");
|
| + }
|
| + }
|
| +
|
| + // Calculate the label for this profile. The label's format depends on the country and
|
| + // language code for the profile.
|
| + PersonalDataManager pmd = PersonalDataManager.getInstance();
|
| + profile.setLabel(pmd.getGetAddressLabelForPaymentRequest(profile));
|
| +
|
| + // Save the edited autofill profile.
|
| + pmd.setProfile(profile);
|
| + }
|
| +
|
| + /** @return The given autofill profile field. */
|
| + private static String getProfileField(AutofillProfile profile, int field) {
|
| + assert profile != null;
|
| + switch (field) {
|
| + case AddressField.COUNTRY:
|
| + return profile.getCountryCode();
|
| + case AddressField.ADMIN_AREA:
|
| + return profile.getRegion();
|
| + case AddressField.LOCALITY:
|
| + return profile.getLocality();
|
| + case AddressField.DEPENDENT_LOCALITY:
|
| + return profile.getDependentLocality();
|
| + case AddressField.SORTING_CODE:
|
| + return profile.getSortingCode();
|
| + case AddressField.POSTAL_CODE:
|
| + return profile.getPostalCode();
|
| + case AddressField.STREET_ADDRESS:
|
| + return profile.getStreetAddress();
|
| + case AddressField.ORGANIZATION:
|
| + return profile.getCompanyName();
|
| + case AddressField.RECIPIENT:
|
| + return profile.getFullName();
|
| + }
|
| +
|
| + assert false;
|
| + return null;
|
| + }
|
| +
|
| + /** Writes the given value into the specified autofill profile field. */
|
| + private static void setProfileField(
|
| + AutofillProfile profile, int field, @Nullable CharSequence value) {
|
| + assert profile != null;
|
| + switch (field) {
|
| + case AddressField.COUNTRY:
|
| + profile.setCountryCode(ensureNotNull(value));
|
| + return;
|
| + case AddressField.ADMIN_AREA:
|
| + profile.setRegion(ensureNotNull(value));
|
| + return;
|
| + case AddressField.LOCALITY:
|
| + profile.setLocality(ensureNotNull(value));
|
| + return;
|
| + case AddressField.DEPENDENT_LOCALITY:
|
| + profile.setDependentLocality(ensureNotNull(value));
|
| + return;
|
| + case AddressField.SORTING_CODE:
|
| + profile.setSortingCode(ensureNotNull(value));
|
| + return;
|
| + case AddressField.POSTAL_CODE:
|
| + profile.setPostalCode(ensureNotNull(value));
|
| + return;
|
| + case AddressField.STREET_ADDRESS:
|
| + profile.setStreetAddress(ensureNotNull(value));
|
| + return;
|
| + case AddressField.ORGANIZATION:
|
| + profile.setCompanyName(ensureNotNull(value));
|
| + return;
|
| + case AddressField.RECIPIENT:
|
| + profile.setFullName(ensureNotNull(value));
|
| + return;
|
| + }
|
| +
|
| + assert false;
|
| + }
|
| +
|
| + private static String ensureNotNull(@Nullable CharSequence value) {
|
| + return value == null ? "" : value.toString();
|
| + }
|
| +
|
| + /**
|
| + * Adds text fields to the editor model based on the country and language code of the profile
|
| + * that's being edited.
|
| + */
|
| + private void addAddressTextFieldsToEditor(
|
| + EditorModel container, String countryCode, String languageCode) {
|
| + mAddressUiComponents = mAutofillProfileBridge.getAddressUiComponents(countryCode,
|
| + languageCode);
|
| +
|
| + for (int i = 0; i < mAddressUiComponents.size(); i++) {
|
| + AddressUiComponent component = mAddressUiComponents.get(i);
|
| +
|
| + // The country field is a dropdown, so there's no need to add a text field for it.
|
| + if (component.id == AddressField.COUNTRY) continue;
|
| +
|
| + EditorFieldModel field = mAddressFields.get(component.id);
|
| + // Labels depend on country, e.g., state is called province in some countries. These are
|
| + // already localized.
|
| + field.setLabel(component.label);
|
| + field.setIsFullLine(component.isFullLine);
|
| +
|
| + // Libaddressinput formats do not always require the full name (RECIPIENT), but
|
| + // PaymentRequest does.
|
| + if (component.isRequired || component.id == AddressField.RECIPIENT) {
|
| + field.setRequiredErrorMessage(mContext.getString(
|
| + R.string.payments_address_field_required_validation_message));
|
| + } else {
|
| + field.setRequiredErrorMessage(null);
|
| + }
|
| +
|
| + container.addField(field);
|
| + }
|
| + }
|
| +
|
| + private EditorFieldValidator getPhoneValidator() {
|
| + if (mPhoneValidator == null) {
|
| + mPhoneValidator = new EditorFieldValidator() {
|
| + @Override
|
| + public boolean isValid(@Nullable CharSequence value) {
|
| + return value != null
|
| + && PhoneNumberUtils.isGlobalPhoneNumber(
|
| + PhoneNumberUtils.stripSeparators(value.toString()));
|
| + }
|
| + };
|
| + }
|
| + return mPhoneValidator;
|
| + }
|
| +}
|
|
|