普通文本  |  237行  |  8.63 KB

// 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;
}