OLD | NEW |
| (Empty) |
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. | |
4 | |
5 #include "chrome/browser/autocomplete/contact_provider_chromeos.h" | |
6 | |
7 #include <algorithm> | |
8 #include <cmath> | |
9 | |
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" | |
19 | |
20 namespace { | |
21 | |
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; | |
26 | |
27 // Base match relevance assigned to a contact with an affinity of 0.0. | |
28 int kBaseRelevance = 1300; | |
29 | |
30 // Maximum boost to relevance for a contact with an affinity of 1.0. | |
31 int kAffinityRelevanceBoost = 200; | |
32 | |
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); | |
43 | |
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)) | |
48 return false; | |
49 | |
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); | |
56 } | |
57 | |
58 return true; | |
59 } | |
60 | |
61 } // namespace | |
62 | |
63 // static | |
64 const char ContactProvider::kMatchContactIdKey[] = "contact_id"; | |
65 | |
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, | |
72 float affinity) | |
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), | |
79 affinity(affinity) { | |
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); | |
84 } | |
85 | |
86 base::string16 full_name; | |
87 base::string16 given_name; | |
88 base::string16 family_name; | |
89 | |
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; | |
94 | |
95 // Unique ID used to look up additional contact information. | |
96 std::string contact_id; | |
97 | |
98 // Affinity between the user and this contact, in the range [0.0, 1.0]. | |
99 float affinity; | |
100 }; | |
101 | |
102 ContactProvider::ContactProvider( | |
103 AutocompleteProviderListener* listener, | |
104 Profile* profile, | |
105 base::WeakPtr<contacts::ContactManagerInterface> contact_manager) | |
106 : AutocompleteProvider(listener, profile, TYPE_CONTACT), | |
107 contact_manager_(contact_manager) { | |
108 contact_manager_->AddObserver(this, profile); | |
109 RefreshContacts(); | |
110 } | |
111 | |
112 void ContactProvider::Start(const AutocompleteInput& input, | |
113 bool minimal_changes) { | |
114 if (minimal_changes) | |
115 return; | |
116 | |
117 matches_.clear(); | |
118 | |
119 if (input.type() != AutocompleteInput::UNKNOWN && | |
120 input.type() != AutocompleteInput::QUERY && | |
121 input.type() != AutocompleteInput::FORCED_QUERY) | |
122 return; | |
123 | |
124 std::vector<base::string16> input_words; | |
125 base::i18n::BreakIterator break_iterator( | |
126 input.text(), | |
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()); | |
132 } | |
133 } | |
134 | |
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); | |
141 } | |
142 | |
143 void ContactProvider::OnContactsUpdated(Profile* profile) { | |
144 DCHECK_EQ(profile, profile_); | |
145 RefreshContacts(); | |
146 } | |
147 | |
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_); | |
154 } | |
155 | |
156 // static | |
157 bool ContactProvider::CompareAffinity(const ContactData& a, | |
158 const ContactData& b) { | |
159 return a.affinity > b.affinity; | |
160 } | |
161 | |
162 void ContactProvider::RefreshContacts() { | |
163 if (!contact_manager_.get()) | |
164 return; | |
165 | |
166 scoped_ptr<contacts::ContactPointers> contacts = | |
167 contact_manager_->GetAllContacts(profile_); | |
168 | |
169 contacts_.clear(); | |
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())); | |
180 float affinity = | |
181 contact.has_affinity() ? contact.affinity() : kDefaultAffinity; | |
182 | |
183 if (!full_name.empty()) { | |
184 contacts_.push_back( | |
185 ContactData(full_name, given_name, family_name, contact.contact_id(), | |
186 affinity)); | |
187 } | |
188 } | |
189 std::sort(contacts_.begin(), contacts_.end(), CompareAffinity); | |
190 } | |
191 | |
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 | |
199 // the full name. | |
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()) | |
206 return; | |
207 | |
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)) | |
218 return; | |
219 } | |
220 } | |
221 | |
222 matches_.push_back(CreateAutocompleteMatch(input, contact)); | |
223 matches_.back().contents_class = classifications; | |
224 } | |
225 | |
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); | |
235 return match; | |
236 } | |
OLD | NEW |