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