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