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.ui; 6 7 import android.content.Context; 8 import android.text.TextUtils; 9 10 import androidx.annotation.Nullable; 11 12 import org.chromium.base.metrics.RecordHistogram; 13 import org.chromium.chrome.browser.autofill.PersonalDataManager.AutofillProfile; 14 import org.chromium.chrome.browser.payments.AutofillAddress; 15 import org.chromium.chrome.browser.payments.AutofillContact; 16 import org.chromium.chrome.browser.payments.ContactEditor; 17 import org.chromium.components.payments.JourneyLogger; 18 import org.chromium.components.payments.Section; 19 20 import java.util.ArrayList; 21 import java.util.Collection; 22 import java.util.Collections; 23 import java.util.Comparator; 24 import java.util.List; 25 26 /** 27 * The data to show in the contact details section where the user can select something. 28 */ 29 public class ContactDetailsSection extends SectionInformation { 30 private final Context mContext; 31 private final ContactEditor mContactEditor; 32 33 private List<AutofillProfile> mProfiles; 34 35 /** 36 * Builds a Contact section from a list of AutofillProfile. 37 * 38 * @param context Context 39 * @param unmodifiableProfiles The list of profiles to build from. 40 * @param contactEditor The Contact Editor associated with this flow. 41 * @param journeyLogger The JourneyLogger for the current Payment Request. 42 */ ContactDetailsSection(Context context, Collection<AutofillProfile> unmodifiableProfiles, ContactEditor contactEditor, JourneyLogger journeyLogger)43 public ContactDetailsSection(Context context, Collection<AutofillProfile> unmodifiableProfiles, 44 ContactEditor contactEditor, JourneyLogger journeyLogger) { 45 // Initially no items are selected, but they are updated later in the constructor. 46 super(PaymentRequestUI.DataType.CONTACT_DETAILS, null); 47 48 mContext = context; 49 mContactEditor = contactEditor; 50 // Copy the profiles from which this section is derived. 51 mProfiles = new ArrayList<AutofillProfile>(unmodifiableProfiles); 52 53 // Refresh the contact section items and selection. 54 createContactListFromAutofillProfiles(journeyLogger); 55 } 56 57 /** 58 * Add (or update) an address in the contact details section. 59 * 60 * @param editedAddress the new or edited address with which to update the contacts section. 61 */ addOrUpdateWithAutofillAddress(AutofillAddress editedAddress)62 public void addOrUpdateWithAutofillAddress(AutofillAddress editedAddress) { 63 if (editedAddress == null) return; 64 65 // If the profile is currently being displayed, update the items in anticipation of the 66 // contacts section refresh. The updatedContact can be null when user has added a new 67 // shipping address without an email, but the contact info section requires only email 68 // address. Null updatedContact should not be added to the mItems list. 69 @Nullable AutofillContact updatedContact = 70 createAutofillContactFromProfile(editedAddress.getProfile()); 71 if (null == updatedContact) return; 72 73 if (mItems != null) { 74 for (int i = 0; i < mItems.size(); i++) { 75 AutofillContact existingContact = (AutofillContact) mItems.get(i); 76 if (existingContact.getProfile().getGUID().equals( 77 editedAddress.getProfile().getGUID())) { 78 // We need to replace |existingContact| with |updatedContact|. 79 mItems.remove(i); 80 mItems.add(i, updatedContact); 81 return; 82 } 83 } 84 } 85 // The contact didn't exist. Add the new address to |mItems| to the end of the list, in 86 // anticipation of the contacts section refresh. 87 if (mItems == null) mItems = new ArrayList<>(); 88 mItems.add(updatedContact); 89 90 // The selection is not updated. 91 } 92 93 /** Recomputes the list of displayed contacts and possibly updates the selection. */ createContactListFromAutofillProfiles(JourneyLogger journeyLogger)94 private void createContactListFromAutofillProfiles(JourneyLogger journeyLogger) { 95 List<AutofillContact> contacts = new ArrayList<>(); 96 List<AutofillContact> uniqueContacts = new ArrayList<>(); 97 98 // Add the profile's valid request values to the editor's autocomplete list and convert 99 // relevant profiles to AutofillContacts, to be deduped later. 100 for (int i = 0; i < mProfiles.size(); ++i) { 101 AutofillContact contact = createAutofillContactFromProfile(mProfiles.get(i)); 102 103 // Only create a contact if the profile has relevant information for the merchant. 104 if (contact != null) { 105 mContactEditor.addPayerNameIfValid(contact.getPayerName()); 106 mContactEditor.addPhoneNumberIfValid(contact.getPayerPhone()); 107 mContactEditor.addEmailAddressIfValid(contact.getPayerEmail()); 108 109 contacts.add(contact); 110 } 111 } 112 113 // Order the contacts so the ones that have most of the required information are put first. 114 // The sort is stable, so contacts with the same relevance score are sorted by frecency. 115 Collections.sort(contacts, new Comparator<AutofillContact>() { 116 @Override 117 public int compare(AutofillContact a, AutofillContact b) { 118 return b.getRelevanceScore() - a.getRelevanceScore(); 119 } 120 }); 121 122 // This algorithm is quadratic, but since the number of contacts is generally very small 123 // ( < 10) a faster but more complicated algorithm would be overkill. 124 for (int i = 0; i < contacts.size(); i++) { 125 AutofillContact contact = contacts.get(i); 126 127 // Different contacts can have identical info. Do not add the same contact info or a 128 // subset of it twice. It's important that the profiles be sorted by the quantity of 129 // required info they have. 130 boolean isNewSuggestion = true; 131 for (int j = 0; j < uniqueContacts.size(); ++j) { 132 if (uniqueContacts.get(j).isEqualOrSupersetOf(contact)) { 133 isNewSuggestion = false; 134 break; 135 } 136 } 137 if (isNewSuggestion) uniqueContacts.add(contact); 138 139 // Limit the number of suggestions. 140 if (uniqueContacts.size() == PaymentUiService.SUGGESTIONS_LIMIT) break; 141 } 142 143 // Automatically select the first address if it is complete. 144 int firstCompleteContactIndex = SectionInformation.NO_SELECTION; 145 if (!uniqueContacts.isEmpty() && uniqueContacts.get(0).isComplete()) { 146 firstCompleteContactIndex = 0; 147 } 148 149 // TODO(crbug.com/746062): Remove this once a journeyLogger is passed in tests. 150 if (journeyLogger != null) { 151 // Log the number of suggested contact info. 152 journeyLogger.setNumberOfSuggestionsShown(Section.CONTACT_INFO, uniqueContacts.size(), 153 firstCompleteContactIndex != SectionInformation.NO_SELECTION); 154 } 155 156 // Record all required and missing fields of the most complete suggestion. 157 recordMissingContactFields(uniqueContacts.isEmpty() ? null : uniqueContacts.get(0)); 158 159 updateItemsWithCollection(firstCompleteContactIndex, uniqueContacts); 160 } 161 162 @Nullable createAutofillContactFromProfile(AutofillProfile profile)163 private AutofillContact createAutofillContactFromProfile(AutofillProfile profile) { 164 boolean requestPayerName = mContactEditor.getRequestPayerName(); 165 boolean requestPayerPhone = mContactEditor.getRequestPayerPhone(); 166 boolean requestPayerEmail = mContactEditor.getRequestPayerEmail(); 167 String name = requestPayerName && !TextUtils.isEmpty(profile.getFullName()) 168 ? profile.getFullName() 169 : null; 170 String phone = requestPayerPhone && !TextUtils.isEmpty(profile.getPhoneNumber()) 171 ? profile.getPhoneNumber() 172 : null; 173 String email = requestPayerEmail && !TextUtils.isEmpty(profile.getEmailAddress()) 174 ? profile.getEmailAddress() 175 : null; 176 177 if (name != null || phone != null || email != null) { 178 @ContactEditor.CompletionStatus 179 int completionStatus = mContactEditor.checkContactCompletionStatus(name, phone, email); 180 return new AutofillContact(mContext, profile, name, phone, email, completionStatus, 181 requestPayerName, requestPayerPhone, requestPayerEmail); 182 } 183 return null; 184 } 185 186 // Bit field values are identical to ProfileFields from payments_profile_comparator.h recordMissingContactFields(AutofillContact contact)187 private void recordMissingContactFields(AutofillContact contact) { 188 int missingFields = 0; 189 if (mContactEditor.getRequestPayerName() 190 && (contact == null || TextUtils.isEmpty(contact.getPayerName()))) { 191 missingFields |= ContactEditor.INVALID_NAME; 192 } 193 if (mContactEditor.getRequestPayerPhone() 194 && (contact == null || TextUtils.isEmpty(contact.getPayerPhone()))) { 195 missingFields |= ContactEditor.INVALID_PHONE_NUMBER; 196 } 197 if (mContactEditor.getRequestPayerEmail() 198 && (contact == null || TextUtils.isEmpty(contact.getPayerEmail()))) { 199 missingFields |= ContactEditor.INVALID_EMAIL; 200 } 201 202 if (missingFields != 0) { 203 RecordHistogram.recordSparseHistogram( 204 "PaymentRequest.MissingContactFields", missingFields); 205 } 206 } 207 } 208