chromium/ios/chrome/browser/omaha/model/omaha_service.mm

// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "ios/chrome/browser/omaha/model/omaha_service.h"

#import <Foundation/Foundation.h>

#import <memory>
#import <utility>

#import "base/functional/bind.h"
#import "base/i18n/time_formatting.h"
#import "base/ios/device_util.h"
#import "base/location.h"
#import "base/logging.h"
#import "base/memory/raw_ptr.h"
#import "base/metrics/field_trial.h"
#import "base/no_destructor.h"
#import "base/rand_util.h"
#import "base/strings/stringprintf.h"
#import "base/strings/sys_string_conversions.h"
#import "base/strings/utf_string_conversions.h"
#import "base/system/sys_info.h"
#import "base/time/time.h"
#import "base/values.h"
#import "build/branding_buildflags.h"
#import "components/metrics/metrics_pref_names.h"
#import "components/prefs/pref_service.h"
#import "components/version_info/version_info.h"
#import "ios/chrome/app/tests_hook.h"
#import "ios/chrome/browser/browser_state_metrics/model/browser_state_metrics.h"
#import "ios/chrome/browser/shared/model/application_context/application_context.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/upgrade/model/upgrade_constants.h"
#import "ios/chrome/browser/upgrade/model/upgrade_recommended_details.h"
#import "ios/chrome/common/channel_info.h"
#import "ios/public/provider/chrome/browser/omaha/omaha_api.h"
#import "ios/public/provider/chrome/browser/raccoon/raccoon_api.h"
#import "ios/web/public/thread/web_task_traits.h"
#import "ios/web/public/thread/web_thread.h"
#import "net/base/backoff_entry.h"
#import "net/base/load_flags.h"
#import "services/network/public/cpp/resource_request.h"
#import "services/network/public/cpp/shared_url_loader_factory.h"
#import "services/network/public/cpp/simple_url_loader.h"
#import "third_party/libxml/chromium/xml_writer.h"
#import "url/gurl.h"

namespace {
// Number of hours to wait between successful requests.
const int kHoursBetweenRequests = 5;
// Minimal time to wait between retry requests.
const int kPostRetryBaseSeconds = 3600;
// Maximal time to wait between retry requests.
const int64_t kPostRetryMaxSeconds = 6 * kPostRetryBaseSeconds;

const char kCurrentArch[] = "arm64";

// 2 is used because 0 is a magic value for Time, and 1 was the pre-M29 value
// which was migrated to a specific date (crbug.com/270124).
const int64_t kUnknownInstallDate = 2;

// Default last sent application version when none has been sent yet.
const char kDefaultLastSentVersion[] = "0.0.0.0";

// Key for saving states in the UserDefaults.
NSString* const kNextTriesTimesKey = @"ChromeOmahaServiceNextTries";
NSString* const kCurrentPingKey = @"ChromeOmahaServiceCurrentPing";
NSString* const kNumberTriesKey = @"ChromeOmahaServiceNumberTries";
NSString* const kLastSentVersionKey = @"ChromeOmahaServiceLastSentVersion";
NSString* const kLastSentTimeKey = @"ChromeOmahaServiceLastSentTime";
NSString* const kRetryRequestIdKey = @"ChromeOmahaServiceRetryRequestId";
NSString* const kLastServerDateKey = @"ChromeOmahaServiceLastServerDate";

class XmlElement {
 public:
  XmlElement(XmlWriter& writer, const std::string& name)
      : writer_(&writer), name_(name) {
    const bool ok = writer_->StartElement(name_);
    DCHECK(ok);

    ios::provider::SetOmahaExtraAttributes(
        name_,
        base::BindRepeating(&XmlElement::AddAttribute, base::Unretained(this)));
  }

  ~XmlElement() {
    if (writer_) {
      const bool ok = writer_->EndElement();
      DCHECK(ok);
    }

    writer_ = nullptr;
  }

  XmlElement(const XmlElement&) = delete;
  XmlElement& operator=(const XmlElement&) = delete;

  XmlElement(XmlElement&& other) : writer_(nullptr), name_(other.name_) {
    std::swap(writer_, other.writer_);
  }

  XmlElement& operator=(XmlElement&&) = delete;

  XmlElement AddElement(const std::string& name) {
    return XmlElement(*writer_, name);
  }

  void AddAttribute(const std::string& name, const std::string& value) {
    const bool ok = writer_->AddAttribute(name, value);
    DCHECK(ok);
  }

 private:
  raw_ptr<XmlWriter> writer_ = nullptr;
  const std::string name_;
};

class XmlWrapper {
 public:
  XmlWrapper() {
    writer_.StartWriting();
    writer_.StopIndenting();
  }

  ~XmlWrapper() = default;

  XmlWrapper(const XmlWrapper&) = delete;
  XmlWrapper& operator=(const XmlWrapper&) = delete;

  XmlElement AddElement(const std::string& name) {
    return XmlElement(writer_, name);
  }

  std::string GetContentAsString() {
    writer_.StopWriting();
    return writer_.GetWrittenString();
  }

 private:
  XmlWriter writer_;
};

}  // namespace

#pragma mark -

// XML parser for the server response.
@interface ResponseParser : NSObject<NSXMLParserDelegate> {
  BOOL _hasError;
  BOOL _responseIsParsed;
  BOOL _appIsParsed;
  BOOL _updateCheckIsParsed;
  BOOL _urlIsParsed;
  BOOL _manifestIsParsed;
  BOOL _eventIsParsed;
  BOOL _dayStartIsParsed;
  NSString* _appId;
  int _serverDate;
  std::unique_ptr<UpgradeRecommendedDetails> _updateInformation;
}

// Initialization method. `appId` is the application id one expects to find in
// the response message.
- (instancetype)initWithAppId:(NSString*)appId;

// Returns YES if the message has been correctly parsed.
- (BOOL)isCorrect;

// If an upgrade is available, returns the details of the notification to send,
// and returns if Chrome is up to date.
- (UpgradeRecommendedDetails*)upgradeRecommendedDetails;

// If the response was successfully parsed, returns the date according to the
// server.
- (int)serverDate;

@end

@implementation ResponseParser

- (instancetype)initWithAppId:(NSString*)appId {
  if ((self = [super init])) {
    _appId = appId;
  }
  return self;
}

- (BOOL)isCorrect {
  // A response should have either an updatecheck ACK or an event ACK,
  // depending on the contents of the request.
  return !_hasError && (_updateCheckIsParsed || _eventIsParsed);
}

- (UpgradeRecommendedDetails*)upgradeRecommendedDetails {
  return _updateInformation.get();
}

- (int)serverDate {
  return _serverDate;
}

// This method is parsing a message with the following type:
// <response...>
//   <daystart elapsed_days="???" .../>
//   <app...>
//     <updatecheck status="ok">
//       <urls>
//         <url codebase="???"/>
//       </urls>
//       <manifest version="???">
//         <packages>
//           <package hash="0" name="Chrome" required="true" size="0"/>
//         </packages>
//         <actions>
//           <action event="update" run="Chrome"/>
//           <action event="postinstall"/>
//         </actions>
//       </manifest>
//     </updatecheck>
//     <ping.../>
//   </app>
// </response>
// --- OR ---
// <response...>
//   <daystart.../>
//   <app...>
//     <event.../>
//   </app>
// </response>
// See http://code.google.com/p/omaha/wiki/ServerProtocol for details.
- (void)parser:(NSXMLParser*)parser
    didStartElement:(NSString*)elementName
       namespaceURI:(NSString*)namespaceURI
      qualifiedName:(NSString*)qualifiedName
         attributes:(NSDictionary*)attributeDict {
  if (_hasError)
    return;

  // Array of uninteresting tags in the Omaha xml response.
  NSArray* ignoredTagNames =
      @[ @"action", @"actions", @"package", @"packages", @"ping", @"urls" ];
  if ([ignoredTagNames containsObject:elementName])
    return;

  if (!_responseIsParsed) {
    if ([elementName isEqualToString:@"response"] &&
        [[attributeDict valueForKey:@"protocol"] isEqualToString:@"3.0"] &&
        [[attributeDict valueForKey:@"server"] isEqualToString:@"prod"]) {
      _responseIsParsed = YES;
    } else {
      _hasError = YES;
    }
  } else if (!_dayStartIsParsed) {
    if ([elementName isEqualToString:@"daystart"]) {
      _dayStartIsParsed = YES;
      _serverDate = [[attributeDict valueForKey:@"elapsed_days"] integerValue];
    } else {
      _hasError = YES;
    }
  } else if (!_appIsParsed) {
    if ([elementName isEqualToString:@"app"] &&
        [[attributeDict valueForKey:@"status"] isEqualToString:@"ok"] &&
        [[attributeDict valueForKey:@"appid"] isEqualToString:_appId]) {
      _appIsParsed = YES;
    } else {
      _hasError = YES;
    }
  } else if (!_eventIsParsed && !_updateCheckIsParsed) {
    if ([elementName isEqualToString:@"updatecheck"]) {
      _updateCheckIsParsed = YES;
      NSString* status = [attributeDict valueForKey:@"status"];
      _updateInformation = std::make_unique<UpgradeRecommendedDetails>();
      if ([status isEqualToString:@"noupdate"]) {
        // No update is available on the Market, so we won't get a <url> or
        // <manifest> tag.
        _urlIsParsed = YES;
        _manifestIsParsed = YES;
        _updateInformation->is_up_to_date = true;
        [[NSUserDefaults standardUserDefaults] setBool:true
                                                forKey:kIOSChromeUpToDateKey];
      } else if ([status isEqualToString:@"ok"]) {
        _updateInformation->is_up_to_date = false;
        [[NSUserDefaults standardUserDefaults] setBool:false
                                                forKey:kIOSChromeUpToDateKey];
      } else {
        _updateInformation = nullptr;
        _hasError = YES;
      }
    } else if ([elementName isEqualToString:@"event"]) {
      if ([[attributeDict valueForKey:@"status"] isEqualToString:@"ok"]) {
        _eventIsParsed = YES;
      } else {
        _hasError = YES;
      }
    } else {
      _hasError = YES;
    }
  } else if (!_urlIsParsed) {
    if ([elementName isEqualToString:@"url"] &&
        [[attributeDict valueForKey:@"codebase"] length] > 0) {
      _urlIsParsed = YES;
      DCHECK(_updateInformation);
      NSString* url = [attributeDict valueForKey:@"codebase"];
      if ([[url substringFromIndex:([url length] - 1)] isEqualToString:@"/"])
        url = [url substringToIndex:([url length] - 1)];
      _updateInformation->upgrade_url = GURL(base::SysNSStringToUTF8(url));
      if (!_updateInformation->upgrade_url.is_valid())
        _hasError = YES;
    } else {
      _hasError = YES;
    }
  } else if (!_manifestIsParsed) {
    if ([elementName isEqualToString:@"manifest"] &&
        [attributeDict valueForKey:@"version"]) {
      _manifestIsParsed = YES;
      DCHECK(_updateInformation);
      _updateInformation->next_version =
          base::SysNSStringToUTF8([attributeDict valueForKey:@"version"]);
    } else {
      _hasError = YES;
    }
  } else {
    _hasError = YES;
  }
}

@end

// static
bool OmahaService::IsEnabled() {
#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
  return !tests_hook::DisableUpdateService();
#else
  return false;
#endif
}

// static
OmahaService* OmahaService::GetInstance() {
  // base::NoDestructor creates its OmahaService as soon as this method is
  // entered for the first time. In build variants where Omaha is disabled, that
  // can lead to a scenario where the OmahaService is started but never
  // stopped. Guard against this by ensuring that GetInstance() can only be
  // called when Omaha is enabled.
  DCHECK(IsEnabled());

  static base::NoDestructor<OmahaService> instance;
  return instance.get();
}

// static
void OmahaService::Start(std::unique_ptr<network::PendingSharedURLLoaderFactory>
                             pending_url_loader_factory,
                         const UpgradeRecommendedCallback& callback) {
  DCHECK(pending_url_loader_factory);
  DCHECK(!callback.is_null());

  if (!OmahaService::IsEnabled()) {
    return;
  }

  OmahaService* service = GetInstance();
  service->StartInternal(base::SequencedTaskRunner::GetCurrentDefault());

  if (IsOmahaServiceRefactorEnabled()) {
    base::RepeatingCallback<void(const UpgradeRecommendedDetails&)>
        wrapped_callback_that_notifies_observers = base::BindRepeating(
            [](OmahaService* service, UpgradeRecommendedCallback callback,
               const UpgradeRecommendedDetails& details) {
              // `OmahaService` is never destroyed due to `NoDestructor`,
              // ensuring the `base::Unretained(service)` reference below
              // remains valid throughout its lifetime.
              service->task_runner_->PostTask(
                  FROM_HERE,
                  base::BindOnce(&OmahaService::NotifyObservers,
                                 base::Unretained(service), details));

              callback.Run(details);
            },
            service, callback);

    service->set_upgrade_recommended_callback(
        wrapped_callback_that_notifies_observers);
  } else {
    service->set_upgrade_recommended_callback(callback);
  }

  // This should only be called once.
  DCHECK(!service->pending_url_loader_factory_ ||
         !service->url_loader_factory_);
  service->pending_url_loader_factory_ = std::move(pending_url_loader_factory);
  service->locale_lang_ = GetApplicationContext()->GetApplicationLocale();
  web::GetIOThreadTaskRunner({})->PostTask(
      FROM_HERE, base::BindOnce(&OmahaService::SendOrScheduleNextPing,
                                base::Unretained(service)));
}

// static
void OmahaService::CheckNow(OneOffCallback callback) {
  DCHECK(!callback.is_null());

  if (OmahaService::IsEnabled()) {
    OmahaService* service = GetInstance();
    DUMP_WILL_BE_CHECK(service->started_);
    // TODO(crbug.com/40070635): Remove when early callers are removed.
    if (!service->started_) {
      return;
    }

    if (IsOmahaServiceRefactorEnabled()) {
      CHECK(service->task_runner_);

      base::OnceCallback<void(UpgradeRecommendedDetails)>
          wrapped_callback_that_notifies_observers = base::BindOnce(
              [](OmahaService* service, OneOffCallback callback,
                 const UpgradeRecommendedDetails details) {
                // `OmahaService` is never destroyed due to `NoDestructor`,
                // ensuring the `base::Unretained(service)` reference below
                // remains valid throughout its lifetime.
                service->task_runner_->PostTask(
                    FROM_HERE,
                    base::BindOnce(&OmahaService::NotifyObservers,
                                   base::Unretained(service), details));

                std::move(callback).Run(details);
              },
              service, std::move(callback));

      web::GetIOThreadTaskRunner({})->PostTask(
          FROM_HERE,
          base::BindOnce(&OmahaService::CheckNowOnIOThread,
                         base::Unretained(service),
                         std::move(wrapped_callback_that_notifies_observers)));
    } else {
      web::GetIOThreadTaskRunner({})->PostTask(
          FROM_HERE,
          base::BindOnce(&OmahaService::CheckNowOnIOThread,
                         base::Unretained(service), std::move(callback)));
    }
  }
}

void OmahaService::AddObserver(OmahaServiceObserver* observer) {
  if (OmahaService::IsEnabled()) {
    GetInstance()->RegisterObserver(observer);
  }
}

void OmahaService::RemoveObserver(OmahaServiceObserver* observer) {
  if (OmahaService::IsEnabled()) {
    GetInstance()->UnregisterObserver(observer);
  }
}

void OmahaService::RegisterObserver(OmahaServiceObserver* observer) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  CHECK(IsOmahaServiceRefactorEnabled());

  observers_.AddObserver(observer);
}

void OmahaService::UnregisterObserver(OmahaServiceObserver* observer) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  CHECK(IsOmahaServiceRefactorEnabled());

  observers_.RemoveObserver(observer);
}

void OmahaService::CheckNowOnIOThread(OneOffCallback callback) {
  DCHECK_CURRENTLY_ON(web::WebThread::IO);
  DCHECK(!callback.is_null());

  DCHECK(one_off_check_callback_.is_null());
  one_off_check_callback_ = std::move(callback);

  // If there is not an ongoing ping, send one.
  if (!url_loader_) {
    SendPing();
  } else {
    // The one off ping is taking the scheduled one, so the scheduled ping is
    // now "canceled".
    scheduled_ping_canceled_ = true;
  }
}

OmahaService::OmahaService() : OmahaService(/*schedule=*/true) {}

OmahaService::OmahaService(bool schedule)
    : started_(false),
      schedule_(schedule),
      application_install_date_(0),
      sending_install_event_(false) {}

OmahaService::~OmahaService() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  for (auto& observer : observers_) {
    observer.ServiceWillShutdown(this);
  }

  DCHECK(observers_.empty());
}

void OmahaService::StartInternal(
    const scoped_refptr<base::SequencedTaskRunner> task_runner) {
  if (started_) {
    return;
  }
  started_ = true;
  task_runner_ = task_runner;

  NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
  next_tries_time_ = base::Time::FromCFAbsoluteTime(
      [defaults doubleForKey:kNextTriesTimesKey]);
  current_ping_time_ =
      base::Time::FromCFAbsoluteTime([defaults doubleForKey:kCurrentPingKey]);
  number_of_tries_ = [defaults integerForKey:kNumberTriesKey];
  last_sent_time_ =
      base::Time::FromCFAbsoluteTime([defaults doubleForKey:kLastSentTimeKey]);
  NSString* lastSentVersion = [defaults stringForKey:kLastSentVersionKey];
  if (lastSentVersion) {
    last_sent_version_ =
        base::Version(base::SysNSStringToUTF8(lastSentVersion));
  } else {
    last_sent_version_ = base::Version(kDefaultLastSentVersion);
  }
  last_server_date_ = [defaults integerForKey:kLastServerDateKey];
  if (last_server_date_ == 0) {
    // If there is no last server date, this is a first active. However, it
    // may be following a reinstall. To avoid overcounting from neutrinos,
    // transmit -2 ("unknown").
    last_server_date_ = -2;
  }

  application_install_date_ =
      GetApplicationContext()->GetLocalState()->GetInt64(
          metrics::prefs::kInstallDate);
  DCHECK(application_install_date_);

  // Whether data should be persisted again to the user preferences.
  bool persist_again = false;

  base::Time now = base::Time::Now();
  // If `last_sent_time_` is in the future, the clock has been tampered with.
  // Reset `last_sent_time_` to now.
  if (last_sent_time_ > now) {
    last_sent_time_ = now;
    persist_again = true;
  }

  // If the `next_tries_time_` is more than kHoursBetweenRequests hours away,
  // there is a possibility that the clock has been tampered with. Reschedule
  // the ping to be the usual interval after the last successful one.
  if (next_tries_time_ - now > base::Hours(kHoursBetweenRequests)) {
    next_tries_time_ = last_sent_time_ + base::Hours(kHoursBetweenRequests);
    persist_again = true;
  }

  // Fire a ping as early as possible if the version changed.
  const base::Version& current_version = version_info::GetVersion();
  if (last_sent_version_ < current_version) {
    next_tries_time_ = base::Time::Now() - base::Seconds(1);
    number_of_tries_ = 0;
    persist_again = true;
  }

  if (persist_again)
    PersistStates();
}

// static
void OmahaService::GetDebugInformation(
    base::OnceCallback<void(base::Value::Dict)> callback) {
  if (OmahaService::IsEnabled()) {
    OmahaService* service = GetInstance();
    web::GetIOThreadTaskRunner({})->PostTask(
        FROM_HERE,
        base::BindOnce(&OmahaService::GetDebugInformationOnIOThread,
                       base::Unretained(service), std::move(callback)));

  } else {
    // Invoke the callback with an empty response.
    web::GetUIThreadTaskRunner({})->PostTask(
        FROM_HERE, base::BindOnce(std::move(callback), base::Value::Dict()));
  }
}

// static
base::TimeDelta OmahaService::GetBackOff(uint8_t number_of_tries) {
  DCHECK_CURRENTLY_ON(web::WebThread::IO);
  // Configuration for the service exponential backoff
  static net::BackoffEntry::Policy kBackoffPolicy = {
      0,                             // num_errors_to_ignore
      kPostRetryBaseSeconds * 1000,  // initial_delay_ms
      2.0,                           // multiply_factor
      0.1,                           // jitter_factor
      kPostRetryMaxSeconds * 1000,   // maximum_backoff_ms
      -1,                            // entry_lifetime_ms
      false                          // always_use_initial_delay
  };

  net::BackoffEntry backoff_entry(&kBackoffPolicy);
  for (int i = 0; i < number_of_tries; ++i) {
    backoff_entry.InformOfRequest(false);
  }

  return backoff_entry.GetTimeUntilRelease();
}

std::string OmahaService::GetPingContent(const std::string& requestId,
                                         const std::string& sessionId,
                                         const std::string& versionName,
                                         const std::string& channelName,
                                         const base::Time& installationTime,
                                         PingContent pingContent) {
  DCHECK_CURRENTLY_ON(web::WebThread::IO);
  XmlWrapper xml_wrapper;

  {
    // Set up <request... />
    XmlElement request_element = xml_wrapper.AddElement("request");
    request_element.AddAttribute("protocol", "3.0");
    request_element.AddAttribute("updater", "iOS");
    request_element.AddAttribute("updaterversion", versionName);
    request_element.AddAttribute("updaterchannel", channelName);
    request_element.AddAttribute("ismachine", "1");
    request_element.AddAttribute("requestid", requestId);
    request_element.AddAttribute("sessionid", sessionId);
    request_element.AddAttribute("hardware_class",
                                 base::SysInfo::HardwareModelName());

    {
      // Set up <os platform="ios"... />
      XmlElement os_element = request_element.AddElement("os");
      os_element.AddAttribute("platform", "ios");
      os_element.AddAttribute("version",
                              base::SysInfo::OperatingSystemVersion());
      os_element.AddAttribute("arch", kCurrentArch);
    }

    const bool is_first_install =
        pingContent == INSTALL_EVENT &&
        last_sent_version_ == base::Version(kDefaultLastSentVersion);

    {
      // Set up <app version="" ...>
      XmlElement app_element = request_element.AddElement("app");
      if (pingContent == INSTALL_EVENT) {
        const std::string previous_version =
            is_first_install ? "" : last_sent_version_.GetString();
        app_element.AddAttribute("version", previous_version);
        app_element.AddAttribute("nextversion", versionName);
      } else {
        app_element.AddAttribute("version", versionName);
        app_element.AddAttribute("nextversion", "");
      }
      app_element.AddAttribute("ap", channelName);
      app_element.AddAttribute("lang", locale_lang_);
      app_element.AddAttribute("client", "");

      std::string install_age;
      if (is_first_install) {
        install_age = "-1";
      } else if (!installationTime.is_null() &&
                 installationTime.ToTimeT() != kUnknownInstallDate) {
        install_age = base::StringPrintf(
            "%d", (base::Time::Now() - installationTime).InDays());
      }

      // If the install date is unknown, send nothing.
      if (!install_age.empty())
        app_element.AddAttribute("installage", install_age);

      if (pingContent == INSTALL_EVENT) {
        // Add an install complete event.
        XmlElement event_element = app_element.AddElement("event");
        if (is_first_install) {
          event_element.AddAttribute("eventtype", "2");  // install
        } else {
          event_element.AddAttribute("eventtype", "3");  // update
        }
        event_element.AddAttribute("eventresult", "1");  // succeeded
      } else {
        // Set up <updatecheck/>
        app_element.AddElement("updatecheck");
      }

      {
        // Set up <ping ... />
        const std::string last_server_date =
            base::StringPrintf("%d", last_server_date_);

        XmlElement ping_element = app_element.AddElement("ping");
        ping_element.AddAttribute("active", "1");
        ping_element.AddAttribute("ad", last_server_date);
        ping_element.AddAttribute("rd", last_server_date);
      }
    }
  }

  return xml_wrapper.GetContentAsString();
}

std::string OmahaService::GetCurrentPingContent() {
  DCHECK_CURRENTLY_ON(web::WebThread::IO);
  const base::Version& current_version = version_info::GetVersion();
  sending_install_event_ = last_sent_version_ < current_version;
  PingContent ping_content =
      sending_install_event_ ? INSTALL_EVENT : USAGE_PING;

  // An install retry ping only makes sense if an install event must be send.
  DCHECK(sending_install_event_ || !IsNextPingInstallRetry());
  std::string request_id = GetNextPingRequestId(ping_content);
  return GetPingContent(
      request_id, ios::device_util::GetRandomId(),
      std::string(version_info::GetVersionNumber()), GetChannelString(),
      base::Time::FromTimeT(application_install_date_), ping_content);
}

void OmahaService::NotifyObservers(UpgradeRecommendedDetails details) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  CHECK(IsOmahaServiceRefactorEnabled());

  for (auto& observer : observers_) {
    observer.UpgradeRecommendedDetailsChanged(details);
  }
}

void OmahaService::SendPing() {
  DCHECK_CURRENTLY_ON(web::WebThread::IO);
  // If a scheduled ping comes during a one off, drop it.
  if (url_loader_ && !one_off_check_callback_.is_null()) {
    scheduled_ping_canceled_ = true;
    return;
  }

  // Check that no request is in progress.
  DCHECK(!url_loader_);

  const GURL url = ios::provider::GetOmahaUpdateServerURL();
  if (!url.is_valid()) {
    return;
  }

  // There are 2 situations here:
  // 1) production code, where `pending_url_loader_factory_` is used.
  // 2) testing code, where the `url_loader_factory_` creation is triggered by
  // the test.
  if (pending_url_loader_factory_) {
    DCHECK(!url_loader_factory_);
    url_loader_factory_ = network::SharedURLLoaderFactory::Create(
        std::move(pending_url_loader_factory_));
    DCHECK(url_loader_factory_);
  } else {
    CHECK(url_loader_factory_);
  }

  auto resource_request = std::make_unique<network::ResourceRequest>();
  resource_request->url = url;
  resource_request->method = "POST";
  resource_request->credentials_mode = network::mojom::CredentialsMode::kOmit;

  // If this is not the first try, notify the omaha server.
  if (number_of_tries_ && IsNextPingInstallRetry()) {
    resource_request->headers.SetHeader(
        "X-RequestAge",
        base::StringPrintf(
            "%lld", (base::Time::Now() - current_ping_time_).InSeconds()));
  }

  // Update last fail time and number of tries, so that if anything fails
  // catastrophically, the fail is taken into account.
  if (number_of_tries_ < 30)
    ++number_of_tries_;
  next_tries_time_ = base::Time::Now() + GetBackOff(number_of_tries_);
  PersistStates();

  url_loader_ = network::SimpleURLLoader::Create(std::move(resource_request),
                                                 NO_TRAFFIC_ANNOTATION_YET);
  url_loader_->AttachStringForUpload(GetCurrentPingContent(), "text/xml");
  url_loader_->DownloadToStringOfUnboundedSizeUntilCrashAndDie(
      url_loader_factory_.get(),
      base::BindOnce(&OmahaService::OnURLLoadComplete, base::Unretained(this)));
}

void OmahaService::SendOrScheduleNextPing() {
  DCHECK_CURRENTLY_ON(web::WebThread::IO);
  base::Time now = base::Time::Now();
  if (next_tries_time_ <= now) {
    SendPing();
    return;
  }
  if (schedule_) {
    timer_.Start(
        FROM_HERE, next_tries_time_ - now,
        base::BindOnce(&OmahaService::SendPing, base::Unretained(this)));
  }
}

void OmahaService::PersistStates() {
  // As a workaround to crbug.com/1247282, dispatch back to the main thread.
  dispatch_async(dispatch_get_main_queue(), ^{
    NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];

    [defaults setDouble:next_tries_time_.ToCFAbsoluteTime()
                 forKey:kNextTriesTimesKey];
    [defaults setDouble:current_ping_time_.ToCFAbsoluteTime()
                 forKey:kCurrentPingKey];
    [defaults setDouble:last_sent_time_.ToCFAbsoluteTime()
                 forKey:kLastSentTimeKey];
    [defaults setInteger:number_of_tries_ forKey:kNumberTriesKey];
    [defaults setObject:base::SysUTF8ToNSString(last_sent_version_.GetString())
                 forKey:kLastSentVersionKey];
    [defaults setInteger:last_server_date_ forKey:kLastServerDateKey];

    // Save critical state information for usage reporting.
    [defaults synchronize];
  });
}

void OmahaService::OnURLLoadComplete(
    std::unique_ptr<std::string> response_body) {
  DCHECK_CURRENTLY_ON(web::WebThread::IO);
  // Reset the loader.
  url_loader_.reset();

  if (!response_body) {
    DLOG(WARNING) << "Error contacting the Omaha server";
    SendOrScheduleNextPing();
    return;
  }

  NSData* xml = [NSData dataWithBytes:response_body->data()
                               length:response_body->length()];
  NSXMLParser* parser = [[NSXMLParser alloc] initWithData:xml];
  const std::string application_id = ios::provider::GetOmahaApplicationId();
  ResponseParser* delegate = [[ResponseParser alloc]
      initWithAppId:base::SysUTF8ToNSString(application_id)];
  parser.delegate = delegate;

  if (![parser parse] || ![delegate isCorrect]) {
    DLOG(ERROR) << "Unable to parse XML response from Omaha server.";
    SendOrScheduleNextPing();
    return;
  }
  // Handle success.
  number_of_tries_ = 0;
  // Schedule the next request. If requset that just finished was an install
  // notification, send an active ping immediately.
  next_tries_time_ =
      sending_install_event_
          ? base::Time::Now()
          : base::Time::Now() + base::Hours(kHoursBetweenRequests);
  current_ping_time_ = next_tries_time_;
  last_sent_time_ = base::Time::Now();
  last_sent_version_ = version_info::GetVersion();
  sending_install_event_ = false;
  last_server_date_ = [delegate serverDate];
  ClearInstallRetryRequestId();
  PersistStates();
  bool need_to_schedule_ping = true;

  // Send notification for updates if needed.
  UpgradeRecommendedDetails* details = [delegate upgradeRecommendedDetails];
  if (details) {
    // Use the correct callback based on if a one-off check is ongoing.
    if (!one_off_check_callback_.is_null()) {
      web::GetUIThreadTaskRunner({})->PostTask(
          FROM_HERE,
          base::BindOnce(std::move(one_off_check_callback_), *details));
      // Do not schedule another ping for one-off checks, unless
      // it canceled a scheduled ping.
      need_to_schedule_ping = scheduled_ping_canceled_;
      scheduled_ping_canceled_ = false;
    } else if (!details->is_up_to_date) {
      web::GetUIThreadTaskRunner({})->PostTask(
          FROM_HERE, base::BindOnce(upgrade_recommended_callback_, *details));
    }
  }

  // Schedule next ping if necessary.
  if (need_to_schedule_ping) {
    SendOrScheduleNextPing();
  }
}

void OmahaService::GetDebugInformationOnIOThread(
    base::OnceCallback<void(base::Value::Dict)> callback) {
  DCHECK_CURRENTLY_ON(web::WebThread::IO);
  base::Value::Dict result;

  result.Set("message", GetCurrentPingContent());
  result.Set("last_sent_time",
             base::TimeFormatShortDateAndTime(last_sent_time_));
  result.Set("next_tries_time",
             base::TimeFormatShortDateAndTime(next_tries_time_));
  result.Set("current_ping_time",
             base::TimeFormatShortDateAndTime(current_ping_time_));
  result.Set("last_sent_version", last_sent_version_.GetString());
  result.Set("number_of_tries", base::StringPrintf("%d", number_of_tries_));
  result.Set("timer_running", base::StringPrintf("%d", timer_.IsRunning()));
  result.Set("timer_current_delay",
             base::StringPrintf("%llds", timer_.GetCurrentDelay().InSeconds()));
  result.Set("timer_desired_run_time",
             base::TimeFormatShortDateAndTime(
                 base::Time::Now() +
                 (timer_.desired_run_time() - base::TimeTicks::Now())));

  // Sending the value to the callback.
  web::GetUIThreadTaskRunner({})->PostTask(
      FROM_HERE, base::BindOnce(std::move(callback), std::move(result)));
}

bool OmahaService::IsNextPingInstallRetry() {
  DCHECK_CURRENTLY_ON(web::WebThread::IO);
  return [[NSUserDefaults standardUserDefaults]
             stringForKey:kRetryRequestIdKey] != nil;
}

std::string OmahaService::GetNextPingRequestId(PingContent ping_content) {
  DCHECK_CURRENTLY_ON(web::WebThread::IO);
  NSString* stored_id =
      [[NSUserDefaults standardUserDefaults] stringForKey:kRetryRequestIdKey];
  if (stored_id) {
    DCHECK(ping_content == INSTALL_EVENT);
    return base::SysNSStringToUTF8(stored_id);
  } else {
    std::string identifier = ios::device_util::GetRandomId();
    if (ping_content == INSTALL_EVENT)
      OmahaService::SetInstallRetryRequestId(identifier);
    return identifier;
  }
}

void OmahaService::SetInstallRetryRequestId(const std::string& request_id) {
  DCHECK_CURRENTLY_ON(web::WebThread::IO);
  NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
  [defaults setObject:base::SysUTF8ToNSString(request_id)
               forKey:kRetryRequestIdKey];
  // Save critical state information for usage reporting.
  [defaults synchronize];
}

void OmahaService::ClearInstallRetryRequestId() {
  DCHECK_CURRENTLY_ON(web::WebThread::IO);
  NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
  [defaults removeObjectForKey:kRetryRequestIdKey];
  // Clear critical state information for usage reporting.
  [defaults synchronize];
}

void OmahaService::InitializeURLLoaderFactory(
    scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory) {
  DCHECK_CURRENTLY_ON(web::WebThread::IO);
  url_loader_factory_ = url_loader_factory;
}

void OmahaService::ClearPersistentStateForTests() {
  DCHECK_CURRENTLY_ON(web::WebThread::IO);
  NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
  [defaults removeObjectForKey:kNextTriesTimesKey];
  [defaults removeObjectForKey:kCurrentPingKey];
  [defaults removeObjectForKey:kNumberTriesKey];
  [defaults removeObjectForKey:kLastSentVersionKey];
  [defaults removeObjectForKey:kLastSentTimeKey];
  [defaults removeObjectForKey:kRetryRequestIdKey];
  [defaults removeObjectForKey:kLastServerDateKey];
  [defaults removeObjectForKey:kIOSChromeUpToDateKey];
}