Chromium Code Reviews| Index: chrome/browser/chrome_to_mobile_service.cc |
| diff --git a/chrome/browser/chrome_to_mobile_service.cc b/chrome/browser/chrome_to_mobile_service.cc |
| new file mode 100755 |
| index 0000000000000000000000000000000000000000..0e8d078072005ec37089a507432f171004331211 |
| --- /dev/null |
| +++ b/chrome/browser/chrome_to_mobile_service.cc |
| @@ -0,0 +1,330 @@ |
| +// 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/chrome_to_mobile_service.h" |
| + |
| +#include "base/bind.h" |
| +#include "base/json/json_writer.h" |
| +#include "base/stringprintf.h" |
| +#include "base/utf_string_conversions.h" |
| +#include "base/values.h" |
| +#include "chrome/app/chrome_command_ids.h" |
| +#include "chrome/common/cloud_print/cloud_print_helpers.h" |
|
Scott Byer
2012/03/12 23:26:23
Put in alphabetical order.
msw
2012/03/13 02:39:14
Done.
|
| +#include "chrome/browser/printing/cloud_print/cloud_print_url.h" |
| +#include "chrome/browser/profiles/profile.h" |
| +#include "chrome/browser/signin/token_service.h" |
| +#include "chrome/browser/signin/token_service_factory.h" |
| +#include "chrome/browser/ui/browser.h" |
| +#include "chrome/browser/ui/browser_list.h" |
| +#include "chrome/common/guid.h" |
| +#include "chrome/common/net/gaia/gaia_urls.h" |
| +#include "chrome/common/net/gaia/oauth2_access_token_fetcher.h" |
| +#include "content/public/browser/browser_thread.h" |
| +#include "content/public/browser/web_contents.h" |
| +#include "content/public/common/url_fetcher.h" |
| +#include "net/base/escape.h" |
| +#include "net/base/load_flags.h" |
| +#include "net/url_request/url_request_context_getter.h" |
| + |
| +namespace { |
| + |
| +// The maximum number of retries for the URLFetcher requests. |
| +size_t kMaxRetries = 10; |
| + |
| +// Seconds between automatically retrying authentication (on failure). |
| +int kAuthRetryDelay = 30; |
| + |
| +// Seconds between automatically updating the mobile device list. |
| +int kAutoSearchRetryDelay = 300; |
| + |
| +// The cloud print Oath2 scope and 'printer' type of compatible mobile devices. |
| +const char kOAuth2Scope[] = "https://www.googleapis.com/auth/cloudprint"; |
| +const char kTypeAndroidChromeSnapshot[] = "ANDROID_CHROME_SNAPSHOT"; |
| + |
| +// The types of Chrome To Mobile requests sent to the cloud print service. |
| +const char kRequestTypeURL[] = "url"; |
| +const char kRequestTypeDelayedSnapshot[] = "url_with_delayed_snapshot"; |
| +const char kRequestTypeSnapshot[] = "snapshot"; |
| + |
| +// The snapshot path constants; used with a guid for each MHTML snapshot file. |
| +const FilePath::CharType kSnapshotPath[] = |
| + FILE_PATH_LITERAL("chrome_to_mobile_snapshot_.mht"); |
| + |
| +// Get the "__c2dm__job_data" tag JSON data for the cloud print job submission. |
| +std::string GetJobString(const ChromeToMobileService::RequestData& data) { |
| + scoped_ptr<DictionaryValue> job(new DictionaryValue()); |
| + job->SetString("url", data.url.spec()); |
| + if (data.type == ChromeToMobileService::URL) { |
| + job->SetString("type", kRequestTypeURL); |
| + } else { |
| + job->SetString("snapID", data.snapshot_id); |
| + job->SetString("type", (data.type == ChromeToMobileService::SNAPSHOT) ? |
| + kRequestTypeSnapshot : kRequestTypeDelayedSnapshot); |
| + } |
| + std::string job_string; |
| + base::JSONWriter::Write(job.get(), false, &job_string); |
| + return job_string; |
| +} |
| + |
| +// Get the URL for cloud print job submission; appends query params if needed. |
| +GURL GetSubmitURL(GURL service_url, |
| + const ChromeToMobileService::RequestData& data) { |
| + GURL submit_url = cloud_print::GetUrlForSubmit(service_url); |
| + if (data.type == ChromeToMobileService::SNAPSHOT) |
| + return submit_url; |
| + |
| + // Append form data to the URL's query for |URL| and |DELAYED_SNAPSHOT| jobs. |
| + static const bool kUsePlus = true; |
| + std::string tag_string = net::EscapeQueryParamValue( |
| + "__c2dm__job_data=" + GetJobString(data), kUsePlus); |
| + GURL::Replacements replacements; |
| + // Provide dummy content to workaround |errorCode| 412 'Document missing'. |
| + std::string query = StringPrintf("printerid=%s&tag=%s&title=%s" |
| + "&contentType=text/plain&content=dummy", |
| + net::EscapeQueryParamValue(UTF16ToUTF8(data.mobile_id), kUsePlus).c_str(), |
| + net::EscapeQueryParamValue(tag_string, kUsePlus).c_str(), |
| + net::EscapeQueryParamValue(UTF16ToUTF8(data.title), kUsePlus).c_str()); |
| + replacements.SetQueryStr(query); |
| + return submit_url.ReplaceComponents(replacements); |
| +} |
| + |
| +// Delete the specified file; called as a BlockingPoolTask. |
| +void DeleteFilePath(const FilePath& file_path) { |
| + bool success = file_util::Delete(file_path, false); |
| + DCHECK(success); |
| +} |
| + |
| +// Construct POST data and submit the MHTML snapshot file; deletes the snapshot. |
| +void SubmitSnapshot(content::URLFetcher* request, |
| + const ChromeToMobileService::RequestData& data) { |
| + std::string file; |
| + if (file_util::ReadFileToString(data.snapshot_path, &file) && !file.empty()) { |
| + std::string post_data, mime_boundary; |
| + cloud_print::CreateMimeBoundaryForUpload(&mime_boundary); |
| + cloud_print::AddMultipartValueForUpload("printerid", |
| + UTF16ToUTF8(data.mobile_id), mime_boundary, std::string(), &post_data); |
| + cloud_print::AddMultipartValueForUpload("tag", "__c2dm__job_data=" + |
| + GetJobString(data), mime_boundary, std::string(), &post_data); |
| + cloud_print::AddMultipartValueForUpload("title", UTF16ToUTF8(data.title), |
| + mime_boundary, std::string(), &post_data); |
| + cloud_print::AddMultipartValueForUpload("contentType", "multipart/related", |
| + mime_boundary, std::string(), &post_data); |
| + |
| + // Append the snapshot MHTML content and terminate the request body. |
| + post_data.append("--" + mime_boundary + "\r\n" |
| + "Content-Disposition: form-data; " |
| + "name=\"content\"; filename=\"blob\"\r\n" |
| + "Content-Type: text/mhtml\r\n" |
| + "\r\n" + file + "\r\n" "--" + mime_boundary + "--\r\n"); |
| + std::string content_type = "multipart/form-data; boundary=" + mime_boundary; |
| + request->SetUploadData(content_type, post_data); |
| + request->Start(); |
| + } |
| + |
| + content::BrowserThread::PostBlockingPoolTask(FROM_HERE, |
| + base::Bind(&DeleteFilePath, data.snapshot_path)); |
| +} |
| + |
| +} // namespace |
| + |
| +ChromeToMobileService::RequestData::RequestData() {} |
| + |
| +ChromeToMobileService::RequestData::~RequestData() {} |
| + |
| +ChromeToMobileService::ChromeToMobileService(Profile* profile) |
| + : profile_(profile), |
| + cloud_print_url_(new CloudPrintURL(profile)), |
| + oauth2_retry_count_(0) { |
| + content::BrowserThread::PostBlockingPoolTask(FROM_HERE, |
| + base::Bind(&ChromeToMobileService::CreateUniqueTempDir, |
| + base::Unretained(this))); |
| + RequestMobileListUpdate(); |
| +} |
| + |
| +ChromeToMobileService::~ChromeToMobileService() {} |
| + |
| +void ChromeToMobileService::OnURLFetchComplete( |
| + const content::URLFetcher* source) { |
| + if (source == search_request_.get()) |
| + HandleSearchResponse(); |
| + else |
| + HandleSubmitResponse(source); |
| +} |
| + |
| +void ChromeToMobileService::OnGetTokenSuccess( |
| + const std::string& access_token) { |
| + oauth2_request_.reset(); |
| + request_timer_.Stop(); |
| + oauth2_token_ = access_token; |
| + RequestSearch(); |
| +} |
| + |
| +void ChromeToMobileService::OnGetTokenFailure( |
| + const GoogleServiceAuthError& error) { |
| + oauth2_request_.reset(); |
| + if (request_timer_.IsRunning() || (oauth2_retry_count_++ > kMaxRetries)) |
| + return; |
| + request_timer_.Start(FROM_HERE, base::TimeDelta::FromSeconds(kAuthRetryDelay), |
| + this, &ChromeToMobileService::RequestAuth); |
| +} |
| + |
| +void ChromeToMobileService::RequestMobileListUpdate() { |
| + if (oauth2_token_.empty()) |
| + RequestAuth(); |
| + else |
| + RequestSearch(); |
| +} |
| + |
| +void ChromeToMobileService::GenerateSnapshot(base::WeakPtr<Observer> observer) { |
| + FilePath path(temp_dir_.path().Append(kSnapshotPath)); |
| + BrowserList::GetLastActiveWithProfile(profile_)->GetSelectedWebContents()-> |
| + GenerateMHTML(path.InsertBeforeExtensionASCII(guid::GenerateGUID()), |
| + base::Bind(&Observer::SnapshotGenerated, observer)); |
| +} |
| + |
| +void ChromeToMobileService::SendToMobile(const string16& mobile_id, |
| + const FilePath& snapshot, |
| + base::WeakPtr<Observer> observer) { |
| + DCHECK(!oauth2_token_.empty()); |
| + RequestData data; |
| + data.mobile_id = mobile_id; |
| + content::WebContents* web_contents = |
| + BrowserList::GetLastActiveWithProfile(profile_)->GetSelectedWebContents(); |
| + data.url = web_contents->GetURL(); |
| + data.title = web_contents->GetTitle(); |
| + data.snapshot_path = snapshot; |
| + bool send_snapshot = !snapshot.empty(); |
| + data.snapshot_id = send_snapshot ? guid::GenerateGUID() : std::string(); |
| + data.type = send_snapshot ? DELAYED_SNAPSHOT : URL; |
| + |
| + content::URLFetcher* submit_url = CreateRequest(data); |
|
nyquist
2012/03/13 00:52:23
Nit: This naming seems odd to me. submit_request?
msw
2012/03/13 02:39:14
Talked offline: submit_url corresponds with submit
|
| + request_observer_map_[submit_url] = observer; |
| + submit_url->Start(); |
| + |
| + if (send_snapshot) { |
| + data.type = SNAPSHOT; |
| + content::URLFetcher* submit_snapshot = CreateRequest(data); |
| + request_observer_map_[submit_snapshot] = observer; |
| + content::BrowserThread::PostBlockingPoolTask(FROM_HERE, |
| + base::Bind(&SubmitSnapshot, submit_snapshot, data)); |
| + } |
| +} |
| + |
| +void ChromeToMobileService::CreateUniqueTempDir() { |
| + bool success = temp_dir_.CreateUniqueTempDir(); |
| + DCHECK(success); |
| +} |
| + |
| +content::URLFetcher* ChromeToMobileService::CreateRequest( |
| + const RequestData& data) { |
| + bool get = data.type != SNAPSHOT; |
| + GURL service_url(cloud_print_url_->GetCloudPrintServiceURL()); |
| + content::URLFetcher* request = content::URLFetcher::Create( |
| + data.type == SEARCH ? cloud_print::GetUrlForSearch(service_url) : |
| + GetSubmitURL(service_url, data), |
| + get ? content::URLFetcher::GET : content::URLFetcher::POST, this); |
| + request->SetRequestContext(profile_->GetRequestContext()); |
| + request->SetMaxRetries(kMaxRetries); |
| + request->SetExtraRequestHeaders("Authorization: OAuth " + |
| + oauth2_token_ + "\r\n" + cloud_print::kChromeCloudPrintProxyHeader); |
| + request->SetLoadFlags(net::LOAD_DO_NOT_SEND_COOKIES | |
| + net::LOAD_DO_NOT_SAVE_COOKIES); |
| + return request; |
| +} |
| + |
| +void ChromeToMobileService::RequestAuth() { |
| + DCHECK(oauth2_token_.empty()); |
| + if (oauth2_request_.get()) |
| + return; |
| + |
| + GURL auth_url = cloud_print::GetUrlForGetAuthCode( |
|
nyquist
2012/03/13 00:52:23
Dead store. This doesn't seem to be used anywhere.
msw
2012/03/13 02:39:14
Awesome catch! This let me revert some CloudPrint
|
| + cloud_print_url_->GetCloudPrintServiceURL(), |
| + cloud_print::kDefaultCloudPrintOAuthClientId, |
| + cloud_print::GenerateProxyId()); |
| + oauth2_request_.reset( |
| + new OAuth2AccessTokenFetcher(this, profile_->GetRequestContext())); |
| + std::string token = TokenServiceFactory::GetForProfile(profile_)-> |
| + GetOAuth2LoginRefreshToken(); |
| + std::vector<std::string> scopes(1, kOAuth2Scope); |
| + oauth2_request_->Start(GaiaUrls::GetInstance()->oauth2_chrome_client_id(), |
| + GaiaUrls::GetInstance()->oauth2_chrome_client_secret(), token, scopes); |
| +} |
| + |
| +void ChromeToMobileService::RequestSearch() { |
| + DCHECK(!oauth2_token_.empty()); |
| + if (search_request_.get()) |
| + return; |
| + |
| + RequestData data; |
| + data.type = SEARCH; |
| + search_request_.reset(CreateRequest(data)); |
| + search_request_->Start(); |
| +} |
| + |
| +void ChromeToMobileService::HandleSearchResponse() { |
| + std::string data; |
| + search_request_->GetResponseAsString(&data); |
| + search_request_.reset(); |
| + |
| + DictionaryValue* json_data = NULL; |
| + ListValue* list = NULL; |
| + cloud_print::ParseResponseJSON(data, NULL, &json_data); |
| + if (json_data && json_data->GetList(cloud_print::kPrinterListValue, &list)) { |
| + mobiles_.clear(); |
|
nyquist
2012/03/13 00:52:23
Updating this list directly means that clients mig
msw
2012/03/13 02:39:14
I'll gladly take additional thread safety advice,
|
| + for (size_t index = 0; index < list->GetSize(); index++) { |
| + DictionaryValue* mobile_data = NULL; |
| + if (list->GetDictionary(index, &mobile_data)) { |
| + std::string mobile_type; |
| + mobile_data->GetString("type", &mobile_type); |
| + if (mobile_type.compare(kTypeAndroidChromeSnapshot) == 0) |
| + mobiles_.push_back(mobile_data); |
| + } |
| + } |
| + } |
| + |
| + BrowserList::GetLastActiveWithProfile(profile_)->command_updater()-> |
| + UpdateCommandEnabled(IDC_CHROME_TO_MOBILE_PAGE, !mobiles_.empty()); |
| + |
| + if (!request_timer_.IsRunning()) |
| + request_timer_.Start(FROM_HERE, |
| + base::TimeDelta::FromSeconds(kAutoSearchRetryDelay), |
| + this, &ChromeToMobileService::RequestSearch); |
| +} |
| + |
| +void ChromeToMobileService::HandleSubmitResponse( |
| + const content::URLFetcher* source) { |
| + // Get the observer for this response; bail if there is none or it is NULL. |
| + RequestObserverMap::iterator i = request_observer_map_.find(source); |
| + if (i == request_observer_map_.end()) |
| + return; |
| + base::WeakPtr<Observer> observer = i->second; |
| + request_observer_map_.erase(i); |
| + if (!observer.get()) |
| + return; |
| + |
| + // Get the success value from the CloudPrint server response data. |
| + std::string data; |
| + source->GetResponseAsString(&data); |
| + DictionaryValue* json_data = NULL; |
| + cloud_print::ParseResponseJSON(data, NULL, &json_data); |
| + bool success = false; |
| + if (json_data) |
| + json_data->GetBoolean("success", &success); |
| + |
| + // Check if the observer is waiting on a second response (url and snapshot). |
| + RequestObserverMap::iterator other = request_observer_map_.begin(); |
| + for (; other != request_observer_map_.end(); ++other) { |
| + if (other->second == observer) { |
| + // Do not call OnSendComplete for observers waiting on a second response. |
| + if (success) |
| + return; |
| + |
| + // Ensure a second response is not sent after reporting failure below. |
| + request_observer_map_.erase(other); |
| + break; |
| + } |
| + } |
| + |
| + observer->OnSendComplete(success); |
| +} |