1 // Copyright (c) 2012 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.
5 #include "chrome/browser/autocomplete/contact_provider_chromeos.h"
10 #include "base/i18n/break_iterator.h"
11 #include "base/i18n/string_search.h"
12 #include "base/strings/string16.h"
13 #include "base/strings/string_split.h"
14 #include "base/strings/utf_string_conversions.h"
15 #include "chrome/browser/autocomplete/autocomplete_input.h"
16 #include "chrome/browser/chromeos/contacts/contact.pb.h"
17 #include "chrome/browser/chromeos/contacts/contact_manager.h"
18 #include "chrome/browser/profiles/profile.h"
22 // Default affinity assigned to contacts whose |affinity| field is unset.
23 // TODO(derat): Set this to something reasonable (probably 0.0) once we're
24 // getting affinity for contacts.
25 float kDefaultAffinity
= 1.0;
27 // Base match relevance assigned to a contact with an affinity of 0.0.
28 int kBaseRelevance
= 1300;
30 // Maximum boost to relevance for a contact with an affinity of 1.0.
31 int kAffinityRelevanceBoost
= 200;
33 // Returns true if |word_to_find| is a prefix of |name_to_search| and marks the
34 // matching text in |classifications| (which corresponds to the contact's full
35 // name). |name_index_in_full_name| contains |name_to_search|'s index within
36 // the full name or base::string16::npos if it doesn't appear in it.
37 bool WordIsNamePrefix(const base::string16
& word_to_find
,
38 const base::string16
& name_to_search
,
39 size_t name_index_in_full_name
,
40 size_t full_name_length
,
41 ACMatchClassifications
* classifications
) {
42 DCHECK(classifications
);
44 size_t match_index
= 0;
45 size_t match_length
= 0;
46 if (!base::i18n::StringSearchIgnoringCaseAndAccents(word_to_find
,
47 name_to_search
, &match_index
, &match_length
) || (match_index
!= 0))
50 if (name_index_in_full_name
!= base::string16::npos
) {
51 AutocompleteMatch::ACMatchClassifications new_class
;
52 AutocompleteMatch::ClassifyLocationInString(name_index_in_full_name
,
53 match_length
, full_name_length
, 0, &new_class
);
54 *classifications
= AutocompleteMatch::MergeClassifications(
55 *classifications
, new_class
);
64 const char ContactProvider::kMatchContactIdKey
[] = "contact_id";
66 // Cached information about a contact.
67 struct ContactProvider::ContactData
{
68 ContactData(const base::string16
& full_name
,
69 const base::string16
& given_name
,
70 const base::string16
& family_name
,
71 const std::string
& contact_id
,
73 : full_name(full_name
),
74 given_name(given_name
),
75 family_name(family_name
),
76 given_name_index(base::string16::npos
),
77 family_name_index(base::string16::npos
),
78 contact_id(contact_id
),
80 base::i18n::StringSearchIgnoringCaseAndAccents(
81 given_name
, full_name
, &given_name_index
, NULL
);
82 base::i18n::StringSearchIgnoringCaseAndAccents(
83 family_name
, full_name
, &family_name_index
, NULL
);
86 base::string16 full_name
;
87 base::string16 given_name
;
88 base::string16 family_name
;
90 // Indices into |full_name| where |given_name| and |family_name| first appear,
91 // or base::string16::npos if they don't appear in it.
92 size_t given_name_index
;
93 size_t family_name_index
;
95 // Unique ID used to look up additional contact information.
96 std::string contact_id
;
98 // Affinity between the user and this contact, in the range [0.0, 1.0].
102 ContactProvider::ContactProvider(
103 AutocompleteProviderListener
* listener
,
105 base::WeakPtr
<contacts::ContactManagerInterface
> contact_manager
)
106 : AutocompleteProvider(listener
, profile
, TYPE_CONTACT
),
107 contact_manager_(contact_manager
) {
108 contact_manager_
->AddObserver(this, profile
);
112 void ContactProvider::Start(const AutocompleteInput
& input
,
113 bool minimal_changes
) {
119 if (input
.type() != AutocompleteInput::UNKNOWN
&&
120 input
.type() != AutocompleteInput::QUERY
&&
121 input
.type() != AutocompleteInput::FORCED_QUERY
)
124 std::vector
<base::string16
> input_words
;
125 base::i18n::BreakIterator
break_iterator(
127 base::i18n::BreakIterator::BREAK_WORD
);
128 if (break_iterator
.Init()) {
129 while (break_iterator
.Advance()) {
130 if (break_iterator
.IsWord())
131 input_words
.push_back(break_iterator
.GetString());
135 // |contacts_| is ordered by descending affinity. Since affinity is currently
136 // the only signal used for computing relevance, we can stop after we've found
137 // kMaxMatches results.
138 for (ContactDataVector::const_iterator it
= contacts_
.begin();
139 it
!= contacts_
.end() && matches_
.size() < kMaxMatches
; ++it
)
140 AddContactIfMatched(input
, input_words
, *it
);
143 void ContactProvider::OnContactsUpdated(Profile
* profile
) {
144 DCHECK_EQ(profile
, profile_
);
148 ContactProvider::~ContactProvider() {
149 // Like ContactProvider, ContactManager gets destroyed at profile destruction.
150 // Make sure that this class doesn't try to access ContactManager after
151 // ContactManager is gone.
152 if (contact_manager_
.get())
153 contact_manager_
->RemoveObserver(this, profile_
);
157 bool ContactProvider::CompareAffinity(const ContactData
& a
,
158 const ContactData
& b
) {
159 return a
.affinity
> b
.affinity
;
162 void ContactProvider::RefreshContacts() {
163 if (!contact_manager_
.get())
166 scoped_ptr
<contacts::ContactPointers
> contacts
=
167 contact_manager_
->GetAllContacts(profile_
);
170 contacts_
.reserve(contacts
->size());
171 for (contacts::ContactPointers::const_iterator it
= contacts
->begin();
172 it
!= contacts
->end(); ++it
) {
173 const contacts::Contact
& contact
= **it
;
174 base::string16 full_name
= AutocompleteMatch::SanitizeString(
175 base::UTF8ToUTF16(contact
.full_name()));
176 base::string16 given_name
= AutocompleteMatch::SanitizeString(
177 base::UTF8ToUTF16(contact
.given_name()));
178 base::string16 family_name
= AutocompleteMatch::SanitizeString(
179 base::UTF8ToUTF16(contact
.family_name()));
181 contact
.has_affinity() ? contact
.affinity() : kDefaultAffinity
;
183 if (!full_name
.empty()) {
185 ContactData(full_name
, given_name
, family_name
, contact
.contact_id(),
189 std::sort(contacts_
.begin(), contacts_
.end(), CompareAffinity
);
192 void ContactProvider::AddContactIfMatched(
193 const AutocompleteInput
& input
,
194 const std::vector
<base::string16
>& input_words
,
195 const ContactData
& contact
) {
196 // First, check if the whole input string is a prefix of the full name.
197 // TODO(derat): Consider additionally segmenting the full name so we can match
198 // e.g. middle names or initials even when they aren't typed as a prefix of
200 ACMatchClassifications classifications
;
201 if (!WordIsNamePrefix(input
.text(), contact
.full_name
, 0,
202 contact
.full_name
.size(), &classifications
)) {
203 // If not, check whether every search term is a prefix of the given name
204 // or the family name.
205 if (input_words
.empty())
208 // TODO(derat): Check new matches against previous ones to make sure they
209 // don't overlap (e.g. the query "bob b" against a contact with full name
210 // "Bob G. Bryson", given name "Bob", and family name "Bryson" should result
211 // in classifications "_Bob_ G. _B_ryson" rather than "_Bob_ G. Bryson".
212 for (std::vector
<base::string16
>::const_iterator it
= input_words
.begin();
213 it
!= input_words
.end(); ++it
) {
214 if (!WordIsNamePrefix(*it
, contact
.given_name
, contact
.given_name_index
,
215 contact
.full_name
.size(), &classifications
) &&
216 !WordIsNamePrefix(*it
, contact
.family_name
, contact
.family_name_index
,
217 contact
.full_name
.size(), &classifications
))
222 matches_
.push_back(CreateAutocompleteMatch(input
, contact
));
223 matches_
.back().contents_class
= classifications
;
226 AutocompleteMatch
ContactProvider::CreateAutocompleteMatch(
227 const AutocompleteInput
& input
,
228 const ContactData
& contact
) {
229 AutocompleteMatch
match(this, 0, false, AutocompleteMatchType::CONTACT
);
230 match
.contents
= contact
.full_name
;
231 match
.fill_into_edit
= match
.contents
;
232 match
.relevance
= kBaseRelevance
+
233 static_cast<int>(roundf(kAffinityRelevanceBoost
* contact
.affinity
));
234 match
.RecordAdditionalInfo(kMatchContactIdKey
, contact
.contact_id
);