// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/autocomplete/contact_provider_chromeos.h"
#include <algorithm>
#include <cmath>
#include "base/i18n/break_iterator.h"
#include "base/i18n/string_search.h"
#include "base/strings/string16.h"
#include "base/strings/string_split.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/autocomplete/autocomplete_input.h"
#include "chrome/browser/chromeos/contacts/contact.pb.h"
#include "chrome/browser/chromeos/contacts/contact_manager.h"
#include "chrome/browser/profiles/profile.h"
namespace {
// Default affinity assigned to contacts whose |affinity| field is unset.
// TODO(derat): Set this to something reasonable (probably 0.0) once we're
// getting affinity for contacts.
float kDefaultAffinity = 1.0;
// Base match relevance assigned to a contact with an affinity of 0.0.
int kBaseRelevance = 1300;
// Maximum boost to relevance for a contact with an affinity of 1.0.
int kAffinityRelevanceBoost = 200;
// Returns true if |word_to_find| is a prefix of |name_to_search| and marks the
// matching text in |classifications| (which corresponds to the contact's full
// name). |name_index_in_full_name| contains |name_to_search|'s index within
// the full name or base::string16::npos if it doesn't appear in it.
bool WordIsNamePrefix(const base::string16& word_to_find,
const base::string16& name_to_search,
size_t name_index_in_full_name,
size_t full_name_length,
ACMatchClassifications* classifications) {
DCHECK(classifications);
size_t match_index = 0;
size_t match_length = 0;
if (!base::i18n::StringSearchIgnoringCaseAndAccents(word_to_find,
name_to_search, &match_index, &match_length) || (match_index != 0))
return false;
if (name_index_in_full_name != base::string16::npos) {
AutocompleteMatch::ACMatchClassifications new_class;
AutocompleteMatch::ClassifyLocationInString(name_index_in_full_name,
match_length, full_name_length, 0, &new_class);
*classifications = AutocompleteMatch::MergeClassifications(
*classifications, new_class);
}
return true;
}
} // namespace
// static
const char ContactProvider::kMatchContactIdKey[] = "contact_id";
// Cached information about a contact.
struct ContactProvider::ContactData {
ContactData(const base::string16& full_name,
const base::string16& given_name,
const base::string16& family_name,
const std::string& contact_id,
float affinity)
: full_name(full_name),
given_name(given_name),
family_name(family_name),
given_name_index(base::string16::npos),
family_name_index(base::string16::npos),
contact_id(contact_id),
affinity(affinity) {
base::i18n::StringSearchIgnoringCaseAndAccents(
given_name, full_name, &given_name_index, NULL);
base::i18n::StringSearchIgnoringCaseAndAccents(
family_name, full_name, &family_name_index, NULL);
}
base::string16 full_name;
base::string16 given_name;
base::string16 family_name;
// Indices into |full_name| where |given_name| and |family_name| first appear,
// or base::string16::npos if they don't appear in it.
size_t given_name_index;
size_t family_name_index;
// Unique ID used to look up additional contact information.
std::string contact_id;
// Affinity between the user and this contact, in the range [0.0, 1.0].
float affinity;
};
ContactProvider::ContactProvider(
AutocompleteProviderListener* listener,
Profile* profile,
base::WeakPtr<contacts::ContactManagerInterface> contact_manager)
: AutocompleteProvider(listener, profile, TYPE_CONTACT),
contact_manager_(contact_manager) {
contact_manager_->AddObserver(this, profile);
RefreshContacts();
}
void ContactProvider::Start(const AutocompleteInput& input,
bool minimal_changes) {
if (minimal_changes)
return;
matches_.clear();
if (input.type() != AutocompleteInput::UNKNOWN &&
input.type() != AutocompleteInput::QUERY &&
input.type() != AutocompleteInput::FORCED_QUERY)
return;
std::vector<base::string16> input_words;
base::i18n::BreakIterator break_iterator(
input.text(),
base::i18n::BreakIterator::BREAK_WORD);
if (break_iterator.Init()) {
while (break_iterator.Advance()) {
if (break_iterator.IsWord())
input_words.push_back(break_iterator.GetString());
}
}
// |contacts_| is ordered by descending affinity. Since affinity is currently
// the only signal used for computing relevance, we can stop after we've found
// kMaxMatches results.
for (ContactDataVector::const_iterator it = contacts_.begin();
it != contacts_.end() && matches_.size() < kMaxMatches; ++it)
AddContactIfMatched(input, input_words, *it);
}
void ContactProvider::OnContactsUpdated(Profile* profile) {
DCHECK_EQ(profile, profile_);
RefreshContacts();
}
ContactProvider::~ContactProvider() {
// Like ContactProvider, ContactManager gets destroyed at profile destruction.
// Make sure that this class doesn't try to access ContactManager after
// ContactManager is gone.
if (contact_manager_.get())
contact_manager_->RemoveObserver(this, profile_);
}
// static
bool ContactProvider::CompareAffinity(const ContactData& a,
const ContactData& b) {
return a.affinity > b.affinity;
}
void ContactProvider::RefreshContacts() {
if (!contact_manager_.get())
return;
scoped_ptr<contacts::ContactPointers> contacts =
contact_manager_->GetAllContacts(profile_);
contacts_.clear();
contacts_.reserve(contacts->size());
for (contacts::ContactPointers::const_iterator it = contacts->begin();
it != contacts->end(); ++it) {
const contacts::Contact& contact = **it;
base::string16 full_name =
AutocompleteMatch::SanitizeString(UTF8ToUTF16(contact.full_name()));
base::string16 given_name =
AutocompleteMatch::SanitizeString(UTF8ToUTF16(contact.given_name()));
base::string16 family_name =
AutocompleteMatch::SanitizeString(UTF8ToUTF16(contact.family_name()));
float affinity =
contact.has_affinity() ? contact.affinity() : kDefaultAffinity;
if (!full_name.empty()) {
contacts_.push_back(
ContactData(full_name, given_name, family_name, contact.contact_id(),
affinity));
}
}
std::sort(contacts_.begin(), contacts_.end(), CompareAffinity);
}
void ContactProvider::AddContactIfMatched(
const AutocompleteInput& input,
const std::vector<base::string16>& input_words,
const ContactData& contact) {
// First, check if the whole input string is a prefix of the full name.
// TODO(derat): Consider additionally segmenting the full name so we can match
// e.g. middle names or initials even when they aren't typed as a prefix of
// the full name.
ACMatchClassifications classifications;
if (!WordIsNamePrefix(input.text(), contact.full_name, 0,
contact.full_name.size(), &classifications)) {
// If not, check whether every search term is a prefix of the given name
// or the family name.
if (input_words.empty())
return;
// TODO(derat): Check new matches against previous ones to make sure they
// don't overlap (e.g. the query "bob b" against a contact with full name
// "Bob G. Bryson", given name "Bob", and family name "Bryson" should result
// in classifications "_Bob_ G. _B_ryson" rather than "_Bob_ G. Bryson".
for (std::vector<base::string16>::const_iterator it = input_words.begin();
it != input_words.end(); ++it) {
if (!WordIsNamePrefix(*it, contact.given_name, contact.given_name_index,
contact.full_name.size(), &classifications) &&
!WordIsNamePrefix(*it, contact.family_name, contact.family_name_index,
contact.full_name.size(), &classifications))
return;
}
}
matches_.push_back(CreateAutocompleteMatch(input, contact));
matches_.back().contents_class = classifications;
}
AutocompleteMatch ContactProvider::CreateAutocompleteMatch(
const AutocompleteInput& input,
const ContactData& contact) {
AutocompleteMatch match(this, 0, false, AutocompleteMatchType::CONTACT);
match.contents = contact.full_name;
match.fill_into_edit = match.contents;
match.relevance = kBaseRelevance +
static_cast<int>(roundf(kAffinityRelevanceBoost * contact.affinity));
match.RecordAdditionalInfo(kMatchContactIdKey, contact.contact_id);
return match;
}