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.
5 #include "chrome/browser/chromeos/contacts/gdata_contacts_service.h"
12 #include "base/json/json_value_converter.h"
13 #include "base/json/json_writer.h"
14 #include "base/logging.h"
15 #include "base/memory/weak_ptr.h"
16 #include "base/metrics/histogram.h"
17 #include "base/stl_util.h"
18 #include "base/string_util.h"
19 #include "base/time.h"
20 #include "base/timer.h"
21 #include "base/values.h"
22 #include "chrome/browser/chromeos/contacts/contact.pb.h"
23 #include "chrome/browser/google_apis/gdata_errorcode.h"
24 #include "chrome/browser/google_apis/gdata_operations.h"
25 #include "chrome/browser/google_apis/operation_runner.h"
26 #include "chrome/browser/google_apis/time_util.h"
27 #include "content/public/browser/browser_thread.h"
29 using content::BrowserThread
;
35 // Download outcomes reported via the "Contacts.FullUpdateResult" and
36 // "Contacts.IncrementalUpdateResult" histograms.
37 enum HistogramResult
{
38 HISTOGRAM_RESULT_SUCCESS
= 0,
39 HISTOGRAM_RESULT_GROUPS_DOWNLOAD_FAILURE
= 1,
40 HISTOGRAM_RESULT_GROUPS_PARSE_FAILURE
= 2,
41 HISTOGRAM_RESULT_MY_CONTACTS_GROUP_NOT_FOUND
= 3,
42 HISTOGRAM_RESULT_CONTACTS_DOWNLOAD_FAILURE
= 4,
43 HISTOGRAM_RESULT_CONTACTS_PARSE_FAILURE
= 5,
44 HISTOGRAM_RESULT_PHOTO_DOWNLOAD_FAILURE
= 6,
45 HISTOGRAM_RESULT_MAX_VALUE
= 7,
48 // Maximum number of profile photos that we'll download per second.
49 // At values above 10, Google starts returning 503 errors.
50 const int kMaxPhotoDownloadsPerSecond
= 10;
52 // Give up after seeing more than this many transient errors while trying to
53 // download a photo for a single contact.
54 const int kMaxTransientPhotoDownloadErrorsPerContact
= 2;
56 // Hardcoded system group ID for the "My Contacts" group, per
57 // https://developers.google.com/google-apps/contacts/v3/#contact_group_entry.
58 const char kMyContactsSystemGroupId
[] = "Contacts";
60 // Top-level field in a contact groups feed containing the list of entries.
61 const char kGroupEntryField
[] = "feed.entry";
63 // Field in group entries containing the system group ID (e.g. ID "Contacts"
64 // for the "My Contacts" system group). See
65 // https://developers.google.com/google-apps/contacts/v3/#contact_group_entry
67 const char kSystemGroupIdField
[] = "gContact$systemGroup.id";
69 // Field in the top-level object containing the contacts feed.
70 const char kFeedField
[] = "feed";
72 // Field in the contacts feed containing a list of category information, along
73 // with fields within the dictionaries contained in the list and expected
75 const char kCategoryField
[] = "category";
76 const char kCategorySchemeField
[] = "scheme";
77 const char kCategorySchemeValue
[] = "http://schemas.google.com/g/2005#kind";
78 const char kCategoryTermField
[] = "term";
79 const char kCategoryTermValue
[] =
80 "http://schemas.google.com/contact/2008#contact";
82 // Field in the contacts feed containing a list of contact entries.
83 const char kEntryField
[] = "entry";
85 // Field in group and contact entries containing the item's ID.
86 const char kIdField
[] = "id.$t";
88 // Top-level fields in contact entries.
89 const char kDeletedField
[] = "gd$deleted";
90 const char kFullNameField
[] = "gd$name.gd$fullName.$t";
91 const char kGivenNameField
[] = "gd$name.gd$givenName.$t";
92 const char kAdditionalNameField
[] = "gd$name.gd$additionalName.$t";
93 const char kFamilyNameField
[] = "gd$name.gd$familyName.$t";
94 const char kNamePrefixField
[] = "gd$name.gd$namePrefix.$t";
95 const char kNameSuffixField
[] = "gd$name.gd$nameSuffix.$t";
96 const char kEmailField
[] = "gd$email";
97 const char kPhoneField
[] = "gd$phoneNumber";
98 const char kPostalAddressField
[] = "gd$structuredPostalAddress";
99 const char kInstantMessagingField
[] = "gd$im";
100 const char kLinkField
[] = "link";
101 const char kUpdatedField
[] = "updated.$t";
103 // Fields in entries in the |kEmailField| list.
104 const char kEmailAddressField
[] = "address";
106 // Fields in entries in the |kPhoneField| list.
107 const char kPhoneNumberField
[] = "$t";
109 // Fields in entries in the |kPostalAddressField| list.
110 const char kPostalAddressFormattedField
[] = "gd$formattedAddress.$t";
112 // Fields in entries in the |kInstantMessagingField| list.
113 const char kInstantMessagingAddressField
[] = "address";
114 const char kInstantMessagingProtocolField
[] = "protocol";
115 const char kInstantMessagingProtocolAimValue
[] =
116 "http://schemas.google.com/g/2005#AIM";
117 const char kInstantMessagingProtocolMsnValue
[] =
118 "http://schemas.google.com/g/2005#MSN";
119 const char kInstantMessagingProtocolYahooValue
[] =
120 "http://schemas.google.com/g/2005#YAHOO";
121 const char kInstantMessagingProtocolSkypeValue
[] =
122 "http://schemas.google.com/g/2005#SKYPE";
123 const char kInstantMessagingProtocolQqValue
[] =
124 "http://schemas.google.com/g/2005#QQ";
125 const char kInstantMessagingProtocolGoogleTalkValue
[] =
126 "http://schemas.google.com/g/2005#GOOGLE_TALK";
127 const char kInstantMessagingProtocolIcqValue
[] =
128 "http://schemas.google.com/g/2005#ICQ";
129 const char kInstantMessagingProtocolJabberValue
[] =
130 "http://schemas.google.com/g/2005#JABBER";
132 // Generic fields shared between address-like items (email, postal, etc.).
133 const char kAddressPrimaryField
[] = "primary";
134 const char kAddressPrimaryTrueValue
[] = "true";
135 const char kAddressRelField
[] = "rel";
136 const char kAddressRelHomeValue
[] = "http://schemas.google.com/g/2005#home";
137 const char kAddressRelWorkValue
[] = "http://schemas.google.com/g/2005#work";
138 const char kAddressRelMobileValue
[] = "http://schemas.google.com/g/2005#mobile";
139 const char kAddressLabelField
[] = "label";
141 // Fields in entries in the |kLinkField| list.
142 const char kLinkHrefField
[] = "href";
143 const char kLinkRelField
[] = "rel";
144 const char kLinkETagField
[] = "gd$etag";
145 const char kLinkRelPhotoValue
[] =
146 "http://schemas.google.com/contacts/2008/rel#photo";
148 // OAuth2 scope for the Contacts API.
149 const char kContactsScope
[] = "https://www.google.com/m8/feeds/";
151 // Returns a string containing a pretty-printed JSON representation of |value|.
152 std::string
PrettyPrintValue(const base::Value
& value
) {
154 base::JSONWriter::WriteWithOptions(
155 &value
, base::JSONWriter::OPTIONS_PRETTY_PRINT
, &out
);
159 // Assigns the value at |path| within |dict| to |out|, returning false if the
160 // path wasn't present. Unicode byte order marks are removed from the string.
161 bool GetCleanedString(const DictionaryValue
& dict
,
162 const std::string
& path
,
164 if (!dict
.GetString(path
, out
))
167 // The Unicode byte order mark, U+FEFF, is useless in UTF-8 strings (which are
168 // interpreted one byte at a time).
169 ReplaceSubstringsAfterOffset(out
, 0, "\xEF\xBB\xBF", "");
173 // Returns whether an address is primary, given a dictionary representing a
175 bool IsAddressPrimary(const DictionaryValue
& address_dict
) {
177 address_dict
.GetString(kAddressPrimaryField
, &primary
);
178 return primary
== kAddressPrimaryTrueValue
;
181 // Initializes an AddressType message given a dictionary representing a single
183 void InitAddressType(const DictionaryValue
& address_dict
,
184 Contact_AddressType
* type
) {
189 address_dict
.GetString(kAddressRelField
, &rel
);
190 if (rel
== kAddressRelHomeValue
)
191 type
->set_relation(Contact_AddressType_Relation_HOME
);
192 else if (rel
== kAddressRelWorkValue
)
193 type
->set_relation(Contact_AddressType_Relation_WORK
);
194 else if (rel
== kAddressRelMobileValue
)
195 type
->set_relation(Contact_AddressType_Relation_MOBILE
);
197 type
->set_relation(Contact_AddressType_Relation_OTHER
);
199 GetCleanedString(address_dict
, kAddressLabelField
, type
->mutable_label());
202 // Maps the protocol from a dictionary representing a contact's IM address to a
203 // contacts::Contact_InstantMessagingAddress_Protocol value.
204 contacts::Contact_InstantMessagingAddress_Protocol
205 GetInstantMessagingProtocol(const DictionaryValue
& im_dict
) {
206 std::string protocol
;
207 im_dict
.GetString(kInstantMessagingProtocolField
, &protocol
);
208 if (protocol
== kInstantMessagingProtocolAimValue
)
209 return contacts::Contact_InstantMessagingAddress_Protocol_AIM
;
210 else if (protocol
== kInstantMessagingProtocolMsnValue
)
211 return contacts::Contact_InstantMessagingAddress_Protocol_MSN
;
212 else if (protocol
== kInstantMessagingProtocolYahooValue
)
213 return contacts::Contact_InstantMessagingAddress_Protocol_YAHOO
;
214 else if (protocol
== kInstantMessagingProtocolSkypeValue
)
215 return contacts::Contact_InstantMessagingAddress_Protocol_SKYPE
;
216 else if (protocol
== kInstantMessagingProtocolQqValue
)
217 return contacts::Contact_InstantMessagingAddress_Protocol_QQ
;
218 else if (protocol
== kInstantMessagingProtocolGoogleTalkValue
)
219 return contacts::Contact_InstantMessagingAddress_Protocol_GOOGLE_TALK
;
220 else if (protocol
== kInstantMessagingProtocolIcqValue
)
221 return contacts::Contact_InstantMessagingAddress_Protocol_ICQ
;
222 else if (protocol
== kInstantMessagingProtocolJabberValue
)
223 return contacts::Contact_InstantMessagingAddress_Protocol_JABBER
;
225 return contacts::Contact_InstantMessagingAddress_Protocol_OTHER
;
228 // Gets the photo URL from a contact's dictionary (within the "entry" list).
229 // Returns an empty string if no photo was found.
230 std::string
GetPhotoUrl(const DictionaryValue
& dict
) {
231 const ListValue
* link_list
= NULL
;
232 if (!dict
.GetList(kLinkField
, &link_list
))
233 return std::string();
235 for (size_t i
= 0; i
< link_list
->GetSize(); ++i
) {
236 const DictionaryValue
* link_dict
= NULL
;
237 if (!link_list
->GetDictionary(i
, &link_dict
))
241 if (!link_dict
->GetString(kLinkRelField
, &rel
))
243 if (rel
!= kLinkRelPhotoValue
)
246 // From https://goo.gl/7T6Od: "If a contact does not have a photo, then the
247 // photo link element has no gd:etag attribute."
249 if (!link_dict
->GetString(kLinkETagField
, &etag
))
253 if (link_dict
->GetString(kLinkHrefField
, &url
))
256 return std::string();
259 // Fills a Contact's fields using an entry from a GData feed.
260 bool FillContactFromDictionary(const base::DictionaryValue
& dict
,
261 contacts::Contact
* contact
) {
265 if (!dict
.GetString(kIdField
, contact
->mutable_contact_id()))
269 if (dict
.GetString(kUpdatedField
, &updated
)) {
270 base::Time update_time
;
271 if (!google_apis::util::GetTimeFromString(updated
, &update_time
)) {
272 LOG(WARNING
) << "Unable to parse time \"" << updated
<< "\"";
275 contact
->set_update_time(update_time
.ToInternalValue());
278 const base::Value
* deleted_value
= NULL
;
279 contact
->set_deleted(dict
.Get(kDeletedField
, &deleted_value
));
280 if (contact
->deleted())
283 GetCleanedString(dict
, kFullNameField
, contact
->mutable_full_name());
284 GetCleanedString(dict
, kGivenNameField
, contact
->mutable_given_name());
286 dict
, kAdditionalNameField
, contact
->mutable_additional_name());
287 GetCleanedString(dict
, kFamilyNameField
, contact
->mutable_family_name());
288 GetCleanedString(dict
, kNamePrefixField
, contact
->mutable_name_prefix());
289 GetCleanedString(dict
, kNameSuffixField
, contact
->mutable_name_suffix());
291 const ListValue
* email_list
= NULL
;
292 if (dict
.GetList(kEmailField
, &email_list
)) {
293 for (size_t i
= 0; i
< email_list
->GetSize(); ++i
) {
294 const DictionaryValue
* email_dict
= NULL
;
295 if (!email_list
->GetDictionary(i
, &email_dict
))
298 contacts::Contact_EmailAddress
* email
= contact
->add_email_addresses();
299 if (!GetCleanedString(*email_dict
,
301 email
->mutable_address())) {
304 email
->set_primary(IsAddressPrimary(*email_dict
));
305 InitAddressType(*email_dict
, email
->mutable_type());
309 const ListValue
* phone_list
= NULL
;
310 if (dict
.GetList(kPhoneField
, &phone_list
)) {
311 for (size_t i
= 0; i
< phone_list
->GetSize(); ++i
) {
312 const DictionaryValue
* phone_dict
= NULL
;
313 if (!phone_list
->GetDictionary(i
, &phone_dict
))
316 contacts::Contact_PhoneNumber
* phone
= contact
->add_phone_numbers();
317 if (!GetCleanedString(*phone_dict
,
319 phone
->mutable_number())) {
322 phone
->set_primary(IsAddressPrimary(*phone_dict
));
323 InitAddressType(*phone_dict
, phone
->mutable_type());
327 const ListValue
* address_list
= NULL
;
328 if (dict
.GetList(kPostalAddressField
, &address_list
)) {
329 for (size_t i
= 0; i
< address_list
->GetSize(); ++i
) {
330 const DictionaryValue
* address_dict
= NULL
;
331 if (!address_list
->GetDictionary(i
, &address_dict
))
334 contacts::Contact_PostalAddress
* address
=
335 contact
->add_postal_addresses();
336 if (!GetCleanedString(*address_dict
,
337 kPostalAddressFormattedField
,
338 address
->mutable_address())) {
341 address
->set_primary(IsAddressPrimary(*address_dict
));
342 InitAddressType(*address_dict
, address
->mutable_type());
346 const ListValue
* im_list
= NULL
;
347 if (dict
.GetList(kInstantMessagingField
, &im_list
)) {
348 for (size_t i
= 0; i
< im_list
->GetSize(); ++i
) {
349 const DictionaryValue
* im_dict
= NULL
;
350 if (!im_list
->GetDictionary(i
, &im_dict
))
353 contacts::Contact_InstantMessagingAddress
* im
=
354 contact
->add_instant_messaging_addresses();
355 if (!GetCleanedString(*im_dict
,
356 kInstantMessagingAddressField
,
357 im
->mutable_address())) {
360 im
->set_primary(IsAddressPrimary(*im_dict
));
361 InitAddressType(*im_dict
, im
->mutable_type());
362 im
->set_protocol(GetInstantMessagingProtocol(*im_dict
));
369 // Structure into which we parse the contact groups feed using
370 // JSONValueConverter.
371 struct ContactGroups
{
372 struct ContactGroup
{
374 // "http://www.google.com/m8/feeds/groups/user%40gmail.com/base/6".
375 std::string group_id
;
377 // System group ID (e.g. "Contacts" for the "My Contacts" system group) if
378 // this is a system group, and empty otherwise. See http://goo.gl/oWVnN
380 std::string system_group_id
;
383 // Given a system group ID, returns the corresponding group ID or an empty
384 // string if the requested system group wasn't present.
385 std::string
GetGroupIdForSystemGroup(const std::string
& system_group_id
) {
386 for (size_t i
= 0; i
< groups
.size(); ++i
) {
387 const ContactGroup
& group
= *groups
[i
];
388 if (group
.system_group_id
== system_group_id
)
389 return group
.group_id
;
391 return std::string();
394 // Given |value| corresponding to a dictionary in a contact group feed's
395 // "entry" list, fills |result| with information about the group.
396 static bool GetContactGroup(const base::Value
* value
, ContactGroup
* result
) {
399 const base::DictionaryValue
* dict
= NULL
;
400 if (!value
->GetAsDictionary(&dict
))
403 dict
->GetString(kIdField
, &result
->group_id
);
404 dict
->GetString(kSystemGroupIdField
, &result
->system_group_id
);
408 static void RegisterJSONConverter(
409 base::JSONValueConverter
<ContactGroups
>* converter
) {
411 converter
->RegisterRepeatedCustomValue
<ContactGroup
>(
412 kGroupEntryField
, &ContactGroups::groups
, &GetContactGroup
);
415 ScopedVector
<ContactGroup
> groups
;
420 // This class handles a single request to download all of a user's contacts.
422 // First, the feed containing the user's contact groups is downloaded via
423 // GetContactGroupsOperation and examined to find the ID for the "My Contacts"
424 // group (by default, the contacts API also returns suggested contacts). The
425 // group ID is cached in GDataContactsService so that this step can be skipped
426 // by later DownloadContactRequests.
428 // Next, the contacts feed is downloaded via GetContactsOperation and parsed.
429 // Individual contacts::Contact objects are created using the data from the
432 // Finally, GetContactPhotoOperations are created and used to start downloading
433 // contacts' photos in parallel. When all photos have been downloaded, the
434 // contacts are passed to the passed-in callback.
435 class GDataContactsService::DownloadContactsRequest
{
437 DownloadContactsRequest(GDataContactsService
* service
,
438 google_apis::OperationRunner
* runner
,
439 SuccessCallback success_callback
,
440 FailureCallback failure_callback
,
441 const base::Time
& min_update_time
)
444 success_callback_(success_callback
),
445 failure_callback_(failure_callback
),
446 min_update_time_(min_update_time
),
447 contacts_(new ScopedVector
<contacts::Contact
>),
448 my_contacts_group_id_(service
->cached_my_contacts_group_id_
),
449 num_in_progress_photo_downloads_(0),
450 photo_download_failed_(false),
451 num_photo_download_404_errors_(0),
452 total_photo_bytes_(0),
453 ALLOW_THIS_IN_INITIALIZER_LIST(weak_ptr_factory_(this)) {
454 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
459 ~DownloadContactsRequest() {
460 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
465 const std::string
my_contacts_group_id() const {
466 return my_contacts_group_id_
;
469 // Begins the contacts-downloading process. If the ID for the "My Contacts"
470 // group has previously been cached, then the contacts download is started.
471 // Otherwise, the contact groups download is started.
473 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
474 download_start_time_
= base::TimeTicks::Now();
475 if (!my_contacts_group_id_
.empty()) {
476 StartContactsDownload();
478 google_apis::GetContactGroupsOperation
* operation
=
479 new google_apis::GetContactGroupsOperation(
480 runner_
->operation_registry(),
481 base::Bind(&DownloadContactsRequest::HandleGroupsFeedData
,
482 weak_ptr_factory_
.GetWeakPtr()));
483 if (!service_
->groups_feed_url_for_testing_
.is_empty()) {
484 operation
->set_feed_url_for_testing(
485 service_
->groups_feed_url_for_testing_
);
487 runner_
->StartOperationWithRetry(operation
);
492 // Invokes the failure callback and notifies GDataContactsService that the
494 void ReportFailure(HistogramResult histogram_result
) {
495 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
496 SendHistograms(histogram_result
);
497 failure_callback_
.Run();
498 service_
->OnRequestComplete(this);
501 // Reports UMA stats after the request has completed.
502 void SendHistograms(HistogramResult result
) {
503 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
504 DCHECK_GE(result
, 0);
505 DCHECK_LT(result
, HISTOGRAM_RESULT_MAX_VALUE
);
507 bool success
= (result
== HISTOGRAM_RESULT_SUCCESS
);
508 base::TimeDelta elapsed_time
=
509 base::TimeTicks::Now() - download_start_time_
;
510 int photo_error_percent
= static_cast<int>(
511 100.0 * transient_photo_download_errors_per_contact_
.size() /
512 contact_photo_urls_
.size() + 0.5);
514 if (min_update_time_
.is_null()) {
515 UMA_HISTOGRAM_ENUMERATION("Contacts.FullUpdateResult",
516 result
, HISTOGRAM_RESULT_MAX_VALUE
);
518 UMA_HISTOGRAM_MEDIUM_TIMES("Contacts.FullUpdateDuration",
520 UMA_HISTOGRAM_COUNTS_10000("Contacts.FullUpdateContacts",
522 UMA_HISTOGRAM_COUNTS_10000("Contacts.FullUpdatePhotos",
523 contact_photo_urls_
.size());
524 UMA_HISTOGRAM_MEMORY_KB("Contacts.FullUpdatePhotoBytes",
526 UMA_HISTOGRAM_COUNTS_10000("Contacts.FullUpdatePhoto404Errors",
527 num_photo_download_404_errors_
);
528 UMA_HISTOGRAM_PERCENTAGE("Contacts.FullUpdatePhotoErrorPercent",
529 photo_error_percent
);
532 UMA_HISTOGRAM_ENUMERATION("Contacts.IncrementalUpdateResult",
533 result
, HISTOGRAM_RESULT_MAX_VALUE
);
535 UMA_HISTOGRAM_MEDIUM_TIMES("Contacts.IncrementalUpdateDuration",
537 UMA_HISTOGRAM_COUNTS_10000("Contacts.IncrementalUpdateContacts",
539 UMA_HISTOGRAM_COUNTS_10000("Contacts.IncrementalUpdatePhotos",
540 contact_photo_urls_
.size());
541 UMA_HISTOGRAM_MEMORY_KB("Contacts.IncrementalUpdatePhotoBytes",
543 UMA_HISTOGRAM_COUNTS_10000("Contacts.IncrementalUpdatePhoto404Errors",
544 num_photo_download_404_errors_
);
545 UMA_HISTOGRAM_PERCENTAGE("Contacts.IncrementalUpdatePhotoErrorPercent",
546 photo_error_percent
);
551 // Callback for GetContactGroupsOperation calls. Starts downloading the
552 // actual contacts after finding the "My Contacts" group ID.
553 void HandleGroupsFeedData(google_apis::GDataErrorCode error
,
554 scoped_ptr
<base::Value
> feed_data
) {
555 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
556 if (error
!= google_apis::HTTP_SUCCESS
) {
557 LOG(WARNING
) << "Got error " << error
<< " while downloading groups";
558 ReportFailure(HISTOGRAM_RESULT_GROUPS_DOWNLOAD_FAILURE
);
562 VLOG(2) << "Got groups feed data:\n"
563 << PrettyPrintValue(*(feed_data
.get()));
564 ContactGroups groups
;
565 base::JSONValueConverter
<ContactGroups
> converter
;
566 if (!converter
.Convert(*feed_data
, &groups
)) {
567 LOG(WARNING
) << "Unable to parse groups feed";
568 ReportFailure(HISTOGRAM_RESULT_GROUPS_PARSE_FAILURE
);
572 my_contacts_group_id_
=
573 groups
.GetGroupIdForSystemGroup(kMyContactsSystemGroupId
);
574 if (!my_contacts_group_id_
.empty()) {
575 StartContactsDownload();
577 LOG(WARNING
) << "Unable to find ID for \"My Contacts\" group";
578 ReportFailure(HISTOGRAM_RESULT_MY_CONTACTS_GROUP_NOT_FOUND
);
582 // Starts a download of the contacts from the "My Contacts" group.
583 void StartContactsDownload() {
584 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
585 google_apis::GetContactsOperation
* operation
=
586 new google_apis::GetContactsOperation(
587 runner_
->operation_registry(),
588 my_contacts_group_id_
,
590 base::Bind(&DownloadContactsRequest::HandleContactsFeedData
,
591 weak_ptr_factory_
.GetWeakPtr()));
592 if (!service_
->contacts_feed_url_for_testing_
.is_empty()) {
593 operation
->set_feed_url_for_testing(
594 service_
->contacts_feed_url_for_testing_
);
596 runner_
->StartOperationWithRetry(operation
);
599 // Callback for GetContactsOperation calls.
600 void HandleContactsFeedData(google_apis::GDataErrorCode error
,
601 scoped_ptr
<base::Value
> feed_data
) {
602 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
603 if (error
!= google_apis::HTTP_SUCCESS
) {
604 LOG(WARNING
) << "Got error " << error
<< " while downloading contacts";
605 ReportFailure(HISTOGRAM_RESULT_CONTACTS_DOWNLOAD_FAILURE
);
609 VLOG(2) << "Got contacts feed data:\n"
610 << PrettyPrintValue(*(feed_data
.get()));
611 if (!ProcessContactsFeedData(*feed_data
.get())) {
612 LOG(WARNING
) << "Unable to process contacts feed data";
613 ReportFailure(HISTOGRAM_RESULT_CONTACTS_PARSE_FAILURE
);
617 StartPhotoDownloads();
618 photo_download_timer_
.Start(
619 FROM_HERE
, service_
->photo_download_timer_interval_
,
620 this, &DownloadContactsRequest::StartPhotoDownloads
);
624 // Processes the raw contacts feed from |feed_data| and fills |contacts_|.
625 // Returns true on success.
626 bool ProcessContactsFeedData(const base::Value
& feed_data
) {
627 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
628 const DictionaryValue
* toplevel_dict
= NULL
;
629 if (!feed_data
.GetAsDictionary(&toplevel_dict
)) {
630 LOG(WARNING
) << "Top-level object is not a dictionary";
634 const DictionaryValue
* feed_dict
= NULL
;
635 if (!toplevel_dict
->GetDictionary(kFeedField
, &feed_dict
)) {
636 LOG(WARNING
) << "Feed dictionary missing";
640 // Check the category field to confirm that this is actually a contact feed.
641 const ListValue
* category_list
= NULL
;
642 if (!feed_dict
->GetList(kCategoryField
, &category_list
)) {
643 LOG(WARNING
) << "Category list missing";
646 const DictionaryValue
* category_dict
= NULL
;
647 if (!category_list
->GetSize() == 1 ||
648 !category_list
->GetDictionary(0, &category_dict
)) {
649 LOG(WARNING
) << "Unable to get dictionary from category list of size "
650 << category_list
->GetSize();
653 std::string category_scheme
, category_term
;
654 if (!category_dict
->GetString(kCategorySchemeField
, &category_scheme
) ||
655 !category_dict
->GetString(kCategoryTermField
, &category_term
) ||
656 category_scheme
!= kCategorySchemeValue
||
657 category_term
!= kCategoryTermValue
) {
658 LOG(WARNING
) << "Unexpected category (scheme was \"" << category_scheme
659 << "\", term was \"" << category_term
<< "\")";
663 // A missing entry list means no entries (maybe we're doing an incremental
664 // update and nothing has changed).
665 const ListValue
* entry_list
= NULL
;
666 if (!feed_dict
->GetList(kEntryField
, &entry_list
))
669 contacts_needing_photo_downloads_
.reserve(entry_list
->GetSize());
671 for (ListValue::const_iterator entry_it
= entry_list
->begin();
672 entry_it
!= entry_list
->end(); ++entry_it
) {
673 const size_t index
= (entry_it
- entry_list
->begin());
674 const DictionaryValue
* contact_dict
= NULL
;
675 if (!(*entry_it
)->GetAsDictionary(&contact_dict
)) {
676 LOG(WARNING
) << "Entry " << index
<< " isn't a dictionary";
680 scoped_ptr
<contacts::Contact
> contact(new contacts::Contact
);
681 if (!FillContactFromDictionary(*contact_dict
, contact
.get())) {
682 LOG(WARNING
) << "Unable to fill entry " << index
;
686 VLOG(1) << "Got contact " << index
<< ":"
687 << " id=" << contact
->contact_id()
688 << " full_name=\"" << contact
->full_name() << "\""
689 << " update_time=" << contact
->update_time();
691 std::string photo_url
= GetPhotoUrl(*contact_dict
);
692 if (!photo_url
.empty()) {
693 if (!service_
->rewrite_photo_url_callback_for_testing_
.is_null()) {
695 service_
->rewrite_photo_url_callback_for_testing_
.Run(photo_url
);
697 contact_photo_urls_
[contact
.get()] = photo_url
;
698 contacts_needing_photo_downloads_
.push_back(contact
.get());
701 contacts_
->push_back(contact
.release());
707 // If we're done downloading photos, invokes a callback and deletes |this|.
708 // Otherwise, starts one or more downloads of URLs from
709 // |contacts_needing_photo_downloads_|.
710 void CheckCompletion() {
711 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
712 if (contacts_needing_photo_downloads_
.empty() &&
713 num_in_progress_photo_downloads_
== 0) {
714 VLOG(1) << "Done downloading photos; invoking callback";
715 photo_download_timer_
.Stop();
716 if (photo_download_failed_
) {
717 ReportFailure(HISTOGRAM_RESULT_PHOTO_DOWNLOAD_FAILURE
);
719 SendHistograms(HISTOGRAM_RESULT_SUCCESS
);
720 success_callback_
.Run(contacts_
.Pass());
721 service_
->OnRequestComplete(this);
727 // Starts photo downloads for contacts in |contacts_needing_photo_downloads_|.
728 // Should be invoked only once per second.
729 void StartPhotoDownloads() {
730 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
731 while (!contacts_needing_photo_downloads_
.empty() &&
732 (num_in_progress_photo_downloads_
<
733 service_
->max_photo_downloads_per_second_
)) {
734 contacts::Contact
* contact
= contacts_needing_photo_downloads_
.back();
735 contacts_needing_photo_downloads_
.pop_back();
736 DCHECK(contact_photo_urls_
.count(contact
));
737 std::string url
= contact_photo_urls_
[contact
];
739 VLOG(1) << "Starting download of photo " << url
<< " for "
740 << contact
->contact_id();
741 runner_
->StartOperationWithRetry(
742 new google_apis::GetContactPhotoOperation(
743 runner_
->operation_registry(),
745 base::Bind(&DownloadContactsRequest::HandlePhotoData
,
746 weak_ptr_factory_
.GetWeakPtr(),
748 num_in_progress_photo_downloads_
++;
752 // Callback for GetContactPhotoOperation calls. Updates the associated
753 // Contact and checks for completion.
754 void HandlePhotoData(contacts::Contact
* contact
,
755 google_apis::GDataErrorCode error
,
756 scoped_ptr
<std::string
> download_data
) {
757 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
758 VLOG(1) << "Got photo data for " << contact
->contact_id()
759 << " (error=" << error
<< " size=" << download_data
->size() << ")";
760 num_in_progress_photo_downloads_
--;
762 if (error
== google_apis::HTTP_INTERNAL_SERVER_ERROR
||
763 error
== google_apis::HTTP_SERVICE_UNAVAILABLE
) {
764 int num_errors
= ++transient_photo_download_errors_per_contact_
[contact
];
765 if (num_errors
<= kMaxTransientPhotoDownloadErrorsPerContact
) {
766 LOG(WARNING
) << "Got error " << error
<< " while downloading photo "
767 << "for " << contact
->contact_id() << "; retrying";
768 contacts_needing_photo_downloads_
.push_back(contact
);
773 if (error
== google_apis::HTTP_NOT_FOUND
) {
774 LOG(WARNING
) << "Got error " << error
<< " while downloading photo "
775 << "for " << contact
->contact_id() << "; skipping";
776 num_photo_download_404_errors_
++;
781 if (error
!= google_apis::HTTP_SUCCESS
) {
782 LOG(WARNING
) << "Got error " << error
<< " while downloading photo "
783 << "for " << contact
->contact_id() << "; giving up";
784 photo_download_failed_
= true;
785 // Make sure we don't start any more downloads.
786 contacts_needing_photo_downloads_
.clear();
791 total_photo_bytes_
+= download_data
->size();
792 contact
->set_raw_untrusted_photo(*download_data
);
796 typedef std::map
<contacts::Contact
*, std::string
> ContactPhotoUrls
;
798 GDataContactsService
* service_
; // not owned
799 google_apis::OperationRunner
* runner_
; // not owned
801 SuccessCallback success_callback_
;
802 FailureCallback failure_callback_
;
804 base::Time min_update_time_
;
806 scoped_ptr
<ScopedVector
<contacts::Contact
> > contacts_
;
808 // ID of the "My Contacts" contacts group.
809 std::string my_contacts_group_id_
;
811 // Map from a contact to the URL at which its photo is located.
812 // Contacts without photos do not appear in this map.
813 ContactPhotoUrls contact_photo_urls_
;
815 // Invokes StartPhotoDownloads() once per second.
816 base::RepeatingTimer
<DownloadContactsRequest
> photo_download_timer_
;
818 // Contacts that have photos that we still need to start downloading.
819 // When we start a download, the contact is removed from this list.
820 std::vector
<contacts::Contact
*> contacts_needing_photo_downloads_
;
822 // Number of in-progress photo downloads.
823 int num_in_progress_photo_downloads_
;
825 // Map from a contact to the number of transient errors that we've encountered
826 // while trying to download its photo. Contacts for which no errors have been
827 // encountered aren't represented in the map.
828 std::map
<contacts::Contact
*, int>
829 transient_photo_download_errors_per_contact_
;
831 // Did we encounter a fatal error while downloading a photo?
832 bool photo_download_failed_
;
834 // How many photos did we skip due to 404 errors?
835 int num_photo_download_404_errors_
;
837 // Total size of all photos that were downloaded.
838 size_t total_photo_bytes_
;
840 // Time at which Run() was called.
841 base::TimeTicks download_start_time_
;
843 // Note: This should remain the last member so it'll be destroyed and
844 // invalidate its weak pointers before any other members are destroyed.
845 base::WeakPtrFactory
<DownloadContactsRequest
> weak_ptr_factory_
;
847 DISALLOW_COPY_AND_ASSIGN(DownloadContactsRequest
);
850 GDataContactsService::GDataContactsService(Profile
* profile
)
851 : max_photo_downloads_per_second_(kMaxPhotoDownloadsPerSecond
),
852 photo_download_timer_interval_(base::TimeDelta::FromSeconds(1)) {
853 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
854 std::vector
<std::string
> scopes
;
855 scopes
.push_back(kContactsScope
);
856 runner_
.reset(new google_apis::OperationRunner(profile
, scopes
));
859 GDataContactsService::~GDataContactsService() {
860 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
861 runner_
->CancelAll();
862 STLDeleteContainerPointers(requests_
.begin(), requests_
.end());
866 google_apis::AuthService
* GDataContactsService::auth_service_for_testing() {
867 return runner_
->auth_service();
870 void GDataContactsService::Initialize() {
871 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
872 runner_
->Initialize();
875 void GDataContactsService::DownloadContacts(SuccessCallback success_callback
,
876 FailureCallback failure_callback
,
877 const base::Time
& min_update_time
) {
878 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
879 DownloadContactsRequest
* request
=
880 new DownloadContactsRequest(this,
885 VLOG(1) << "Starting contacts download with request " << request
;
886 requests_
.insert(request
);
890 void GDataContactsService::OnRequestComplete(DownloadContactsRequest
* request
) {
891 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
893 VLOG(1) << "Download request " << request
<< " complete";
894 if (!request
->my_contacts_group_id().empty())
895 cached_my_contacts_group_id_
= request
->my_contacts_group_id();
896 requests_
.erase(request
);
900 } // namespace contacts