1 // Copyright 2016 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package org.chromium.chrome.browser.payments; 6 7 import android.telephony.PhoneNumberUtils; 8 import android.text.TextUtils; 9 import android.util.Patterns; 10 11 import androidx.annotation.Nullable; 12 13 import org.chromium.base.Callback; 14 import org.chromium.base.StrictModeContext; 15 import org.chromium.chrome.R; 16 import org.chromium.chrome.browser.autofill.PersonalDataManager; 17 import org.chromium.chrome.browser.autofill.PersonalDataManager.AutofillProfile; 18 import org.chromium.chrome.browser.autofill.PhoneNumberUtil; 19 import org.chromium.chrome.browser.autofill.prefeditor.EditorBase; 20 import org.chromium.chrome.browser.autofill.prefeditor.EditorFieldModel; 21 import org.chromium.chrome.browser.autofill.prefeditor.EditorFieldModel.EditorFieldValidator; 22 import org.chromium.chrome.browser.autofill.prefeditor.EditorModel; 23 import org.chromium.payments.mojom.PayerErrors; 24 25 import java.util.HashSet; 26 import java.util.Set; 27 import java.util.UUID; 28 29 /** 30 * Contact information editor. 31 */ 32 public class ContactEditor extends EditorBase<AutofillContact> { 33 // Bit field values are identical to ProfileFields in payments_profile_comparator.h. 34 // Please also modify payments_profile_comparator.h after changing these bits since 35 // missing fields on both Android and Desktop are recorded in the same UMA metric: 36 // PaymentRequest.MissingContactFields. 37 public @interface CompletionStatus {} 38 /** Can be sent to the merchant as-is without editing first. */ 39 public static final int COMPLETE = 0; 40 /** The contact name is missing. */ 41 public static final int INVALID_NAME = 1 << 0; 42 /** The contact phone number is invalid or missing. */ 43 public static final int INVALID_PHONE_NUMBER = 1 << 1; 44 /** The contact email is invalid or missing. */ 45 public static final int INVALID_EMAIL = 1 << 2; 46 47 private final boolean mRequestPayerName; 48 private final boolean mRequestPayerPhone; 49 private final boolean mRequestPayerEmail; 50 private final boolean mSaveToDisk; 51 private final Set<CharSequence> mPayerNames; 52 private final Set<CharSequence> mPhoneNumbers; 53 private final Set<CharSequence> mEmailAddresses; 54 @Nullable private PayerErrors mPayerErrors; 55 @Nullable private EditorFieldValidator mPhoneValidator; 56 @Nullable private EditorFieldValidator mEmailValidator; 57 58 /** 59 * Builds a contact information editor. 60 * 61 * @param requestPayerName Whether to request the user's name. 62 * @param requestPayerPhone Whether to request the user's phone number. 63 * @param requestPayerEmail Whether to request the user's email address. 64 * @param saveToDisk Whether to save changes to disk. 65 */ ContactEditor(boolean requestPayerName, boolean requestPayerPhone, boolean requestPayerEmail, boolean saveToDisk)66 public ContactEditor(boolean requestPayerName, boolean requestPayerPhone, 67 boolean requestPayerEmail, boolean saveToDisk) { 68 assert requestPayerName || requestPayerPhone || requestPayerEmail; 69 mRequestPayerName = requestPayerName; 70 mRequestPayerPhone = requestPayerPhone; 71 mRequestPayerEmail = requestPayerEmail; 72 mSaveToDisk = saveToDisk; 73 mPayerNames = new HashSet<>(); 74 mPhoneNumbers = new HashSet<>(); 75 mEmailAddresses = new HashSet<>(); 76 } 77 78 /** 79 * @return Whether this editor requires the payer name. 80 */ getRequestPayerName()81 public boolean getRequestPayerName() { 82 return mRequestPayerName; 83 } 84 85 /** 86 * @return Whether this editor requires the payer phone. 87 */ getRequestPayerPhone()88 public boolean getRequestPayerPhone() { 89 return mRequestPayerPhone; 90 } 91 92 /** 93 * @return Whether this editor requires the payer email. 94 */ getRequestPayerEmail()95 public boolean getRequestPayerEmail() { 96 return mRequestPayerEmail; 97 } 98 99 /** 100 * Returns the contact completion status with the given name, phone and email. 101 * 102 * @param name The payer name to check. 103 * @param phone The phone number to check. 104 * @param email The email address to check. 105 * @return The completion status. 106 */ 107 @CompletionStatus checkContactCompletionStatus( @ullable String name, @Nullable String phone, @Nullable String email)108 public int checkContactCompletionStatus( 109 @Nullable String name, @Nullable String phone, @Nullable String email) { 110 int completionStatus = COMPLETE; 111 112 if (mRequestPayerName && TextUtils.isEmpty(name)) { 113 completionStatus |= INVALID_NAME; 114 } 115 116 if (mRequestPayerPhone && !getPhoneValidator().isValid(phone)) { 117 completionStatus |= INVALID_PHONE_NUMBER; 118 } 119 120 if (mRequestPayerEmail && !getEmailValidator().isValid(email)) { 121 completionStatus |= INVALID_EMAIL; 122 } 123 124 return completionStatus; 125 } 126 127 /** 128 * Adds the given payer name to the autocomplete set, if it's valid. 129 * 130 * @param payerName The payer name to possibly add. 131 */ addPayerNameIfValid(@ullable CharSequence payerName)132 public void addPayerNameIfValid(@Nullable CharSequence payerName) { 133 if (!TextUtils.isEmpty(payerName)) mPayerNames.add(payerName); 134 } 135 136 /** 137 * Adds the given phone number to the autocomplete set, if it's valid. 138 * 139 * @param phoneNumber The phone number to possibly add. 140 */ addPhoneNumberIfValid(@ullable CharSequence phoneNumber)141 public void addPhoneNumberIfValid(@Nullable CharSequence phoneNumber) { 142 if (getPhoneValidator().isValid(phoneNumber)) mPhoneNumbers.add(phoneNumber); 143 } 144 145 /** 146 * Adds the given email address to the autocomplete set, if it's valid. 147 * 148 * @param emailAddress The email address to possibly add. 149 */ addEmailAddressIfValid(@ullable CharSequence emailAddress)150 public void addEmailAddressIfValid(@Nullable CharSequence emailAddress) { 151 if (getEmailValidator().isValid(emailAddress)) mEmailAddresses.add(emailAddress); 152 } 153 154 /** 155 * Sets the payer errors to indicate error messages from merchant's retry() call. 156 * 157 * @param errors The payer errors from merchant's retry() call. 158 */ setPayerErrors(@ullable PayerErrors errors)159 public void setPayerErrors(@Nullable PayerErrors errors) { 160 mPayerErrors = errors; 161 } 162 163 /** 164 * Allows calling |edit| with a single callback used for both 'done' and 'cancel'. 165 * @see #edit(AutofillContact, Callback, Callback) 166 */ edit( @ullable final AutofillContact toEdit, final Callback<AutofillContact> callback)167 public void edit( 168 @Nullable final AutofillContact toEdit, final Callback<AutofillContact> callback) { 169 edit(toEdit, callback, callback); 170 } 171 172 @Override edit(@ullable final AutofillContact toEdit, final Callback<AutofillContact> doneCallback, final Callback<AutofillContact> cancelCallback)173 public void edit(@Nullable final AutofillContact toEdit, 174 final Callback<AutofillContact> doneCallback, 175 final Callback<AutofillContact> cancelCallback) { 176 super.edit(toEdit, doneCallback, cancelCallback); 177 178 final AutofillContact contact = toEdit == null 179 ? new AutofillContact(mContext, new AutofillProfile(), null, null, null, 180 INVALID_NAME | INVALID_PHONE_NUMBER | INVALID_EMAIL, mRequestPayerName, 181 mRequestPayerPhone, mRequestPayerEmail) 182 : toEdit; 183 184 final EditorFieldModel nameField = mRequestPayerName 185 ? EditorFieldModel.createTextInput(EditorFieldModel.INPUT_TYPE_HINT_PERSON_NAME, 186 mContext.getString(R.string.payments_name_field_in_contact_details), 187 mPayerNames, null /* suggestions */, null /* formatter */, 188 null /* validator */, 189 mContext.getString( 190 R.string.pref_edit_dialog_field_required_validation_message), 191 null, contact.getPayerName()) 192 : null; 193 194 final EditorFieldModel phoneField = mRequestPayerPhone 195 ? EditorFieldModel.createTextInput(EditorFieldModel.INPUT_TYPE_HINT_PHONE, 196 mContext.getString(R.string.autofill_profile_editor_phone_number), 197 mPhoneNumbers, new PhoneNumberUtil.CountryAwareFormatTextWatcher(), 198 getPhoneValidator(), null, 199 mContext.getString( 200 R.string.pref_edit_dialog_field_required_validation_message), 201 mContext.getString(R.string.payments_phone_invalid_validation_message), 202 contact.getPayerPhone()) 203 : null; 204 205 final EditorFieldModel emailField = mRequestPayerEmail 206 ? EditorFieldModel.createTextInput(EditorFieldModel.INPUT_TYPE_HINT_EMAIL, 207 mContext.getString(R.string.autofill_profile_editor_email_address), 208 mEmailAddresses, null, getEmailValidator(), null, 209 mContext.getString( 210 R.string.pref_edit_dialog_field_required_validation_message), 211 mContext.getString(R.string.payments_email_invalid_validation_message), 212 contact.getPayerEmail()) 213 : null; 214 215 EditorModel editor = new EditorModel(toEdit == null 216 ? mContext.getString(R.string.payments_add_contact_details_label) 217 : toEdit.getEditTitle()); 218 219 if (nameField != null) { 220 nameField.setCustomErrorMessage(mPayerErrors != null ? mPayerErrors.name : null); 221 editor.addField(nameField); 222 } 223 if (phoneField != null) { 224 phoneField.setCustomErrorMessage(mPayerErrors != null ? mPayerErrors.phone : null); 225 editor.addField(phoneField); 226 } 227 if (emailField != null) { 228 emailField.setCustomErrorMessage(mPayerErrors != null ? mPayerErrors.email : null); 229 editor.addField(emailField); 230 } 231 232 // If the user clicks [Cancel], send |toEdit| contact back to the caller, which was the 233 // original state (could be null, a complete contact, a partial contact). 234 editor.setCancelCallback(cancelCallback.bind(toEdit)); 235 236 editor.setDoneCallback(() -> { 237 String name = null; 238 String phone = null; 239 String email = null; 240 AutofillProfile profile = contact.getProfile(); 241 242 if (nameField != null) { 243 name = nameField.getValue().toString(); 244 profile.setFullName(name); 245 } 246 247 if (phoneField != null) { 248 phone = phoneField.getValue().toString(); 249 profile.setPhoneNumber(phone); 250 } 251 252 if (emailField != null) { 253 email = emailField.getValue().toString(); 254 profile.setEmailAddress(email); 255 } 256 257 if (mSaveToDisk) { 258 profile.setGUID(PersonalDataManager.getInstance().setProfileToLocal(profile)); 259 } 260 261 if (profile.getGUID().isEmpty()) { 262 assert !mSaveToDisk; 263 264 // Set a fake guid for a new temp AutofillProfile. 265 profile.setGUID(UUID.randomUUID().toString()); 266 } 267 268 profile.setIsLocal(true); 269 contact.completeContact(profile.getGUID(), name, phone, email); 270 doneCallback.onResult(contact); 271 }); 272 273 mEditorDialog.show(editor); 274 if (mPayerErrors != null) mEditorDialog.validateForm(); 275 } 276 getPhoneValidator()277 private EditorFieldValidator getPhoneValidator() { 278 if (mPhoneValidator == null) { 279 mPhoneValidator = new EditorFieldValidator() { 280 @Override 281 public boolean isValid(@Nullable CharSequence value) { 282 // TODO(crbug.com/999286): PhoneNumberUtils internally trigger disk reads for 283 // certain devices/configurations. 284 try (StrictModeContext ignored = StrictModeContext.allowDiskReads()) { 285 return value != null 286 && PhoneNumberUtils.isGlobalPhoneNumber( 287 PhoneNumberUtils.stripSeparators(value.toString())); 288 } 289 } 290 291 @Override 292 public boolean isLengthMaximum(@Nullable CharSequence value) { 293 return false; 294 } 295 }; 296 } 297 return mPhoneValidator; 298 } 299 getEmailValidator()300 private EditorFieldValidator getEmailValidator() { 301 if (mEmailValidator == null) { 302 mEmailValidator = new EditorFieldValidator() { 303 @Override 304 public boolean isValid(@Nullable CharSequence value) { 305 return value != null && Patterns.EMAIL_ADDRESS.matcher(value).matches(); 306 } 307 308 @Override 309 public boolean isLengthMaximum(@Nullable CharSequence value) { 310 return false; 311 } 312 }; 313 } 314 return mEmailValidator; 315 } 316 } 317