Commit 33227920 authored by derat@chromium.org's avatar derat@chromium.org

contacts: Add GDataContactsService.

This adds a class for fetching a user's Google contacts via
the Contacts API.

BUG=128805
TEST=added
TBR=ben@chromium.org

Review URL: https://chromiumcodereview.appspot.com/10818017

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@149005 0039d316-1c4b-4281-b951-d872f2087c98
parent ba00db9f
......@@ -19,7 +19,8 @@ message Contact {
// Provider-assigned unique identifier.
optional string provider_id = 1;
// Last time at which this contact was updated within the upstream provider.
// Last time at which this contact was updated within the upstream provider,
// as given by base::Time::ToInternalValue().
optional int64 update_time = 2;
// Has the contact been deleted recently within the upstream provider?
......
// 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/chromeos/gdata/gdata_contacts_service.h"
#include <cstring>
#include <string>
#include <map>
#include <utility>
#include "base/json/json_writer.h"
#include "base/logging.h"
#include "base/memory/weak_ptr.h"
#include "base/stl_util.h"
#include "base/values.h"
#include "chrome/browser/chromeos/contacts/contact.pb.h"
#include "chrome/browser/chromeos/gdata/gdata_operation_registry.h"
#include "chrome/browser/chromeos/gdata/gdata_operation_runner.h"
#include "chrome/browser/chromeos/gdata/gdata_operations.h"
#include "chrome/browser/chromeos/gdata/gdata_params.h"
#include "chrome/browser/chromeos/gdata/gdata_util.h"
#include "chrome/browser/profiles/profile.h"
#include "content/public/browser/browser_thread.h"
using content::BrowserThread;
namespace gdata {
namespace {
// Maximum number of profile photos that we'll download at once.
const int kMaxSimultaneousPhotoDownloads = 10;
// Field in the top-level object containing the contacts feed.
const char kFeedField[] = "feed";
// Field in the contacts feed containing a list of category information, along
// with fields within the dictionaries contained in the list and expected
// values.
const char kCategoryField[] = "category";
const char kCategorySchemeField[] = "scheme";
const char kCategorySchemeValue[] = "http://schemas.google.com/g/2005#kind";
const char kCategoryTermField[] = "term";
const char kCategoryTermValue[] =
"http://schemas.google.com/contact/2008#contact";
// Field in the contacts feed containing a list of contact entries.
const char kEntryField[] = "entry";
// Top-level fields in contact entries.
const char kIdField[] = "id.$t";
const char kDeletedField[] = "gd$deleted";
const char kFullNameField[] = "gd$name.gd$fullName.$t";
const char kGivenNameField[] = "gd$name.gd$givenName.$t";
const char kAdditionalNameField[] = "gd$name.gd$additionalName.$t";
const char kFamilyNameField[] = "gd$name.gd$familyName.$t";
const char kNamePrefixField[] = "gd$name.gd$namePrefix.$t";
const char kNameSuffixField[] = "gd$name.gd$nameSuffix.$t";
const char kEmailField[] = "gd$email";
const char kPhoneField[] = "gd$phoneNumber";
const char kPostalAddressField[] = "gd$structuredPostalAddress";
const char kInstantMessagingField[] = "gd$im";
const char kLinkField[] = "link";
const char kUpdatedField[] = "updated.$t";
// Fields in entries in the |kEmailField| list.
const char kEmailAddressField[] = "address";
// Fields in entries in the |kPhoneField| list.
const char kPhoneNumberField[] = "$t";
// Fields in entries in the |kPostalAddressField| list.
const char kPostalAddressFormattedField[] = "gd$formattedAddress.$t";
// Fields in entries in the |kInstantMessagingField| list.
const char kInstantMessagingAddressField[] = "address";
const char kInstantMessagingProtocolField[] = "protocol";
const char kInstantMessagingProtocolAimValue[] =
"http://schemas.google.com/g/2005#AIM";
const char kInstantMessagingProtocolMsnValue[] =
"http://schemas.google.com/g/2005#MSN";
const char kInstantMessagingProtocolYahooValue[] =
"http://schemas.google.com/g/2005#YAHOO";
const char kInstantMessagingProtocolSkypeValue[] =
"http://schemas.google.com/g/2005#SKYPE";
const char kInstantMessagingProtocolQqValue[] =
"http://schemas.google.com/g/2005#QQ";
const char kInstantMessagingProtocolGoogleTalkValue[] =
"http://schemas.google.com/g/2005#GOOGLE_TALK";
const char kInstantMessagingProtocolIcqValue[] =
"http://schemas.google.com/g/2005#ICQ";
const char kInstantMessagingProtocolJabberValue[] =
"http://schemas.google.com/g/2005#JABBER";
// Generic fields shared between address-like items (email, postal, etc.).
const char kAddressPrimaryField[] = "primary";
const char kAddressPrimaryTrueValue[] = "true";
const char kAddressRelField[] = "rel";
const char kAddressRelHomeValue[] = "http://schemas.google.com/g/2005#home";
const char kAddressRelWorkValue[] = "http://schemas.google.com/g/2005#work";
const char kAddressRelMobileValue[] = "http://schemas.google.com/g/2005#mobile";
const char kAddressLabelField[] = "label";
// Fields in entries in the |kLinkField| list.
const char kLinkHrefField[] = "href";
const char kLinkRelField[] = "rel";
const char kLinkETagField[] = "gd$etag";
const char kLinkRelPhotoValue[] =
"http://schemas.google.com/contacts/2008/rel#photo";
// Returns a string containing a pretty-printed JSON representation of |value|.
std::string PrettyPrintValue(const base::Value& value) {
std::string out;
base::JSONWriter::WriteWithOptions(
&value, base::JSONWriter::OPTIONS_PRETTY_PRINT, &out);
return out;
}
// Returns whether an address is primary, given a dictionary representing a
// single address.
bool IsAddressPrimary(const DictionaryValue& address_dict) {
std::string primary;
address_dict.GetString(kAddressPrimaryField, &primary);
return primary == kAddressPrimaryTrueValue;
}
// Initializes an AddressType message given a dictionary representing a single
// address.
void InitAddressType(const DictionaryValue& address_dict,
contacts::Contact_AddressType* type) {
DCHECK(type);
type->Clear();
std::string rel;
address_dict.GetString(kAddressRelField, &rel);
if (rel == kAddressRelHomeValue)
type->set_relation(contacts::Contact_AddressType_Relation_HOME);
else if (rel == kAddressRelWorkValue)
type->set_relation(contacts::Contact_AddressType_Relation_WORK);
else if (rel == kAddressRelMobileValue)
type->set_relation(contacts::Contact_AddressType_Relation_MOBILE);
else
type->set_relation(contacts::Contact_AddressType_Relation_OTHER);
address_dict.GetString(kAddressLabelField, type->mutable_label());
}
// Maps the protocol from a dictionary representing a contact's IM address to a
// contacts::Contact_InstantMessagingAddress_Protocol value.
contacts::Contact_InstantMessagingAddress_Protocol
GetInstantMessagingProtocol(const DictionaryValue& im_dict) {
std::string protocol;
im_dict.GetString(kInstantMessagingProtocolField, &protocol);
if (protocol == kInstantMessagingProtocolAimValue)
return contacts::Contact_InstantMessagingAddress_Protocol_AIM;
else if (protocol == kInstantMessagingProtocolMsnValue)
return contacts::Contact_InstantMessagingAddress_Protocol_MSN;
else if (protocol == kInstantMessagingProtocolYahooValue)
return contacts::Contact_InstantMessagingAddress_Protocol_YAHOO;
else if (protocol == kInstantMessagingProtocolSkypeValue)
return contacts::Contact_InstantMessagingAddress_Protocol_SKYPE;
else if (protocol == kInstantMessagingProtocolQqValue)
return contacts::Contact_InstantMessagingAddress_Protocol_QQ;
else if (protocol == kInstantMessagingProtocolGoogleTalkValue)
return contacts::Contact_InstantMessagingAddress_Protocol_GOOGLE_TALK;
else if (protocol == kInstantMessagingProtocolIcqValue)
return contacts::Contact_InstantMessagingAddress_Protocol_ICQ;
else if (protocol == kInstantMessagingProtocolJabberValue)
return contacts::Contact_InstantMessagingAddress_Protocol_JABBER;
else
return contacts::Contact_InstantMessagingAddress_Protocol_OTHER;
}
// Gets the photo URL from a contact's dictionary (within the "entry" list).
// Returns an empty string if no photo was found.
std::string GetPhotoUrl(const DictionaryValue& dict) {
const ListValue* link_list = NULL;
if (!dict.GetList(kLinkField, &link_list))
return std::string();
for (size_t i = 0; i < link_list->GetSize(); ++i) {
DictionaryValue* link_dict = NULL;
if (!link_list->GetDictionary(i, &link_dict))
continue;
std::string rel;
if (!link_dict->GetString(kLinkRelField, &rel))
continue;
if (rel != kLinkRelPhotoValue)
continue;
// From https://goo.gl/7T6Od: "If a contact does not have a photo, then the
// photo link element has no gd:etag attribute."
std::string etag;
if (!link_dict->GetString(kLinkETagField, &etag))
continue;
std::string url;
if (link_dict->GetString(kLinkHrefField, &url))
return url;
}
return std::string();
}
// Fills a Contact's fields using an entry from a GData feed.
bool FillContactFromDictionary(const base::DictionaryValue& dict,
contacts::Contact* contact) {
DCHECK(contact);
contact->Clear();
if (!dict.GetString(kIdField, contact->mutable_provider_id()))
return false;
std::string updated;
if (dict.GetString(kUpdatedField, &updated)) {
base::Time update_time;
if (!util::GetTimeFromString(updated, &update_time)) {
LOG(WARNING) << "Unable to parse time \"" << updated << "\"";
return false;
}
contact->set_update_time(update_time.ToInternalValue());
}
const base::Value* deleted_value = NULL;
contact->set_deleted(dict.Get(kDeletedField, &deleted_value));
if (contact->deleted())
return true;
dict.GetString(kFullNameField, contact->mutable_full_name());
dict.GetString(kGivenNameField, contact->mutable_given_name());
dict.GetString(kAdditionalNameField, contact->mutable_additional_name());
dict.GetString(kFamilyNameField, contact->mutable_family_name());
dict.GetString(kNamePrefixField, contact->mutable_name_prefix());
dict.GetString(kNameSuffixField, contact->mutable_name_suffix());
const ListValue* email_list = NULL;
if (dict.GetList(kEmailField, &email_list)) {
for (size_t i = 0; i < email_list->GetSize(); ++i) {
DictionaryValue* email_dict = NULL;
if (!email_list->GetDictionary(i, &email_dict))
return false;
contacts::Contact_EmailAddress* email = contact->add_email_addresses();
if (!email_dict->GetString(kEmailAddressField, email->mutable_address()))
return false;
email->set_primary(IsAddressPrimary(*email_dict));
InitAddressType(*email_dict, email->mutable_type());
}
}
const ListValue* phone_list = NULL;
if (dict.GetList(kPhoneField, &phone_list)) {
for (size_t i = 0; i < phone_list->GetSize(); ++i) {
DictionaryValue* phone_dict = NULL;
if (!phone_list->GetDictionary(i, &phone_dict))
return false;
contacts::Contact_PhoneNumber* phone = contact->add_phone_numbers();
if (!phone_dict->GetString(kPhoneNumberField, phone->mutable_number()))
return false;
phone->set_primary(IsAddressPrimary(*phone_dict));
InitAddressType(*phone_dict, phone->mutable_type());
}
}
const ListValue* address_list = NULL;
if (dict.GetList(kPostalAddressField, &address_list)) {
for (size_t i = 0; i < address_list->GetSize(); ++i) {
DictionaryValue* address_dict = NULL;
if (!address_list->GetDictionary(i, &address_dict))
return false;
contacts::Contact_PostalAddress* address =
contact->add_postal_addresses();
if (!address_dict->GetString(kPostalAddressFormattedField,
address->mutable_address())) {
return false;
}
address->set_primary(IsAddressPrimary(*address_dict));
InitAddressType(*address_dict, address->mutable_type());
}
}
const ListValue* im_list = NULL;
if (dict.GetList(kInstantMessagingField, &im_list)) {
for (size_t i = 0; i < im_list->GetSize(); ++i) {
DictionaryValue* im_dict = NULL;
if (!im_list->GetDictionary(i, &im_dict))
return false;
contacts::Contact_InstantMessagingAddress* im =
contact->add_instant_messaging_addresses();
if (!im_dict->GetString(kInstantMessagingAddressField,
im->mutable_address())) {
return false;
}
im->set_primary(IsAddressPrimary(*im_dict));
InitAddressType(*im_dict, im->mutable_type());
im->set_protocol(GetInstantMessagingProtocol(*im_dict));
}
}
return true;
}
} // namespace
// This class handles a single request to download all of a user's contacts.
//
// First, the contacts feed is downloaded via GetContactsOperation and parsed.
// Individual contacts::Contact objects are created using the data from the
// feed. Next, GetContactPhotoOperations are created and used to start
// downloading contacts' photos in parallel. When all photos have been
// downloaded, the contacts are passed to the passed-in callback.
class GDataContactsService::DownloadContactsRequest
: public base::SupportsWeakPtr<DownloadContactsRequest> {
public:
DownloadContactsRequest(GDataContactsService* service,
Profile* profile,
GDataOperationRunner* runner,
SuccessCallback success_callback,
FailureCallback failure_callback,
const base::Time& min_update_time,
int max_simultaneous_photo_downloads)
: service_(service),
profile_(profile),
runner_(runner),
success_callback_(success_callback),
failure_callback_(failure_callback),
min_update_time_(min_update_time),
contacts_(new ScopedVector<contacts::Contact>),
max_simultaneous_photo_downloads_(max_simultaneous_photo_downloads),
num_in_progress_photo_downloads_(0),
photo_download_failed_(false) {
DCHECK(service_);
DCHECK(profile_);
DCHECK(runner_);
}
~DownloadContactsRequest() {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
service_ = NULL;
profile_ = NULL;
runner_ = NULL;
}
// Issues the initial request to download the contact feed.
void Run() {
GetContactsOperation* operation =
new GetContactsOperation(
runner_->operation_registry(),
profile_,
min_update_time_,
base::Bind(&DownloadContactsRequest::HandleFeedData,
base::Unretained(this)));
if (!service_->feed_url_for_testing_.is_empty())
operation->set_feed_url_for_testing(service_->feed_url_for_testing_);
runner_->StartOperationWithRetry(operation);
}
private:
// Callback for GetContactsOperation calls.
void HandleFeedData(GDataErrorCode error,
scoped_ptr<base::Value> feed_data) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
if (error != HTTP_SUCCESS) {
LOG(WARNING) << "Got error " << error << " while downloading contacts";
failure_callback_.Run();
service_->OnRequestComplete(this);
return;
}
VLOG(2) << "Got feed data:\n" << PrettyPrintValue(*(feed_data.get()));
if (!ProcessFeedData(*feed_data.get())) {
LOG(WARNING) << "Unable to process feed data";
failure_callback_.Run();
service_->OnRequestComplete(this);
return;
}
CheckCompletion();
}
// Processes the raw contacts feed from |feed_data| and fills |contacts_|.
// Returns true on success.
bool ProcessFeedData(const base::Value& feed_data) {
const DictionaryValue* toplevel_dict = NULL;
if (!feed_data.GetAsDictionary(&toplevel_dict)) {
LOG(WARNING) << "Top-level object is not a dictionary";
return false;
}
const DictionaryValue* feed_dict = NULL;
if (!toplevel_dict->GetDictionary(kFeedField, &feed_dict)) {
LOG(WARNING) << "Feed dictionary missing";
return false;
}
// Check the category field to confirm that this is actually a contact feed.
const ListValue* category_list = NULL;
if (!feed_dict->GetList(kCategoryField, &category_list)) {
LOG(WARNING) << "Category list missing";
return false;
}
DictionaryValue* category_dict = NULL;
if (!category_list->GetSize() == 1 ||
!category_list->GetDictionary(0, &category_dict)) {
LOG(WARNING) << "Unable to get dictionary from category list of size "
<< category_list->GetSize();
return false;
}
std::string category_scheme, category_term;
if (!category_dict->GetString(kCategorySchemeField, &category_scheme) ||
!category_dict->GetString(kCategoryTermField, &category_term) ||
category_scheme != kCategorySchemeValue ||
category_term != kCategoryTermValue) {
LOG(WARNING) << "Unexpected category (scheme was \"" << category_scheme
<< "\", term was \"" << category_term << "\")";
return false;
}
// A missing entry list means no entries (maybe we're doing an incremental
// update and nothing has changed).
const ListValue* entry_list = NULL;
if (!feed_dict->GetList(kEntryField, &entry_list))
return true;
contacts_needing_photo_downloads_.reserve(entry_list->GetSize());
for (ListValue::const_iterator entry_it = entry_list->begin();
entry_it != entry_list->end(); ++entry_it) {
const size_t index = (entry_it - entry_list->begin());
const DictionaryValue* contact_dict = NULL;
if (!(*entry_it)->GetAsDictionary(&contact_dict)) {
LOG(WARNING) << "Entry " << index << " isn't a dictionary";
return false;
}
scoped_ptr<contacts::Contact> contact(new contacts::Contact);
if (!FillContactFromDictionary(*contact_dict, contact.get())) {
LOG(WARNING) << "Unable to fill entry " << index;
return false;
}
VLOG(1) << "Got contact " << index << ":"
<< " id=" << contact->provider_id()
<< " full_name=\"" << contact->full_name() << "\""
<< " update_time=" << contact->update_time();
std::string photo_url = GetPhotoUrl(*contact_dict);
if (!photo_url.empty()) {
if (!service_->rewrite_photo_url_callback_for_testing_.is_null()) {
photo_url =
service_->rewrite_photo_url_callback_for_testing_.Run(photo_url);
}
contact_photo_urls_[contact.get()] = photo_url;
contacts_needing_photo_downloads_.push_back(contact.get());
}
contacts_->push_back(contact.release());
}
return true;
}
// If we're done downloading photos, invokes a callback and deletes |this|.
// Otherwise, starts one or more downloads of URLs from
// |contacts_needing_photo_downloads_|.
void CheckCompletion() {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
if (contacts_needing_photo_downloads_.empty() &&
num_in_progress_photo_downloads_ == 0) {
VLOG(1) << "Done downloading photos; invoking callback";
if (photo_download_failed_)
failure_callback_.Run();
else
success_callback_.Run(contacts_.Pass());
service_->OnRequestComplete(this);
return;
}
while (!contacts_needing_photo_downloads_.empty() &&
(num_in_progress_photo_downloads_ <
max_simultaneous_photo_downloads_)) {
contacts::Contact* contact = contacts_needing_photo_downloads_.back();
contacts_needing_photo_downloads_.pop_back();
DCHECK(contact_photo_urls_.count(contact));
std::string url = contact_photo_urls_[contact];
VLOG(1) << "Starting download of photo " << url << " for "
<< contact->provider_id();
runner_->StartOperationWithRetry(
new GetContactPhotoOperation(
runner_->operation_registry(),
profile_,
GURL(url),
base::Bind(&DownloadContactsRequest::HandlePhotoData,
AsWeakPtr(), contact)));
num_in_progress_photo_downloads_++;
}
}
// Callback for GetContactPhotoOperation calls. Updates the associated
// Contact and checks for completion.
void HandlePhotoData(contacts::Contact* contact,
GDataErrorCode error,
scoped_ptr<std::string> download_data) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
VLOG(1) << "Got photo data for " << contact->provider_id()
<< " (error=" << error << " size=" << download_data->size() << ")";
num_in_progress_photo_downloads_--;
if (error != HTTP_SUCCESS) {
LOG(WARNING) << "Got error " << error << " while downloading photo "
<< "for " << contact->provider_id();
// TODO(derat): Retry several times for temporary failures?
photo_download_failed_ = true;
// Make sure we don't start any more downloads.
contacts_needing_photo_downloads_.clear();
CheckCompletion();
return;
}
contact->set_raw_untrusted_photo(*download_data);
CheckCompletion();
}
private:
typedef std::map<contacts::Contact*, std::string> ContactPhotoUrls;
GDataContactsService* service_; // not owned
Profile* profile_; // not owned
GDataOperationRunner* runner_; // not owned
SuccessCallback success_callback_;
FailureCallback failure_callback_;
base::Time min_update_time_;
scoped_ptr<ScopedVector<contacts::Contact> > contacts_;
// Map from a contact to the URL at which its photo is located.
// Contacts without photos do not appear in this map.
ContactPhotoUrls contact_photo_urls_;
// Contacts that have photos that we still need to start downloading.
// When we start a download, the contact is removed from this list.
std::vector<contacts::Contact*> contacts_needing_photo_downloads_;
// Maximum number of photos we'll try to download at once.
int max_simultaneous_photo_downloads_;
// Number of in-progress photo downloads.
int num_in_progress_photo_downloads_;
// Did we encounter a fatal error while downloading a photo?
bool photo_download_failed_;
DISALLOW_COPY_AND_ASSIGN(DownloadContactsRequest);
};
GDataContactsService::GDataContactsService(Profile* profile)
: profile_(profile),
runner_(new GDataOperationRunner(profile)),
max_simultaneous_photo_downloads_(kMaxSimultaneousPhotoDownloads) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
DCHECK(profile_);
}
GDataContactsService::~GDataContactsService() {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
runner_->CancelAll();
STLDeleteContainerPointers(requests_.begin(), requests_.end());
requests_.clear();
}
GDataAuthService* GDataContactsService::auth_service_for_testing() {
return runner_->auth_service();
}
void GDataContactsService::Initialize() {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
runner_->Initialize();
}
void GDataContactsService::DownloadContacts(SuccessCallback success_callback,
FailureCallback failure_callback,
const base::Time& min_update_time) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
DownloadContactsRequest* request =
new DownloadContactsRequest(this,
profile_,
runner_.get(),
success_callback,
failure_callback,
min_update_time,
max_simultaneous_photo_downloads_);
VLOG(1) << "Starting contacts download with request " << request;
requests_.insert(request);
request->Run();
}
void GDataContactsService::OnRequestComplete(DownloadContactsRequest* request) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
DCHECK(request);
VLOG(1) << "Download request " << request << " complete";
requests_.erase(request);
delete request;
}
} // namespace contacts
// 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.
#ifndef CHROME_BROWSER_CHROMEOS_GDATA_GDATA_CONTACTS_SERVICE_H_
#define CHROME_BROWSER_CHROMEOS_GDATA_GDATA_CONTACTS_SERVICE_H_
#include <set>
#include <vector>
#include "base/basictypes.h"
#include "base/callback.h"
#include "base/compiler_specific.h"
#include "base/memory/scoped_ptr.h"
#include "base/memory/scoped_vector.h"
#include "base/time.h"
#include "chrome/browser/chromeos/gdata/gdata_errorcode.h"
#include "googleurl/src/gurl.h"
class Profile;
namespace base {
class Value;
}
namespace contacts {
class Contact;
}
namespace gdata {
class GDataAuthService;
class GDataOperationRunner;
// Interface for fetching a user's Google contacts via the Contacts API
// (described at https://developers.google.com/google-apps/contacts/v3/).
class GDataContactsServiceInterface {
public:
typedef base::Callback<void(scoped_ptr<ScopedVector<contacts::Contact> >)>
SuccessCallback;
typedef base::Closure FailureCallback;
virtual ~GDataContactsServiceInterface() {}
virtual void Initialize() = 0;
// Downloads all contacts changed at or after |min_update_time| and invokes
// the appropriate callback asynchronously on the UI thread when complete. If
// min_update_time.is_null() is true, all contacts will be returned.
virtual void DownloadContacts(SuccessCallback success_callback,
FailureCallback failure_callback,
const base::Time& min_update_time) = 0;
protected:
GDataContactsServiceInterface() {}
private:
DISALLOW_COPY_AND_ASSIGN(GDataContactsServiceInterface);
};
class GDataContactsService : public GDataContactsServiceInterface {
public:
typedef base::Callback<std::string(const std::string&)>
RewritePhotoUrlCallback;
explicit GDataContactsService(Profile* profile);
virtual ~GDataContactsService();
GDataAuthService* auth_service_for_testing();
void set_max_simultaneous_photo_downloads_for_testing(int max_downloads) {
max_simultaneous_photo_downloads_ = max_downloads;
}
void set_feed_url_for_testing(const GURL& url) {
feed_url_for_testing_ = url;
}
void set_rewrite_photo_url_callback_for_testing(RewritePhotoUrlCallback cb) {
rewrite_photo_url_callback_for_testing_ = cb;
}
// Overridden from GDataContactsServiceInterface:
virtual void Initialize() OVERRIDE;
virtual void DownloadContacts(SuccessCallback success_callback,
FailureCallback failure_callback,
const base::Time& min_update_time) OVERRIDE;
private:
class DownloadContactsRequest;
// Invoked by a download request once it's finished (either successfully or
// unsuccessfully).
void OnRequestComplete(DownloadContactsRequest* request);
Profile* profile_; // not owned
scoped_ptr<GDataOperationRunner> runner_;
// In-progress download requests. Pointers are owned by this class.
std::set<DownloadContactsRequest*> requests_;
// If non-empty, URL that will be used to fetch the feed. URLs contained
// within the feed will also be modified to use the host and port from this
// member.
GURL feed_url_for_testing_;
// Maximum number of photos we'll try to download at once (per
// DownloadContacts() request).
int max_simultaneous_photo_downloads_;
// Callback that's invoked to rewrite photo URLs for tests.
// This is needed for tests that serve static feed data from a host/port
// that's only known at runtime.
RewritePhotoUrlCallback rewrite_photo_url_callback_for_testing_;
DISALLOW_COPY_AND_ASSIGN(GDataContactsService);
};
} // namespace gdata
#endif // CHROME_BROWSER_CHROMEOS_GDATA_GDATA_CONTACTS_SERVICE_H_
// 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/chromeos/gdata/gdata_contacts_service.h"
#include "base/bind.h"
#include "base/file_path.h"
#include "base/file_util.h"
#include "base/message_loop.h"
#include "base/stringprintf.h"
#include "base/time.h"
#include "chrome/browser/chromeos/contacts/contact.pb.h"
#include "chrome/browser/chromeos/contacts/contact_test_util.h"
#include "chrome/browser/chromeos/gdata/gdata_auth_service.h"
#include "chrome/browser/chromeos/gdata/gdata_util.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/test/test_utils.h"
#include "net/test/test_server.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/gfx/size.h"
using content::BrowserThread;
namespace gdata {
namespace {
// Path to the files that are served by the test server.
const FilePath::CharType kTestDataPath[] =
FILE_PATH_LITERAL("chrome/test/data");
// Base URL where feeds are located on the test server.
const char kFeedBaseUrl[] = "files/chromeos/gdata/contacts/";
// Width and height of /photo.png on the test server.
const int kPhotoSize = 48;
// Initializes |contact| using the passed-in values.
void InitContact(const std::string& provider_id,
const std::string& rfc_3339_update_time,
bool deleted,
const std::string& full_name,
const std::string& given_name,
const std::string& additional_name,
const std::string& family_name,
const std::string& name_prefix,
const std::string& name_suffix,
contacts::Contact* contact) {
DCHECK(contact);
contact->set_provider_id(provider_id);
base::Time update_time;
CHECK(util::GetTimeFromString(rfc_3339_update_time, &update_time))
<< "Unable to parse time \"" << rfc_3339_update_time << "\"";
contact->set_update_time(update_time.ToInternalValue());
contact->set_deleted(deleted);
contact->set_full_name(full_name);
contact->set_given_name(given_name);
contact->set_additional_name(additional_name);
contact->set_family_name(family_name);
contact->set_name_prefix(name_prefix);
contact->set_name_suffix(name_suffix);
}
class GDataContactsServiceTest : public InProcessBrowserTest {
public:
GDataContactsServiceTest()
: InProcessBrowserTest(),
test_server_(net::TestServer::TYPE_GDATA,
net::TestServer::kLocalhost,
FilePath(kTestDataPath)),
download_was_successful_(false) {
}
virtual void SetUpOnMainThread() OVERRIDE {
ASSERT_TRUE(test_server_.Start());
service_.reset(new GDataContactsService(browser()->profile()));
service_->Initialize();
service_->auth_service_for_testing()->set_access_token_for_testing(
net::TestServer::kGDataAuthToken);
service_->set_rewrite_photo_url_callback_for_testing(
base::Bind(&GDataContactsServiceTest::RewritePhotoUrl,
base::Unretained(this)));
}
virtual void CleanUpOnMainThread() {
service_.reset();
}
protected:
GDataContactsService* service() { return service_.get(); }
// Downloads contacts from |feed_filename| (within the chromeos/gdata/contacts
// test data directory). |min_update_time| is appended to the URL and the
// resulting contacts are swapped into |contacts|. Returns false if the
// download failed.
bool Download(const std::string& feed_filename,
const base::Time& min_update_time,
scoped_ptr<ScopedVector<contacts::Contact> >* contacts) {
DCHECK(contacts);
service_->set_feed_url_for_testing(
test_server_.GetURL(kFeedBaseUrl + feed_filename));
service_->DownloadContacts(
base::Bind(&GDataContactsServiceTest::OnSuccess,
base::Unretained(this)),
base::Bind(&GDataContactsServiceTest::OnFailure,
base::Unretained(this)),
min_update_time);
content::RunMessageLoop();
contacts->swap(downloaded_contacts_);
return download_was_successful_;
}
private:
// Rewrites |original_url|, a photo URL from a contacts feed, to instead point
// at a file on |test_server_|.
std::string RewritePhotoUrl(const std::string& original_url) {
return test_server_.GetURL(kFeedBaseUrl + GURL(original_url).path()).spec();
}
// Handles success for Download().
void OnSuccess(scoped_ptr<ScopedVector<contacts::Contact> > contacts) {
CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
download_was_successful_ = true;
downloaded_contacts_.swap(contacts);
MessageLoop::current()->Quit();
}
// Handles failure for Download().
void OnFailure() {
CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
download_was_successful_ = false;
downloaded_contacts_.reset(new ScopedVector<contacts::Contact>());
MessageLoop::current()->Quit();
}
net::TestServer test_server_;
scoped_ptr<GDataContactsService> service_;
// Was the last download successful? Used to pass the result back from
// OnSuccess() and OnFailure() to Download().
bool download_was_successful_;
// Used to pass downloaded contacts back to Download().
scoped_ptr<ScopedVector<contacts::Contact> > downloaded_contacts_;
};
} // namespace
// Test that we report failure for feeds that are broken in various ways.
IN_PROC_BROWSER_TEST_F(GDataContactsServiceTest, BrokenFeeds) {
scoped_ptr<ScopedVector<contacts::Contact> > contacts;
EXPECT_FALSE(Download("some_bogus_file", base::Time(), &contacts));
EXPECT_FALSE(Download("empty.txt", base::Time(), &contacts));
EXPECT_FALSE(Download("not_json.txt", base::Time(), &contacts));
EXPECT_FALSE(Download("not_dictionary.json", base::Time(), &contacts));
EXPECT_FALSE(Download("no_feed.json", base::Time(), &contacts));
EXPECT_FALSE(Download("no_category.json", base::Time(), &contacts));
EXPECT_FALSE(Download("wrong_category.json", base::Time(), &contacts));
EXPECT_FALSE(Download("feed_photo_404.json", base::Time(), &contacts));
}
// Check that we're able to download an empty feed and a normal-looking feed
// with two regular contacts and one deleted one.
IN_PROC_BROWSER_TEST_F(GDataContactsServiceTest, Download) {
scoped_ptr<ScopedVector<contacts::Contact> > contacts;
EXPECT_TRUE(Download("no_entries.json", base::Time(), &contacts));
EXPECT_TRUE(contacts->empty());
EXPECT_TRUE(Download("feed.json", base::Time(), &contacts));
// All of these expected values are hardcoded in the feed.
scoped_ptr<contacts::Contact> contact1(new contacts::Contact);
InitContact("http://example.com/1",
"2012-06-04T15:53:36.023Z",
false, "Joe Contact", "Joe", "", "Contact", "", "",
contact1.get());
contacts::test::SetPhoto(gfx::Size(kPhotoSize, kPhotoSize), contact1.get());
contacts::test::AddEmailAddress(
"joe.contact@gmail.com",
contacts::Contact_AddressType_Relation_OTHER, "", true, contact1.get());
contacts::test::AddPostalAddress(
"345 Spear St\nSan Francisco CA 94105",
contacts::Contact_AddressType_Relation_HOME, "", false, contact1.get());
scoped_ptr<contacts::Contact> contact2(new contacts::Contact);
InitContact("http://example.com/2",
"2012-06-21T16:20:13.208Z",
false, "Dr. Jane Liz Doe Sr.", "Jane", "Liz", "Doe", "Dr.", "Sr.",
contact2.get());
contacts::test::AddEmailAddress(
"jane.doe@gmail.com",
contacts::Contact_AddressType_Relation_HOME, "", true, contact2.get());
contacts::test::AddEmailAddress(
"me@privacy.net",
contacts::Contact_AddressType_Relation_WORK, "", false, contact2.get());
contacts::test::AddEmailAddress(
"foo@example.org",
contacts::Contact_AddressType_Relation_OTHER, "Fake", false,
contact2.get());
contacts::test::AddPhoneNumber(
"123-456-7890",
contacts::Contact_AddressType_Relation_MOBILE, "", false,
contact2.get());
contacts::test::AddPhoneNumber(
"234-567-8901",
contacts::Contact_AddressType_Relation_OTHER, "grandcentral", false,
contact2.get());
contacts::test::AddPostalAddress(
"100 Elm St\nSan Francisco, CA 94110",
contacts::Contact_AddressType_Relation_HOME, "", false, contact2.get());
contacts::test::AddInstantMessagingAddress(
"foo@example.org",
contacts::Contact_InstantMessagingAddress_Protocol_GOOGLE_TALK,
contacts::Contact_AddressType_Relation_OTHER, "", false,
contact2.get());
contacts::test::AddInstantMessagingAddress(
"12345678",
contacts::Contact_InstantMessagingAddress_Protocol_ICQ,
contacts::Contact_AddressType_Relation_OTHER, "", false,
contact2.get());
scoped_ptr<contacts::Contact> contact3(new contacts::Contact);
InitContact("http://example.com/3",
"2012-07-23T23:07:06.133Z",
true, "", "", "", "", "", "",
contact3.get());
EXPECT_EQ(contacts::test::VarContactsToString(
3, contact1.get(), contact2.get(), contact3.get()),
contacts::test::ContactsToString(*contacts));
}
// Download a feed containing more photos than we're able to download in
// parallel to check that we still end up with all the photos.
IN_PROC_BROWSER_TEST_F(GDataContactsServiceTest, ParallelPhotoDownload) {
// The feed used for this test contains 8 contacts.
const int kNumContacts = 8;
service()->set_max_simultaneous_photo_downloads_for_testing(2);
scoped_ptr<ScopedVector<contacts::Contact> > contacts;
EXPECT_TRUE(Download("feed_multiple_photos.json", base::Time(), &contacts));
ASSERT_EQ(static_cast<size_t>(kNumContacts), contacts->size());
ScopedVector<contacts::Contact> expected_contacts;
for (int i = 0; i < kNumContacts; ++i) {
contacts::Contact* contact = new contacts::Contact;
InitContact(base::StringPrintf("http://example.com/%d", i + 1),
"2012-06-04T15:53:36.023Z",
false, "", "", "", "", "", "", contact);
contacts::test::SetPhoto(gfx::Size(kPhotoSize, kPhotoSize), contact);
expected_contacts.push_back(contact);
}
EXPECT_EQ(contacts::test::ContactsToString(expected_contacts),
contacts::test::ContactsToString(*contacts));
}
} // namespace gdata
......@@ -7,6 +7,7 @@
#include "base/string_number_conversions.h"
#include "base/stringprintf.h"
#include "base/values.h"
#include "chrome/browser/chromeos/gdata/gdata_util.h"
#include "chrome/browser/chromeos/gdata/gdata_wapi_parser.h"
#include "chrome/common/net/url_util.h"
#include "content/public/browser/browser_thread.h"
......@@ -48,6 +49,18 @@ const char kGetDocumentEntryURLFormat[] =
const char kAccountMetadataURL[] =
"https://docs.google.com/feeds/metadata/default";
// URL requesting all contacts.
// TODO(derat): Per https://goo.gl/AufHP, "The feed may not contain all of the
// user's contacts, because there's a default limit on the number of results
// returned." Decide if 10000 is reasonable or not.
const char kGetContactsURL[] =
"https://www.google.com/m8/feeds/contacts/default/full"
"?alt=json&showdeleted=true&max-results=10000";
// Query parameter optionally appended to |kGetContactsURL| to return only
// recently-updated contacts.
const char kGetContactsUpdatedMinParam[] = "updated-min";
const char kUploadContentRange[] = "Content-Range: bytes ";
const char kUploadContentType[] = "X-Upload-Content-Type: ";
const char kUploadContentLength[] = "X-Upload-Content-Length: ";
......@@ -875,4 +888,62 @@ void ResumeUploadOperation::OnURLFetchUploadProgress(
NotifyProgress(params_.start_range + current, params_.content_length);
}
//============================ GetContactsOperation ============================
GetContactsOperation::GetContactsOperation(GDataOperationRegistry* registry,
Profile* profile,
const base::Time& min_update_time,
const GetDataCallback& callback)
: GetDataOperation(registry, profile, callback),
min_update_time_(min_update_time) {
}
GetContactsOperation::~GetContactsOperation() {}
GURL GetContactsOperation::GetURL() const {
if (!feed_url_for_testing_.is_empty())
return GURL(feed_url_for_testing_);
GURL url(kGetContactsURL);
if (!min_update_time_.is_null()) {
std::string time_rfc3339 = util::FormatTimeAsString(min_update_time_);
url = chrome_common_net::AppendQueryParameter(
url, kGetContactsUpdatedMinParam, time_rfc3339);
}
return url;
}
//========================== GetContactPhotoOperation ==========================
GetContactPhotoOperation::GetContactPhotoOperation(
GDataOperationRegistry* registry,
Profile* profile,
const GURL& photo_url,
const GetDownloadDataCallback& callback)
: UrlFetchOperationBase(registry, profile),
photo_url_(photo_url),
callback_(callback) {
}
GetContactPhotoOperation::~GetContactPhotoOperation() {}
GURL GetContactPhotoOperation::GetURL() const {
return photo_url_;
}
bool GetContactPhotoOperation::ProcessURLFetchResults(
const net::URLFetcher* source) {
GDataErrorCode code = static_cast<GDataErrorCode>(source->GetResponseCode());
scoped_ptr<std::string> data(new std::string);
source->GetResponseAsString(data.get());
callback_.Run(code, data.Pass());
return code == HTTP_SUCCESS;
}
void GetContactPhotoOperation::RunCallbackOnPrematureFailure(
GDataErrorCode code) {
scoped_ptr<std::string> data(new std::string);
callback_.Run(code, data.Pass());
}
} // namespace gdata
......@@ -383,6 +383,63 @@ class ResumeUploadOperation : public UrlFetchOperationBase {
DISALLOW_COPY_AND_ASSIGN(ResumeUploadOperation);
};
//============================ GetContactsOperation ============================
// This class fetches a user's contacts.
class GetContactsOperation : public GetDataOperation {
public:
GetContactsOperation(GDataOperationRegistry* registry,
Profile* profile,
const base::Time& min_update_time,
const GetDataCallback& callback);
virtual ~GetContactsOperation();
void set_feed_url_for_testing(const GURL& url) {
feed_url_for_testing_ = url;
}
protected:
// Overridden from GetDataOperation.
virtual GURL GetURL() const OVERRIDE;
private:
// If non-empty, URL of the feed to fetch.
GURL feed_url_for_testing_;
// If is_null() is false, contains a minimum last-updated time that will be
// used to filter contacts.
base::Time min_update_time_;
DISALLOW_COPY_AND_ASSIGN(GetContactsOperation);
};
//========================== GetContactPhotoOperation ==========================
// This class fetches a contact's photo.
class GetContactPhotoOperation : public UrlFetchOperationBase {
public:
GetContactPhotoOperation(GDataOperationRegistry* registry,
Profile* profile,
const GURL& photo_url,
const GetDownloadDataCallback& callback);
virtual ~GetContactPhotoOperation();
protected:
// Overridden from UrlFetchOperationBase.
virtual GURL GetURL() const OVERRIDE;
virtual bool ProcessURLFetchResults(const net::URLFetcher* source) OVERRIDE;
virtual void RunCallbackOnPrematureFailure(GDataErrorCode code) OVERRIDE;
private:
// Location of the photo to fetch.
GURL photo_url_;
// Callback to which the photo data is passed.
GetDownloadDataCallback callback_;
DISALLOW_COPY_AND_ASSIGN(GetContactPhotoOperation);
};
} // namespace gdata
#endif // CHROME_BROWSER_CHROMEOS_GDATA_GDATA_OPERATIONS_H_
......@@ -8,6 +8,7 @@
#include "base/bind_helpers.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/chromeos/gdata/drive_webapps_registry.h"
#include "chrome/browser/chromeos/gdata/gdata_contacts_service.h"
#include "chrome/browser/chromeos/gdata/gdata_documents_service.h"
#include "chrome/browser/chromeos/gdata/gdata_download_observer.h"
#include "chrome/browser/chromeos/gdata/gdata_file_system.h"
......@@ -70,6 +71,7 @@ void GDataSystemService::Initialize(
download_observer_.reset(new GDataDownloadObserver(uploader(),
file_system()));
sync_client_.reset(new GDataSyncClient(profile_, file_system(), cache()));
contacts_service_.reset(new GDataContactsService(profile_));
sync_client_->Initialize();
file_system_->Initialize();
......@@ -84,6 +86,7 @@ void GDataSystemService::Initialize(
GDataCache::CACHE_TYPE_TMP_DOWNLOADS));
AddDriveMountPoint();
contacts_service_->Initialize();
}
void GDataSystemService::Shutdown() {
......@@ -91,6 +94,7 @@ void GDataSystemService::Shutdown() {
RemoveDriveMountPoint();
// Shut down the member objects in the reverse order of creation.
contacts_service_.reset();
sync_client_.reset();
download_observer_.reset();
file_system_.reset();
......
......@@ -20,6 +20,7 @@ namespace gdata {
class DocumentsServiceInterface;
class DriveWebAppsRegistry;
class GDataCache;
class GDataContactsService;
class GDataDownloadObserver;
class GDataFileSystemInterface;
class GDataSyncClient;
......@@ -33,19 +34,11 @@ class GDataUploader;
// created per-profile.
class GDataSystemService : public ProfileKeyedService {
public:
// Returns the documents service instance.
DocumentsServiceInterface* docs_service() { return documents_service_.get(); }
// Returns the cache instance.
GDataCache* cache() { return cache_; }
// Returns the file system instance.
GDataFileSystemInterface* file_system() { return file_system_.get(); }
// Returns the uploader instance.
GDataUploader* uploader() { return uploader_.get(); }
// Returns the file system instance.
GDataContactsService* contacts_service() { return contacts_service_.get(); }
DriveWebAppsRegistry* webapps_registry() { return webapps_registry_.get(); }
// ProfileKeyedService override:
......@@ -76,6 +69,7 @@ class GDataSystemService : public ProfileKeyedService {
scoped_ptr<GDataFileSystemInterface> file_system_;
scoped_ptr<GDataDownloadObserver> download_observer_;
scoped_ptr<GDataSyncClient> sync_client_;
scoped_ptr<GDataContactsService> contacts_service_;
DISALLOW_COPY_AND_ASSIGN(GDataSystemService);
};
......
......@@ -46,6 +46,9 @@ const char kDocsListScope[] = "https://docs.google.com/feeds/";
const char kSpreadsheetsScope[] = "https://spreadsheets.google.com/feeds/";
const char kUserContentScope[] = "https://docs.googleusercontent.com/";
// OAuth scope for the Contacts API.
const char kContactsScope[] = "https://www.google.com/m8/feeds/";
// OAuth scope for Drive API.
const char kDriveAppsScope[] = "https://www.googleapis.com/auth/drive.apps";
......@@ -71,6 +74,7 @@ void AuthOperation::Start() {
scopes.push_back(kDocsListScope);
scopes.push_back(kSpreadsheetsScope);
scopes.push_back(kUserContentScope);
scopes.push_back(kContactsScope);
if (gdata::util::IsDriveV2ApiEnabled())
scopes.push_back(kDriveAppsScope);
oauth2_access_token_fetcher_.reset(new OAuth2AccessTokenFetcher(
......
......@@ -549,6 +549,8 @@
'browser/chromeos/gdata/gdata_cache.h',
'browser/chromeos/gdata/gdata_cache_metadata.cc',
'browser/chromeos/gdata/gdata_cache_metadata.h',
'browser/chromeos/gdata/gdata_contacts_service.cc',
'browser/chromeos/gdata/gdata_contacts_service.h',
'browser/chromeos/gdata/gdata_db.h',
'browser/chromeos/gdata/gdata_db_factory.cc',
'browser/chromeos/gdata/gdata_db_factory.h',
......
......@@ -2633,6 +2633,7 @@
'browser/chromeos/bluetooth/test/mock_bluetooth_adapter.h',
'browser/chromeos/bluetooth/test/mock_bluetooth_device.cc',
'browser/chromeos/bluetooth/test/mock_bluetooth_device.h',
'browser/chromeos/contacts/contact_test_util.cc',
'browser/chromeos/cros/cros_in_process_browser_test.cc',
'browser/chromeos/cros/cros_in_process_browser_test.h',
'browser/chromeos/cros/cros_mock.cc',
......@@ -2644,6 +2645,7 @@
'browser/chromeos/extensions/file_browser_private_apitest.cc',
'browser/chromeos/extensions/echo_private_apitest.cc',
'browser/chromeos/extensions/external_filesystem_apitest.cc',
'browser/chromeos/gdata/gdata_contacts_service_browsertest.cc',
'browser/chromeos/gdata/gdata_documents_service_browsertest.cc',
'browser/chromeos/gdata/mock_gdata_documents_service.cc',
'browser/chromeos/gdata/mock_gdata_documents_service.h',
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment