// 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.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 = new Handler();
private final Map<Integer, EditorFieldModel> mAddressFields = new HashMap<>();
private final Set<CharSequence> mPhoneNumbers = new HashSet<>();
@Nullable private AutofillProfileBridge mAutofillProfileBridge;
@Nullable private EditorFieldModel mCountryField;
@Nullable private EditorFieldModel mPhoneField;
@Nullable private EditorFieldValidator mPhoneValidator;
@Nullable private List<AddressUiComponent> mAddressUiComponents;
/**
* 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 set, 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();
// If |toEdit| is null, we're creating a new autofill profile with the country code of the
// default locale on this device.
boolean isNewAddress = toEdit == null;
// Ensure that |address| and |profile| are always not null.
final AutofillAddress address = isNewAddress
? 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 or editing an
// existing address.
final EditorModel editor = new EditorModel(mContext.getString(isNewAddress
? R.string.autofill_create_profile
: R.string.autofill_edit_profile));
// The country dropdown is always present on the editor.
if (mCountryField == null) {
mCountryField = EditorFieldModel.createDropdown(
mContext.getString(R.string.autofill_profile_editor_country),
AutofillProfileBridge.getSupportedCountries());
}
// 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);
}
});
// Country dropdown is cached, so the selected item needs to be updated for the new profile
// that's being edited. This will not fire the dropdown callback.
mCountryField.setValue(AutofillAddress.getCountryCode(profile));
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, EditorFieldModel.createTextInput());
mAddressFields.put(AddressField.DEPENDENT_LOCALITY, EditorFieldModel.createTextInput());
mAddressFields.put(AddressField.ORGANIZATION, EditorFieldModel.createTextInput());
// State should be formatted in all capitals.
mAddressFields.put(AddressField.ADMIN_AREA, EditorFieldModel.createTextInput(
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, EditorFieldModel.createTextInput(
EditorFieldModel.INPUT_TYPE_HINT_ALPHA_NUMERIC));
mAddressFields.put(AddressField.POSTAL_CODE, EditorFieldModel.createTextInput(
EditorFieldModel.INPUT_TYPE_HINT_ALPHA_NUMERIC));
// Street line field can contain \n to indicate line breaks.
mAddressFields.put(AddressField.STREET_ADDRESS, EditorFieldModel.createTextInput(
EditorFieldModel.INPUT_TYPE_HINT_STREET_LINES));
// Android has special formatting rules for names.
mAddressFields.put(AddressField.RECIPIENT, EditorFieldModel.createTextInput(
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 = EditorFieldModel.createTextInput(EditorFieldModel.INPUT_TYPE_HINT_PHONE,
mContext.getString(R.string.autofill_profile_editor_phone_number),
mPhoneNumbers, getPhoneValidator(),
mContext.getString(R.string.payments_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 pdm = PersonalDataManager.getInstance();
profile.setLabel(pdm.getAddressLabelForPaymentRequest(profile));
// Save the edited autofill profile.
profile.setGUID(pdm.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_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;
}
}