Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(3781)

Unified Diff: chrome/android/java/src/org/chromium/chrome/browser/payments/AddressEditor.java

Issue 2093363002: Autofill address editor in PaymentRequest UI. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@contact-editor
Patch Set: Fix try-bot Created 4 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
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;
+ }
+}

Powered by Google App Engine
This is Rietveld 408576698