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; |
+ } |
+} |