// 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/command_line.h"
#include "base/file_util.h"
#include "base/guid.h"
#include "base/json/json_reader.h"
#include "base/json/json_writer.h"
#include "base/metrics/histogram.h"
#include "base/prefs/pref_service.h"
#include "base/utf_string_conversions.h"
#include "chrome/app/chrome_command_ids.h"
#include "chrome/browser/chrome_to_mobile_service_factory.h"
#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/sync/profile_sync_service.h"
#include "chrome/browser/sync/profile_sync_service_factory.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_command_controller.h"
#include "chrome/browser/ui/browser_finder.h"
#include "chrome/browser/ui/browser_navigator.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/common/chrome_notification_types.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/common/cloud_print/cloud_print_constants.h"
#include "chrome/common/cloud_print/cloud_print_helpers.h"
#include "chrome/common/extensions/feature_switch.h"
#include "chrome/common/pref_names.h"
#include "chrome/common/url_constants.h"
#include "components/user_prefs/pref_registry_syncable.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/notification_details.h"
#include "content/public/browser/notification_source.h"
#include "content/public/browser/web_contents.h"
#include "google/cacheinvalidation/include/types.h"
#include "google/cacheinvalidation/types.pb.h"
#include "google_apis/gaia/gaia_constants.h"
#include "google_apis/gaia/gaia_urls.h"
#include "google_apis/gaia/oauth2_access_token_fetcher.h"
#include "net/base/escape.h"
#include "net/base/load_flags.h"
#include "net/base/mime_util.h"
#include "net/http/http_status_code.h"
#include "net/url_request/url_fetcher.h"
#include "net/url_request/url_request_context_getter.h"
#include "sync/notifier/invalidation_util.h"

namespace {

enum Metric {
  DEVICES_REQUESTED = 0,  // Cloud print was contacted to list devices.
  DEVICES_AVAILABLE,      // Cloud print returned 1+ compatible devices.
  BUBBLE_SHOWN,           // The page action bubble was shown.
  SNAPSHOT_GENERATED,     // A snapshot was successfully generated.
  SNAPSHOT_ERROR,         // An error occurred during snapshot generation.
  SENDING_URL,            // Send was invoked (with or without a snapshot).
  SENDING_SNAPSHOT,       // A snapshot was sent along with the page URL.
  SEND_SUCCESS,           // Cloud print responded with success on send.
  SEND_ERROR,             // Cloud print responded with failure on send.
  LEARN_MORE_CLICKED,     // The "Learn more" help article link was clicked.
  BAD_TOKEN,              // The cloud print access token could not be minted.
  BAD_SEARCH_AUTH,        // The cloud print search request failed (auth).
  BAD_SEARCH_OTHER,       // The cloud print search request failed (other).
  BAD_SEND_407,           // The cloud print send response was errorCode==407.
                          // "Print job added but failed to notify printer..."
  BAD_SEND_ERROR,         // The cloud print send response was errorCode!=407.
  BAD_SEND_AUTH,          // The cloud print send request failed (auth).
  BAD_SEND_OTHER,         // The cloud print send request failed (other).
  SEARCH_SUCCESS,         // Cloud print responded with success on search.
  SEARCH_ERROR,           // Cloud print responded with failure on search.
  NUM_METRICS,
};

// The maximum number of retries for the URLFetcher requests.
const size_t kMaxRetries = 5;

// The number of hours to delay before retrying certain failed operations.
const size_t kDelayHours = 1;

// The sync invalidation object ID for Chrome to Mobile's mobile device list.
// This corresponds with cloud print's server-side invalidation object ID.
// Meaning: "U" == "User", "CM" == "Chrome to Mobile", "MLST" == "Mobile LiST".
const char kSyncInvalidationObjectIdChromeToMobileDeviceList[] = "UCMMLST";

// The cloud print OAuth2 scope and 'printer' type of compatible mobile devices.
const char kCloudPrintAuth[] = "https://www.googleapis.com/auth/cloudprint";
const char kTypeAndroid[] = "ANDROID_CHROME_SNAPSHOT";
const char kTypeIOS[] = "IOS_CHROME_SNAPSHOT";

// Log a metric for the "ChromeToMobile.Service" histogram.
void LogMetric(Metric metric) {
  UMA_HISTOGRAM_ENUMERATION("ChromeToMobile.Service", metric, NUM_METRICS);
}

// Get the job type string for a cloud print job submission.
std::string GetType(const ChromeToMobileService::JobData& data) {
  if (data.type == ChromeToMobileService::URL)
    return "url";
  if (data.type == ChromeToMobileService::DELAYED_SNAPSHOT)
    return "url_with_delayed_snapshot";
  DCHECK_EQ(data.type, ChromeToMobileService::SNAPSHOT);
  return "snapshot";
}

// Get the JSON string for cloud print job submissions.
std::string GetJSON(const ChromeToMobileService::JobData& data) {
  DictionaryValue json;
  switch (data.mobile_os) {
    case ChromeToMobileService::ANDROID:
      json.SetString("url", data.url.spec());
      json.SetString("type", GetType(data));
      if (data.type != ChromeToMobileService::URL)
        json.SetString("snapID", data.snapshot_id);
      break;
    case ChromeToMobileService::IOS:
      // TODO(chenyu|msw): Currently only sends an alert; include the url here?
      json.SetString("aps.alert.body", "A print job is available");
      json.SetString("aps.alert.loc-key", "IDS_CHROME_TO_DEVICE_SNAPSHOTS");
      break;
    default:
      NOTREACHED() << "Unknown mobile_os " << data.mobile_os;
      break;
  }
  std::string json_string;
  base::JSONWriter::Write(&json, &json_string);
  return json_string;
}

// Get the POST content type for a cloud print job submission.
std::string GetContentType(ChromeToMobileService::JobType type) {
  return (type == ChromeToMobileService::SNAPSHOT) ?
      "multipart/related" : "text/plain";
}

// Utility function to call net::AddMultipartValueForUpload.
void AddValue(const std::string& value_name,
              const std::string& value,
              const std::string& mime_boundary,
              std::string* post_data) {
  net::AddMultipartValueForUpload(value_name, value, mime_boundary,
                                  std::string(), post_data);
}

// Append the Chrome To Mobile client query parameter, used by cloud print.
GURL AppendClientQueryParam(const GURL& url) {
  GURL::Replacements replacements;
  std::string query("client=chrome-to-mobile");
  replacements.SetQueryStr(query);
  return url.ReplaceComponents(replacements);
}

// Get the URL for cloud print device search with the client query param.
GURL GetSearchURL(const GURL& cloud_print_url) {
  return AppendClientQueryParam(cloud_print::GetUrlForSearch(cloud_print_url));
}

// Get the URL for cloud print job submission with the client query param.
GURL GetSubmitURL(const GURL& cloud_print_url) {
  return AppendClientQueryParam(cloud_print::GetUrlForSubmit(cloud_print_url));
}

// A callback to continue snapshot generation after creating the temp file.
typedef base::Callback<void(const base::FilePath& path)>
    CreateSnapshotFileCallback;

// Create a temp file and post the callback on the UI thread with the results.
// Call this as a BlockingPoolTask to avoid the FILE thread.
void CreateSnapshotFile(CreateSnapshotFileCallback callback) {
  DCHECK(!content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
  base::FilePath file;
  if (!file_util::CreateTemporaryFile(&file))
    file.clear();
  if (!content::BrowserThread::PostTask(content::BrowserThread::UI, FROM_HERE,
                                        base::Bind(callback, file))) {
    LogMetric(SNAPSHOT_ERROR);
    NOTREACHED();
  }
}

// A callback to continue sending the snapshot data after reading the file.
typedef base::Callback<void(scoped_ptr<ChromeToMobileService::JobData> data)>
    ReadSnapshotFileCallback;

// Read the temp file and post the callback on the UI thread with the results.
// Call this as a BlockingPoolTask to avoid the FILE thread.
void ReadSnapshotFile(scoped_ptr<ChromeToMobileService::JobData> data,
                      ReadSnapshotFileCallback callback) {
  DCHECK(!content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
  if (!file_util::ReadFileToString(data->snapshot, &data->snapshot_content))
    data->snapshot_content.clear();
  if (!content::BrowserThread::PostTask(content::BrowserThread::UI, FROM_HERE,
          base::Bind(callback, base::Passed(&data)))) {
    LogMetric(SNAPSHOT_ERROR);
    NOTREACHED();
  }
}

// Delete the snapshot file; DCHECK, but really ignore the result of the delete.
// Call this as a BlockingPoolSequencedTask [after posting SubmitSnapshotFile].
void DeleteSnapshotFile(const base::FilePath& snapshot) {
  DCHECK(!content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
  bool success = file_util::Delete(snapshot, false);
  DCHECK(success);
}

// Returns true if the url can be sent via Chrome To Mobile.
bool CanSendURL(const GURL& url) {
  return url.SchemeIs(chrome::kHttpScheme) ||
         url.SchemeIs(chrome::kHttpsScheme) ||
         url.SchemeIs(chrome::kFtpScheme);
}

}  // namespace

ChromeToMobileService::Observer::Observer() {
  LogMetric(BUBBLE_SHOWN);
}

ChromeToMobileService::Observer::~Observer() {}

ChromeToMobileService::JobData::JobData() : mobile_os(ANDROID), type(URL) {}

ChromeToMobileService::JobData::~JobData() {}

// static
bool ChromeToMobileService::IsChromeToMobileEnabled() {
  // Chrome To Mobile is currently gated on the Action Box UI.
  return extensions::FeatureSwitch::action_box()->IsEnabled();
}

// static
bool ChromeToMobileService::UpdateAndGetCommandState(Browser* browser) {
  bool enabled = IsChromeToMobileEnabled();
  if (enabled) {
    const ChromeToMobileService* service =
        ChromeToMobileServiceFactory::GetForProfile(browser->profile());
    DCHECK(!browser->profile()->IsOffTheRecord() || !service);
    enabled =
        service && service->HasMobiles() &&
        CanSendURL(
            browser->tab_strip_model()->GetActiveWebContents()->GetURL());
  }
  browser->command_controller()->command_updater()->
      UpdateCommandEnabled(IDC_CHROME_TO_MOBILE_PAGE, enabled);
  return enabled;
}

// static
void ChromeToMobileService::RegisterUserPrefs(
    user_prefs::PrefRegistrySyncable* registry) {
  registry->RegisterListPref(prefs::kChromeToMobileDeviceList,
                             user_prefs::PrefRegistrySyncable::UNSYNCABLE_PREF);
}

ChromeToMobileService::ChromeToMobileService(Profile* profile)
    : weak_ptr_factory_(this),
      profile_(profile),
      sync_invalidation_enabled_(false) {
  // TODO(msw): Unit tests do not provide profiles; see http://crbug.com/122183
  ProfileSyncService* profile_sync_service =
      profile_ ? ProfileSyncServiceFactory::GetForProfile(profile_) : NULL;
  if (profile_sync_service) {
    CloudPrintURL cloud_print_url(profile_);
    cloud_print_url_ = cloud_print_url.GetCloudPrintServiceURL();
    sync_invalidation_enabled_ =
        (profile_sync_service->GetInvalidatorState() ==
         syncer::INVALIDATIONS_ENABLED);
    // Register for cloud print device list invalidation notifications.
    profile_sync_service->RegisterInvalidationHandler(this);
    syncer::ObjectIdSet ids;
    ids.insert(invalidation::ObjectId(
        ipc::invalidation::ObjectSource::CHROME_COMPONENTS,
        kSyncInvalidationObjectIdChromeToMobileDeviceList));
    profile_sync_service->UpdateRegisteredInvalidationIds(this, ids);
  }
}

ChromeToMobileService::~ChromeToMobileService() {
  while (!snapshots_.empty())
    DeleteSnapshot(*snapshots_.begin());
}

bool ChromeToMobileService::HasMobiles() const {
  const base::ListValue* mobiles = GetMobiles();
  return mobiles && !mobiles->empty();
}

const base::ListValue* ChromeToMobileService::GetMobiles() const {
  return sync_invalidation_enabled_ ?
      profile_->GetPrefs()->GetList(prefs::kChromeToMobileDeviceList) : NULL;
}

void ChromeToMobileService::GenerateSnapshot(Browser* browser,
                                             base::WeakPtr<Observer> observer) {
  DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
  // Callback SnapshotFileCreated from CreateSnapshotFile to continue.
  CreateSnapshotFileCallback callback =
      base::Bind(&ChromeToMobileService::SnapshotFileCreated,
                 weak_ptr_factory_.GetWeakPtr(), observer,
                 browser->session_id().id());
  // Create a temporary file via the blocking pool for snapshot storage.
  if (!content::BrowserThread::PostBlockingPoolTask(FROM_HERE,
          base::Bind(&CreateSnapshotFile, callback))) {
    LogMetric(SNAPSHOT_ERROR);
    NOTREACHED();
  }
}

void ChromeToMobileService::SendToMobile(const base::DictionaryValue* mobile,
                                         const base::FilePath& snapshot,
                                         Browser* browser,
                                         base::WeakPtr<Observer> observer) {
  DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
  if (access_token_.empty()) {
    // Enqueue this task to perform after obtaining an access token.
    task_queue_.push(base::Bind(&ChromeToMobileService::SendToMobile,
        weak_ptr_factory_.GetWeakPtr(), base::Owned(mobile->DeepCopy()),
        snapshot, browser, observer));
    RequestAccessToken();
    return;
  }

  scoped_ptr<JobData> data(new JobData());
  std::string mobile_os;
  if (!mobile->GetString("type", &mobile_os))
    NOTREACHED();
  data->mobile_os = (mobile_os.compare(kTypeAndroid) == 0) ? ANDROID : IOS;
  if (!mobile->GetString("id", &data->mobile_id))
    NOTREACHED();
  content::WebContents* web_contents =
      browser->tab_strip_model()->GetActiveWebContents();
  DCHECK(CanSendURL(web_contents->GetURL()));
  data->url = web_contents->GetURL();
  data->title = web_contents->GetTitle();
  data->snapshot = snapshot;
  data->snapshot_id = base::GenerateGUID();
  data->type = !snapshot.empty() ? DELAYED_SNAPSHOT : URL;
  SendJobRequest(observer, *data);

  if (data->type == DELAYED_SNAPSHOT) {
    // Callback SnapshotFileRead from ReadSnapshotFile to continue.
    ReadSnapshotFileCallback callback =
        base::Bind(&ChromeToMobileService::SnapshotFileRead,
                   weak_ptr_factory_.GetWeakPtr(), observer);
    std::string sequence_token_name = data->snapshot.AsUTF8Unsafe();
    if (!content::BrowserThread::PostBlockingPoolSequencedTask(
            sequence_token_name, FROM_HERE,
            base::Bind(&ReadSnapshotFile, base::Passed(&data), callback))) {
      LogMetric(SNAPSHOT_ERROR);
      NOTREACHED();
    }
  }
}

void ChromeToMobileService::DeleteSnapshot(const base::FilePath& snapshot) {
  DCHECK(snapshot.empty() || snapshots_.find(snapshot) != snapshots_.end());
  if (snapshots_.find(snapshot) != snapshots_.end()) {
    if (!snapshot.empty()) {
      if (!content::BrowserThread::PostBlockingPoolSequencedTask(
              snapshot.AsUTF8Unsafe(), FROM_HERE,
              base::Bind(&DeleteSnapshotFile, snapshot))) {
        LogMetric(SNAPSHOT_ERROR);
        NOTREACHED();
      }
    }
    snapshots_.erase(snapshot);
  }
}

void ChromeToMobileService::LearnMore(Browser* browser) const {
  LogMetric(LEARN_MORE_CLICKED);
  chrome::NavigateParams params(browser,
      GURL(chrome::kChromeToMobileLearnMoreURL), content::PAGE_TRANSITION_LINK);
  params.disposition = NEW_FOREGROUND_TAB;
  chrome::Navigate(&params);
}

void ChromeToMobileService::Shutdown() {
  // TODO(msw): Unit tests do not provide profiles; see http://crbug.com/122183
  // Unregister for cloud print device list invalidation notifications.
  ProfileSyncService* profile_sync_service =
      profile_ ? ProfileSyncServiceFactory::GetForProfile(profile_) : NULL;
  if (profile_sync_service)
    profile_sync_service->UnregisterInvalidationHandler(this);
}

void ChromeToMobileService::OnURLFetchComplete(const net::URLFetcher* source) {
  if (source->GetOriginalURL() == GetSearchURL(cloud_print_url_))
    HandleSearchResponse(source);
  else if (source->GetOriginalURL() == GetSubmitURL(cloud_print_url_))
    HandleSubmitResponse(source);
  else
    NOTREACHED();

  // Remove the URLFetcher from the ScopedVector; this deletes the URLFetcher.
  for (ScopedVector<net::URLFetcher>::iterator it = url_fetchers_.begin();
       it != url_fetchers_.end(); ++it) {
    if (*it == source) {
      url_fetchers_.erase(it);
      break;
    }
  }
}

void ChromeToMobileService::Observe(
    int type,
    const content::NotificationSource& source,
    const content::NotificationDetails& details) {
  DCHECK_EQ(type, chrome::NOTIFICATION_TOKEN_AVAILABLE);
  TokenService::TokenAvailableDetails* token_details =
      content::Details<TokenService::TokenAvailableDetails>(details).ptr();
  // Invalidate the cloud print access token on Gaia login token updates.
  if (token_details->service() == GaiaConstants::kGaiaOAuth2LoginRefreshToken)
    access_token_.clear();
}

void ChromeToMobileService::OnGetTokenSuccess(
    const std::string& access_token,
    const base::Time& expiration_time) {
  DCHECK(!access_token.empty());
  access_token_fetcher_.reset();
  auth_retry_timer_.Stop();
  access_token_ = access_token;

  // Post a delayed task to invalidate the access token at its expiration time.
  if (!content::BrowserThread::PostDelayedTask(
          content::BrowserThread::UI, FROM_HERE,
          base::Bind(&ChromeToMobileService::ClearAccessToken,
                     weak_ptr_factory_.GetWeakPtr()),
          expiration_time - base::Time::Now())) {
    NOTREACHED();
  }

  while (!task_queue_.empty()) {
    // Post all tasks that were queued and waiting on a valid access token.
    if (!content::BrowserThread::PostTask(content::BrowserThread::UI, FROM_HERE,
                                          task_queue_.front())) {
      NOTREACHED();
    }
    task_queue_.pop();
  }
}

void ChromeToMobileService::OnGetTokenFailure(
    const GoogleServiceAuthError& error) {
  // Log a general auth error metric for the "ChromeToMobile.Service" histogram.
  LogMetric(BAD_TOKEN);
  // Log a more detailed metric for the "ChromeToMobile.AuthError" histogram.
  UMA_HISTOGRAM_ENUMERATION("ChromeToMobile.AuthError", error.state(),
                            GoogleServiceAuthError::NUM_STATES);
  VLOG(0) << "ChromeToMobile auth failed: " << error.ToString();

  access_token_.clear();
  access_token_fetcher_.reset();
  auth_retry_timer_.Stop();

  base::TimeDelta delay = std::max(base::TimeDelta::FromHours(kDelayHours),
                                   auth_retry_timer_.GetCurrentDelay() * 2);
  auth_retry_timer_.Start(FROM_HERE, delay, this,
                          &ChromeToMobileService::RequestAccessToken);

  // Clear the mobile list, which may be (or become) out of date.
  ListValue empty;
  profile_->GetPrefs()->Set(prefs::kChromeToMobileDeviceList, empty);
}

void ChromeToMobileService::OnInvalidatorStateChange(
    syncer::InvalidatorState state) {
  sync_invalidation_enabled_ = (state == syncer::INVALIDATIONS_ENABLED);
}

void ChromeToMobileService::OnIncomingInvalidation(
    const syncer::ObjectIdInvalidationMap& invalidation_map) {
  DCHECK_EQ(1U, invalidation_map.size());
  DCHECK_EQ(1U, invalidation_map.count(invalidation::ObjectId(
      ipc::invalidation::ObjectSource::CHROME_COMPONENTS,
      kSyncInvalidationObjectIdChromeToMobileDeviceList)));
  // TODO(msw): Unit tests do not provide profiles; see http://crbug.com/122183
  ProfileSyncService* profile_sync_service =
      profile_ ? ProfileSyncServiceFactory::GetForProfile(profile_) : NULL;
  if (profile_sync_service) {
    // TODO(dcheng): Only acknowledge the invalidation once the device search
    // has finished. http://crbug.com/156843.
    profile_sync_service->AcknowledgeInvalidation(
        invalidation_map.begin()->first,
        invalidation_map.begin()->second.ack_handle);
  }
  RequestDeviceSearch();
}

const std::string& ChromeToMobileService::GetAccessTokenForTest() const {
  return access_token_;
}

void ChromeToMobileService::SetAccessTokenForTest(
    const std::string& access_token) {
  access_token_ = access_token;
}

void ChromeToMobileService::SnapshotFileCreated(
    base::WeakPtr<Observer> observer,
    SessionID::id_type browser_id,
    const base::FilePath& path) {
  DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
  // Track the set of temporary files to be deleted later.
  snapshots_.insert(path);

  // Generate the snapshot and callback SnapshotGenerated, or signal failure.
  Browser* browser = chrome::FindBrowserWithID(browser_id);
  if (!path.empty() && browser &&
      browser->tab_strip_model()->GetActiveWebContents()) {
    browser->tab_strip_model()->GetActiveWebContents()->GenerateMHTML(path,
        base::Bind(&ChromeToMobileService::SnapshotGenerated,
                   weak_ptr_factory_.GetWeakPtr(), observer));
  } else {
    SnapshotGenerated(observer, base::FilePath(), 0);
  }
}

void ChromeToMobileService::SnapshotGenerated(base::WeakPtr<Observer> observer,
                                              const base::FilePath& path,
                                              int64 bytes) {
  LogMetric(bytes > 0 ? SNAPSHOT_GENERATED : SNAPSHOT_ERROR);
  if (observer.get())
    observer->SnapshotGenerated(path, bytes);
}

void ChromeToMobileService::SnapshotFileRead(base::WeakPtr<Observer> observer,
                                             scoped_ptr<JobData> data) {
  DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
  if (data->snapshot_content.empty()) {
    LogMetric(SNAPSHOT_ERROR);
    return;
  }

  data->type = SNAPSHOT;
  SendJobRequest(observer, *data);
}

void ChromeToMobileService::InitRequest(net::URLFetcher* request) {
  DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
  request->SetRequestContext(profile_->GetRequestContext());
  request->SetMaxRetriesOn5xx(kMaxRetries);
  DCHECK(!access_token_.empty());
  request->SetExtraRequestHeaders("Authorization: OAuth " +
      access_token_ + "\r\n" + cloud_print::kChromeCloudPrintProxyHeader);
  request->SetLoadFlags(net::LOAD_DO_NOT_SEND_COOKIES |
                        net::LOAD_DO_NOT_SAVE_COOKIES);
}

void ChromeToMobileService::SendJobRequest(base::WeakPtr<Observer> observer,
                                           const JobData& data) {
  DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
  std::string post, bound;
  cloud_print::CreateMimeBoundaryForUpload(&bound);
  AddValue("printerid", UTF16ToUTF8(data.mobile_id), bound, &post);
  switch (data.mobile_os) {
    case ANDROID:
      AddValue("tag", "__c2dm__job_data=" + GetJSON(data), bound, &post);
      break;
    case IOS:
      AddValue("tag", "__snapshot_id=" + data.snapshot_id, bound, &post);
      AddValue("tag", "__snapshot_type=" + GetType(data), bound, &post);
      if (data.type == SNAPSHOT) {
        AddValue("tag", "__apns__suppress_notification", bound, &post);
      } else {
        const std::string url = data.url.spec();
        AddValue("tag", "__apns__payload=" + GetJSON(data), bound, &post);
        AddValue("tag", "__apns__original_url=" + url, bound, &post);
      }
      break;
    default:
      NOTREACHED() << "Unknown mobile_os " << data.mobile_os;
      break;
  }

  AddValue("title", UTF16ToUTF8(data.title), bound, &post);
  AddValue("contentType", GetContentType(data.type), bound, &post);

  // Add the snapshot or use dummy content to workaround a URL submission error.
  net::AddMultipartValueForUpload("content",
      data.snapshot_content.empty() ? "content" : data.snapshot_content,
      bound, "text/mhtml", &post);
  net::AddMultipartFinalDelimiterForUpload(bound, &post);

  LogMetric(data.type == SNAPSHOT ? SENDING_SNAPSHOT : SENDING_URL);
  net::URLFetcher* request = net::URLFetcher::Create(
      GetSubmitURL(cloud_print_url_), net::URLFetcher::POST, this);
  url_fetchers_.push_back(request);
  InitRequest(request);
  request_observer_map_[request] = observer;
  request->SetUploadData("multipart/form-data; boundary=" + bound, post);
  request->Start();
}

void ChromeToMobileService::ClearAccessToken() {
  access_token_.clear();
}

void ChromeToMobileService::RequestAccessToken() {
  // Register to observe Gaia login refresh token updates.
  TokenService* token_service = TokenServiceFactory::GetForProfile(profile_);
  if (registrar_.IsEmpty())
    registrar_.Add(this, chrome::NOTIFICATION_TOKEN_AVAILABLE,
                   content::Source<TokenService>(token_service));

  // Deny concurrent requests.
  if (access_token_fetcher_.get())
    return;

  // Handle invalid login refresh tokens as a failure.
  if (!token_service->HasOAuthLoginToken()) {
    OnGetTokenFailure(GoogleServiceAuthError(GoogleServiceAuthError::NONE));
    return;
  }

  auth_retry_timer_.Stop();
  access_token_fetcher_.reset(
      new OAuth2AccessTokenFetcher(this, profile_->GetRequestContext()));
  GaiaUrls* gaia_urls = GaiaUrls::GetInstance();
  access_token_fetcher_->Start(gaia_urls->oauth2_chrome_client_id(),
                               gaia_urls->oauth2_chrome_client_secret(),
                               token_service->GetOAuth2LoginRefreshToken(),
                               std::vector<std::string>(1, kCloudPrintAuth));
}

void ChromeToMobileService::RequestDeviceSearch() {
  DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
  search_retry_timer_.Stop();
  if (access_token_.empty()) {
    // Enqueue this task to perform after obtaining an access token.
    task_queue_.push(base::Bind(&ChromeToMobileService::RequestDeviceSearch,
                                weak_ptr_factory_.GetWeakPtr()));
    RequestAccessToken();
    return;
  }

  LogMetric(DEVICES_REQUESTED);

  net::URLFetcher* search_request = net::URLFetcher::Create(
      GetSearchURL(cloud_print_url_), net::URLFetcher::GET, this);
  url_fetchers_.push_back(search_request);
  InitRequest(search_request);
  search_request->Start();
}

void ChromeToMobileService::HandleSearchResponse(
    const net::URLFetcher* source) {
  DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
  DCHECK_EQ(source->GetOriginalURL(), GetSearchURL(cloud_print_url_));

  ListValue mobiles;
  std::string data;
  bool success = false;
  ListValue* list = NULL;
  DictionaryValue* dictionary = NULL;
  source->GetResponseAsString(&data);
  scoped_ptr<Value> json(base::JSONReader::Read(data));
  if (json.get() && json->GetAsDictionary(&dictionary) && dictionary) {
    dictionary->GetBoolean("success", &success);
    if (dictionary->GetList(cloud_print::kPrinterListValue, &list)) {
      std::string type, name, id;
      DictionaryValue* printer = NULL;
      DictionaryValue* mobile = NULL;
      for (size_t index = 0; index < list->GetSize(); ++index) {
        if (list->GetDictionary(index, &printer) &&
            printer->GetString("type", &type) &&
            (type.compare(kTypeAndroid) == 0 || type.compare(kTypeIOS) == 0)) {
          // Copy just the requisite values from the full |printer| definition.
          if (printer->GetString("displayName", &name) &&
              printer->GetString("id", &id)) {
            mobile = new DictionaryValue();
            mobile->SetString("type", type);
            mobile->SetString("name", name);
            mobile->SetString("id", id);
            mobiles.Append(mobile);
          } else {
            NOTREACHED();
          }
        }
      }
    }
  } else if (source->GetResponseCode() == net::HTTP_FORBIDDEN) {
    LogMetric(BAD_SEARCH_AUTH);
    // Invalidate the access token and retry a delayed search on access errors.
    access_token_.clear();
    search_retry_timer_.Stop();
    base::TimeDelta delay = std::max(base::TimeDelta::FromHours(kDelayHours),
                                     search_retry_timer_.GetCurrentDelay() * 2);
    search_retry_timer_.Start(FROM_HERE, delay, this,
                              &ChromeToMobileService::RequestDeviceSearch);
  } else {
    LogMetric(BAD_SEARCH_OTHER);
  }

  // Update the cached mobile device list in profile prefs.
  profile_->GetPrefs()->Set(prefs::kChromeToMobileDeviceList, mobiles);

  if (HasMobiles())
    LogMetric(DEVICES_AVAILABLE);
  LogMetric(success ? SEARCH_SUCCESS : SEARCH_ERROR);
  VLOG_IF(0, !success) << "ChromeToMobile search failed (" <<
                          source->GetResponseCode() << "): " << data;
}

void ChromeToMobileService::HandleSubmitResponse(
    const net::URLFetcher* source) {
  DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
  DCHECK_EQ(source->GetOriginalURL(), GetSubmitURL(cloud_print_url_));

  // Get the success value from the cloud print server response data.
  std::string data;
  bool success = false;
  source->GetResponseAsString(&data);
  DictionaryValue* dictionary = NULL;
  scoped_ptr<Value> json(base::JSONReader::Read(data));
  if (json.get() && json->GetAsDictionary(&dictionary) && dictionary) {
    dictionary->GetBoolean("success", &success);
    int error_code = -1;
    if (dictionary->GetInteger("errorCode", &error_code))
      LogMetric(error_code == 407 ? BAD_SEND_407 : BAD_SEND_ERROR);
  } else if (source->GetResponseCode() == net::HTTP_FORBIDDEN) {
    LogMetric(BAD_SEND_AUTH);
  } else {
    LogMetric(BAD_SEND_OTHER);
  }

  // Log each URL and [DELAYED_]SNAPSHOT job submission response.
  LogMetric(success ? SEND_SUCCESS : SEND_ERROR);
  VLOG_IF(0, !success) << "ChromeToMobile send failed (" <<
                          source->GetResponseCode() << "): " << data;

  // Get the observer for this job submission response.
  base::WeakPtr<Observer> observer;
  RequestObserverMap::iterator i = request_observer_map_.find(source);
  if (i != request_observer_map_.end()) {
    observer = i->second;
    request_observer_map_.erase(i);
  }

  // Check if the observer is waiting on a second response (url or snapshot).
  for (RequestObserverMap::iterator other = request_observer_map_.begin();
       observer.get() && (other != request_observer_map_.end()); ++other) {
    if (other->second == observer) {
      // Delay reporting success until the second response is received.
      if (success)
        return;

      // Report failure below and ignore the second response.
      request_observer_map_.erase(other);
      break;
    }
  }

  if (observer.get())
    observer->OnSendComplete(success);
}