OLD | NEW |
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | 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 | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 #include "chrome/browser/chrome_to_mobile_service.h" | 5 #include "chrome/browser/chrome_to_mobile_service.h" |
6 | 6 |
7 #include "base/bind.h" | 7 #include "base/bind.h" |
8 #include "base/command_line.h" | 8 #include "base/command_line.h" |
9 #include "base/file_util.h" | 9 #include "base/file_util.h" |
10 #include "base/guid.h" | 10 #include "base/guid.h" |
(...skipping 25 matching lines...) Expand all Loading... |
36 #include "content/public/browser/notification_details.h" | 36 #include "content/public/browser/notification_details.h" |
37 #include "content/public/browser/notification_source.h" | 37 #include "content/public/browser/notification_source.h" |
38 #include "content/public/browser/web_contents.h" | 38 #include "content/public/browser/web_contents.h" |
39 #include "google/cacheinvalidation/include/types.h" | 39 #include "google/cacheinvalidation/include/types.h" |
40 #include "google/cacheinvalidation/types.pb.h" | 40 #include "google/cacheinvalidation/types.pb.h" |
41 #include "google_apis/gaia/gaia_constants.h" | 41 #include "google_apis/gaia/gaia_constants.h" |
42 #include "google_apis/gaia/gaia_urls.h" | 42 #include "google_apis/gaia/gaia_urls.h" |
43 #include "google_apis/gaia/oauth2_access_token_fetcher.h" | 43 #include "google_apis/gaia/oauth2_access_token_fetcher.h" |
44 #include "net/base/escape.h" | 44 #include "net/base/escape.h" |
45 #include "net/base/load_flags.h" | 45 #include "net/base/load_flags.h" |
| 46 #include "net/http/http_status_code.h" |
46 #include "net/url_request/url_fetcher.h" | 47 #include "net/url_request/url_fetcher.h" |
47 #include "net/url_request/url_request_context_getter.h" | 48 #include "net/url_request/url_request_context_getter.h" |
48 #include "sync/notifier/invalidation_util.h" | 49 #include "sync/notifier/invalidation_util.h" |
49 | 50 |
50 namespace { | 51 namespace { |
51 | 52 |
52 // The maximum number of retries for the URLFetcher requests. | 53 // The maximum number of retries for the URLFetcher requests. |
53 const size_t kMaxRetries = 1; | 54 const size_t kMaxRetries = 5; |
54 | 55 |
55 // The number of hours to delay before retrying authentication on failure. | 56 // The number of hours to delay before retrying certain failed operations. |
56 const size_t kAuthRetryDelayHours = 6; | 57 const size_t kDelayHours = 1; |
57 | |
58 // The number of hours before subsequent search requests are allowed. | |
59 // This value is used to throttle expensive cloud print search requests. | |
60 // Note that this limitation does not hold across application restarts. | |
61 const int kSearchRequestDelayHours = 24; | |
62 | 58 |
63 // The sync invalidation object ID for Chrome to Mobile's mobile device list. | 59 // The sync invalidation object ID for Chrome to Mobile's mobile device list. |
64 // This corresponds with cloud print's server-side invalidation object ID. | 60 // This corresponds with cloud print's server-side invalidation object ID. |
65 // Meaning: "U" == "User", "CM" == "Chrome to Mobile", "MLST" == "Mobile LiST". | 61 // Meaning: "U" == "User", "CM" == "Chrome to Mobile", "MLST" == "Mobile LiST". |
66 const char kSyncInvalidationObjectIdChromeToMobileDeviceList[] = "UCMMLST"; | 62 const char kSyncInvalidationObjectIdChromeToMobileDeviceList[] = "UCMMLST"; |
67 | 63 |
68 // The cloud print OAuth2 scope and 'printer' type of compatible mobile devices. | 64 // The cloud print OAuth2 scope and 'printer' type of compatible mobile devices. |
69 const char kCloudPrintAuth[] = "https://www.googleapis.com/auth/cloudprint"; | 65 const char kCloudPrintAuth[] = "https://www.googleapis.com/auth/cloudprint"; |
70 const char kTypeAndroid[] = "ANDROID_CHROME_SNAPSHOT"; | 66 const char kTypeAndroid[] = "ANDROID_CHROME_SNAPSHOT"; |
71 const char kTypeIOS[] = "IOS_CHROME_SNAPSHOT"; | 67 const char kTypeIOS[] = "IOS_CHROME_SNAPSHOT"; |
(...skipping 233 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
305 profile_ ? ProfileSyncServiceFactory::GetForProfile(profile_) : NULL; | 301 profile_ ? ProfileSyncServiceFactory::GetForProfile(profile_) : NULL; |
306 if (profile_sync_service) | 302 if (profile_sync_service) |
307 profile_sync_service->UnregisterInvalidationHandler(this); | 303 profile_sync_service->UnregisterInvalidationHandler(this); |
308 } | 304 } |
309 | 305 |
310 void ChromeToMobileService::OnURLFetchComplete(const net::URLFetcher* source) { | 306 void ChromeToMobileService::OnURLFetchComplete(const net::URLFetcher* source) { |
311 if (source->GetURL() == GetSearchURL(cloud_print_url_)) | 307 if (source->GetURL() == GetSearchURL(cloud_print_url_)) |
312 HandleSearchResponse(source); | 308 HandleSearchResponse(source); |
313 else | 309 else |
314 HandleSubmitResponse(source); | 310 HandleSubmitResponse(source); |
| 311 |
| 312 // Remove the URLFetcher from the ScopedVector; this deletes the URLFetcher. |
| 313 for (ScopedVector<net::URLFetcher>::iterator it = url_fetchers_.begin(); |
| 314 it != url_fetchers_.end(); ++it) { |
| 315 if (*it == source) { |
| 316 url_fetchers_.erase(it); |
| 317 break; |
| 318 } |
| 319 } |
315 } | 320 } |
316 | 321 |
317 void ChromeToMobileService::Observe( | 322 void ChromeToMobileService::Observe( |
318 int type, | 323 int type, |
319 const content::NotificationSource& source, | 324 const content::NotificationSource& source, |
320 const content::NotificationDetails& details) { | 325 const content::NotificationDetails& details) { |
321 DCHECK_EQ(type, chrome::NOTIFICATION_TOKEN_AVAILABLE); | 326 DCHECK_EQ(type, chrome::NOTIFICATION_TOKEN_AVAILABLE); |
322 TokenService::TokenAvailableDetails* token_details = | 327 TokenService::TokenAvailableDetails* token_details = |
323 content::Details<TokenService::TokenAvailableDetails>(details).ptr(); | 328 content::Details<TokenService::TokenAvailableDetails>(details).ptr(); |
324 // Invalidate the cloud print access token on Gaia login token updates. | 329 // Invalidate the cloud print access token on Gaia login token updates. |
325 if (token_details->service() == GaiaConstants::kGaiaOAuth2LoginRefreshToken) | 330 if (token_details->service() == GaiaConstants::kGaiaOAuth2LoginRefreshToken || |
| 331 token_details->service() == GaiaConstants::kGaiaOAuth2LoginAccessToken) |
326 access_token_.clear(); | 332 access_token_.clear(); |
327 } | 333 } |
328 | 334 |
329 void ChromeToMobileService::OnGetTokenSuccess( | 335 void ChromeToMobileService::OnGetTokenSuccess( |
330 const std::string& access_token, | 336 const std::string& access_token, |
331 const base::Time& expiration_time) { | 337 const base::Time& expiration_time) { |
332 DCHECK(!access_token.empty()); | 338 DCHECK(!access_token.empty()); |
333 access_token_fetcher_.reset(); | 339 access_token_fetcher_.reset(); |
334 auth_retry_timer_.Stop(); | 340 auth_retry_timer_.Stop(); |
335 access_token_ = access_token; | 341 access_token_ = access_token; |
336 | 342 |
337 while (!task_queue_.empty()) { | 343 while (!task_queue_.empty()) { |
338 // Post all tasks that were queued and waiting on a valid access token. | 344 // Post all tasks that were queued and waiting on a valid access token. |
339 if (!content::BrowserThread::PostTask(content::BrowserThread::UI, FROM_HERE, | 345 if (!content::BrowserThread::PostTask(content::BrowserThread::UI, FROM_HERE, |
340 task_queue_.front())) { | 346 task_queue_.front())) { |
341 NOTREACHED(); | 347 NOTREACHED(); |
342 } | 348 } |
343 task_queue_.pop(); | 349 task_queue_.pop(); |
344 } | 350 } |
345 } | 351 } |
346 | 352 |
347 void ChromeToMobileService::OnGetTokenFailure( | 353 void ChromeToMobileService::OnGetTokenFailure( |
348 const GoogleServiceAuthError& error) { | 354 const GoogleServiceAuthError& error) { |
| 355 LogMetric(BAD_TOKEN); |
| 356 access_token_.clear(); |
349 access_token_fetcher_.reset(); | 357 access_token_fetcher_.reset(); |
350 auth_retry_timer_.Stop(); | 358 auth_retry_timer_.Stop(); |
351 | 359 |
352 auth_retry_timer_.Start( | 360 base::TimeDelta delay = std::max(base::TimeDelta::FromHours(kDelayHours), |
353 FROM_HERE, base::TimeDelta::FromHours(kAuthRetryDelayHours), | 361 auth_retry_timer_.GetCurrentDelay() * 2); |
354 this, &ChromeToMobileService::RequestAccessToken); | 362 auth_retry_timer_.Start(FROM_HERE, delay, this, |
| 363 &ChromeToMobileService::RequestAccessToken); |
| 364 |
| 365 // Clear the mobile list, which may be (or become) out of date. |
| 366 ListValue empty; |
| 367 profile_->GetPrefs()->Set(prefs::kChromeToMobileDeviceList, empty); |
| 368 UpdateCommandState(); |
355 } | 369 } |
356 | 370 |
357 void ChromeToMobileService::OnNotificationsEnabled() { | 371 void ChromeToMobileService::OnNotificationsEnabled() { |
358 sync_invalidation_enabled_ = true; | 372 sync_invalidation_enabled_ = true; |
359 UpdateCommandState(); | 373 UpdateCommandState(); |
360 } | 374 } |
361 | 375 |
362 void ChromeToMobileService::OnNotificationsDisabled( | 376 void ChromeToMobileService::OnNotificationsDisabled( |
363 syncer::NotificationsDisabledReason reason) { | 377 syncer::NotificationsDisabledReason reason) { |
364 sync_invalidation_enabled_ = false; | 378 sync_invalidation_enabled_ = false; |
(...skipping 50 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
415 } else if (observer.get()) { | 429 } else if (observer.get()) { |
416 // Signal snapshot generation failure. | 430 // Signal snapshot generation failure. |
417 observer->SnapshotGenerated(FilePath(), 0); | 431 observer->SnapshotGenerated(FilePath(), 0); |
418 } | 432 } |
419 } | 433 } |
420 | 434 |
421 net::URLFetcher* ChromeToMobileService::CreateRequest() { | 435 net::URLFetcher* ChromeToMobileService::CreateRequest() { |
422 net::URLFetcher* request = net::URLFetcher::Create( | 436 net::URLFetcher* request = net::URLFetcher::Create( |
423 cloud_print::GetUrlForSubmit(cloud_print_url_), | 437 cloud_print::GetUrlForSubmit(cloud_print_url_), |
424 net::URLFetcher::POST, this); | 438 net::URLFetcher::POST, this); |
| 439 url_fetchers_.push_back(request); |
425 InitRequest(request); | 440 InitRequest(request); |
426 return request; | 441 return request; |
427 } | 442 } |
428 | 443 |
429 void ChromeToMobileService::InitRequest(net::URLFetcher* request) { | 444 void ChromeToMobileService::InitRequest(net::URLFetcher* request) { |
430 request->SetRequestContext(profile_->GetRequestContext()); | 445 request->SetRequestContext(profile_->GetRequestContext()); |
431 request->SetMaxRetries(kMaxRetries); | 446 request->SetMaxRetries(kMaxRetries); |
432 DCHECK(!access_token_.empty()); | 447 DCHECK(!access_token_.empty()); |
433 request->SetExtraRequestHeaders("Authorization: OAuth " + | 448 request->SetExtraRequestHeaders("Authorization: OAuth " + |
434 access_token_ + "\r\n" + cloud_print::kChromeCloudPrintProxyHeader); | 449 access_token_ + "\r\n" + cloud_print::kChromeCloudPrintProxyHeader); |
(...skipping 44 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
479 request->Start(); | 494 request->Start(); |
480 } | 495 } |
481 | 496 |
482 void ChromeToMobileService::RequestAccessToken() { | 497 void ChromeToMobileService::RequestAccessToken() { |
483 // Register to observe Gaia login refresh token updates. | 498 // Register to observe Gaia login refresh token updates. |
484 TokenService* token_service = TokenServiceFactory::GetForProfile(profile_); | 499 TokenService* token_service = TokenServiceFactory::GetForProfile(profile_); |
485 if (registrar_.IsEmpty()) | 500 if (registrar_.IsEmpty()) |
486 registrar_.Add(this, chrome::NOTIFICATION_TOKEN_AVAILABLE, | 501 registrar_.Add(this, chrome::NOTIFICATION_TOKEN_AVAILABLE, |
487 content::Source<TokenService>(token_service)); | 502 content::Source<TokenService>(token_service)); |
488 | 503 |
489 // Deny concurrent requests and bail without a valid Gaia login refresh token. | 504 // Deny concurrent requests. |
490 if (access_token_fetcher_.get() || !token_service->HasOAuthLoginToken()) | 505 if (access_token_fetcher_.get()) |
491 return; | 506 return; |
492 | 507 |
| 508 // Handle invalid login refresh tokens as a failure. |
| 509 if (!token_service->HasOAuthLoginToken()) { |
| 510 OnGetTokenFailure(GoogleServiceAuthError(GoogleServiceAuthError::NONE)); |
| 511 return; |
| 512 } |
| 513 |
493 auth_retry_timer_.Stop(); | 514 auth_retry_timer_.Stop(); |
494 access_token_fetcher_.reset( | 515 access_token_fetcher_.reset( |
495 new OAuth2AccessTokenFetcher(this, profile_->GetRequestContext())); | 516 new OAuth2AccessTokenFetcher(this, profile_->GetRequestContext())); |
496 GaiaUrls* gaia_urls = GaiaUrls::GetInstance(); | 517 GaiaUrls* gaia_urls = GaiaUrls::GetInstance(); |
497 access_token_fetcher_->Start(gaia_urls->oauth2_chrome_client_id(), | 518 access_token_fetcher_->Start(gaia_urls->oauth2_chrome_client_id(), |
498 gaia_urls->oauth2_chrome_client_secret(), | 519 gaia_urls->oauth2_chrome_client_secret(), |
499 token_service->GetOAuth2LoginRefreshToken(), | 520 token_service->GetOAuth2LoginRefreshToken(), |
500 std::vector<std::string>(1, kCloudPrintAuth)); | 521 std::vector<std::string>(1, kCloudPrintAuth)); |
501 } | 522 } |
502 | 523 |
503 void ChromeToMobileService::RequestDeviceSearch() { | 524 void ChromeToMobileService::RequestDeviceSearch() { |
| 525 search_retry_timer_.Stop(); |
504 if (access_token_.empty()) { | 526 if (access_token_.empty()) { |
505 // Enqueue this task to perform after obtaining an access token. | 527 // Enqueue this task to perform after obtaining an access token. |
506 task_queue_.push(base::Bind(&ChromeToMobileService::RequestDeviceSearch, | 528 task_queue_.push(base::Bind(&ChromeToMobileService::RequestDeviceSearch, |
507 weak_ptr_factory_.GetWeakPtr())); | 529 weak_ptr_factory_.GetWeakPtr())); |
508 RequestAccessToken(); | 530 RequestAccessToken(); |
509 return; | 531 return; |
510 } | 532 } |
511 | 533 |
512 LogMetric(DEVICES_REQUESTED); | 534 LogMetric(DEVICES_REQUESTED); |
513 | 535 |
514 net::URLFetcher* search_request = net::URLFetcher::Create( | 536 net::URLFetcher* search_request = net::URLFetcher::Create( |
515 GetSearchURL(cloud_print_url_), net::URLFetcher::GET, this); | 537 GetSearchURL(cloud_print_url_), net::URLFetcher::GET, this); |
| 538 url_fetchers_.push_back(search_request); |
516 InitRequest(search_request); | 539 InitRequest(search_request); |
517 search_request->Start(); | 540 search_request->Start(); |
518 } | 541 } |
519 | 542 |
520 void ChromeToMobileService::HandleSearchResponse( | 543 void ChromeToMobileService::HandleSearchResponse( |
521 const net::URLFetcher* source) { | 544 const net::URLFetcher* source) { |
522 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); | 545 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); |
523 DCHECK_EQ(source->GetURL(), GetSearchURL(cloud_print_url_)); | 546 DCHECK_EQ(source->GetURL(), GetSearchURL(cloud_print_url_)); |
524 | 547 |
| 548 ListValue mobiles; |
525 std::string data; | 549 std::string data; |
526 ListValue* list = NULL; | 550 ListValue* list = NULL; |
527 DictionaryValue* dictionary = NULL; | 551 DictionaryValue* dictionary = NULL; |
528 source->GetResponseAsString(&data); | 552 source->GetResponseAsString(&data); |
529 scoped_ptr<Value> json(base::JSONReader::Read(data)); | 553 scoped_ptr<Value> json(base::JSONReader::Read(data)); |
530 if (json.get() && json->GetAsDictionary(&dictionary) && dictionary && | 554 if (json.get() && json->GetAsDictionary(&dictionary) && dictionary && |
531 dictionary->GetList(cloud_print::kPrinterListValue, &list)) { | 555 dictionary->GetList(cloud_print::kPrinterListValue, &list)) { |
532 ListValue mobiles; | |
533 std::string type, name, id; | 556 std::string type, name, id; |
534 DictionaryValue* printer = NULL; | 557 DictionaryValue* printer = NULL; |
535 DictionaryValue* mobile = NULL; | 558 DictionaryValue* mobile = NULL; |
536 for (size_t index = 0; index < list->GetSize(); ++index) { | 559 for (size_t index = 0; index < list->GetSize(); ++index) { |
537 if (list->GetDictionary(index, &printer) && | 560 if (list->GetDictionary(index, &printer) && |
538 printer->GetString("type", &type) && | 561 printer->GetString("type", &type) && |
539 (type.compare(kTypeAndroid) == 0 || type.compare(kTypeIOS) == 0)) { | 562 (type.compare(kTypeAndroid) == 0 || type.compare(kTypeIOS) == 0)) { |
540 // Copy just the requisite values from the full |printer| definition. | 563 // Copy just the requisite values from the full |printer| definition. |
541 if (printer->GetString("displayName", &name) && | 564 if (printer->GetString("displayName", &name) && |
542 printer->GetString("id", &id)) { | 565 printer->GetString("id", &id)) { |
543 mobile = new DictionaryValue(); | 566 mobile = new DictionaryValue(); |
544 mobile->SetString("type", type); | 567 mobile->SetString("type", type); |
545 mobile->SetString("name", name); | 568 mobile->SetString("name", name); |
546 mobile->SetString("id", id); | 569 mobile->SetString("id", id); |
547 mobiles.Append(mobile); | 570 mobiles.Append(mobile); |
548 } else { | 571 } else { |
549 NOTREACHED(); | 572 NOTREACHED(); |
550 } | 573 } |
551 } | 574 } |
552 } | 575 } |
| 576 } else if (source->GetResponseCode() == net::HTTP_FORBIDDEN) { |
| 577 LogMetric(BAD_SEARCH_AUTH); |
| 578 // Invalidate the access token and retry a delayed search on access errors. |
| 579 access_token_.clear(); |
| 580 search_retry_timer_.Stop(); |
| 581 base::TimeDelta delay = std::max(base::TimeDelta::FromHours(kDelayHours), |
| 582 search_retry_timer_.GetCurrentDelay() * 2); |
| 583 search_retry_timer_.Start(FROM_HERE, delay, this, |
| 584 &ChromeToMobileService::RequestDeviceSearch); |
| 585 } else { |
| 586 LogMetric(BAD_SEARCH_OTHER); |
| 587 } |
553 | 588 |
554 // Update the cached mobile device list in profile prefs. | 589 // Update the cached mobile device list in profile prefs. |
555 profile_->GetPrefs()->Set(prefs::kChromeToMobileDeviceList, mobiles); | 590 profile_->GetPrefs()->Set(prefs::kChromeToMobileDeviceList, mobiles); |
556 | 591 if (HasMobiles()) |
557 if (HasMobiles()) | 592 LogMetric(DEVICES_AVAILABLE); |
558 LogMetric(DEVICES_AVAILABLE); | 593 UpdateCommandState(); |
559 UpdateCommandState(); | |
560 } | |
561 } | 594 } |
562 | 595 |
563 void ChromeToMobileService::HandleSubmitResponse( | 596 void ChromeToMobileService::HandleSubmitResponse( |
564 const net::URLFetcher* source) { | 597 const net::URLFetcher* source) { |
565 // Get the success value from the cloud print server response data. | 598 // Get the success value from the cloud print server response data. |
566 std::string data; | 599 std::string data; |
| 600 bool success = false; |
567 source->GetResponseAsString(&data); | 601 source->GetResponseAsString(&data); |
568 bool success = false; | |
569 DictionaryValue* dictionary = NULL; | 602 DictionaryValue* dictionary = NULL; |
570 scoped_ptr<Value> json(base::JSONReader::Read(data)); | 603 scoped_ptr<Value> json(base::JSONReader::Read(data)); |
571 if (json.get() && json->GetAsDictionary(&dictionary) && dictionary) | 604 if (json.get() && json->GetAsDictionary(&dictionary) && dictionary) { |
572 dictionary->GetBoolean("success", &success); | 605 dictionary->GetBoolean("success", &success); |
| 606 int error_code = -1; |
| 607 if (dictionary->GetInteger("errorCode", &error_code)) |
| 608 LogMetric(error_code == 407 ? BAD_SEND_407 : BAD_SEND_ERROR); |
| 609 } else if (source->GetResponseCode() == net::HTTP_FORBIDDEN) { |
| 610 LogMetric(BAD_SEND_AUTH); |
| 611 } else { |
| 612 LogMetric(BAD_SEND_OTHER); |
| 613 } |
573 | 614 |
574 // Log each URL and [DELAYED_]SNAPSHOT job submission response. | 615 // Log each URL and [DELAYED_]SNAPSHOT job submission response. |
575 LogMetric(success ? SEND_SUCCESS : SEND_ERROR); | 616 LogMetric(success ? SEND_SUCCESS : SEND_ERROR); |
| 617 LOG_IF(INFO, !success) << "ChromeToMobile send failed (" << |
| 618 source->GetResponseCode() << "): " << data; |
576 | 619 |
577 // Get the observer for this job submission response. | 620 // Get the observer for this job submission response. |
578 base::WeakPtr<Observer> observer; | 621 base::WeakPtr<Observer> observer; |
579 RequestObserverMap::iterator i = request_observer_map_.find(source); | 622 RequestObserverMap::iterator i = request_observer_map_.find(source); |
580 if (i != request_observer_map_.end()) { | 623 if (i != request_observer_map_.end()) { |
581 observer = i->second; | 624 observer = i->second; |
582 request_observer_map_.erase(i); | 625 request_observer_map_.erase(i); |
583 } | 626 } |
584 | 627 |
585 // Check if the observer is waiting on a second response (url or snapshot). | 628 // Check if the observer is waiting on a second response (url or snapshot). |
586 for (RequestObserverMap::iterator other = request_observer_map_.begin(); | 629 for (RequestObserverMap::iterator other = request_observer_map_.begin(); |
587 observer.get() && (other != request_observer_map_.end()); ++other) { | 630 observer.get() && (other != request_observer_map_.end()); ++other) { |
588 if (other->second == observer) { | 631 if (other->second == observer) { |
589 // Delay reporting success until the second response is received. | 632 // Delay reporting success until the second response is received. |
590 if (success) | 633 if (success) |
591 return; | 634 return; |
592 | 635 |
593 // Report failure below and ignore the second response. | 636 // Report failure below and ignore the second response. |
594 request_observer_map_.erase(other); | 637 request_observer_map_.erase(other); |
595 break; | 638 break; |
596 } | 639 } |
597 } | 640 } |
598 | 641 |
599 if (observer.get()) | 642 if (observer.get()) |
600 observer->OnSendComplete(success); | 643 observer->OnSendComplete(success); |
601 } | 644 } |
OLD | NEW |