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 = 175 AutocompleteMatch::SanitizeString(UTF8ToUTF16(contact.full_name())); 176 base::string16 given_name = 177 AutocompleteMatch::SanitizeString(UTF8ToUTF16(contact.given_name())); 178 base::string16 family_name = 179 AutocompleteMatch::SanitizeString(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 } 237