chromium/ash/system/focus_mode/sounds/soundscape/soundscapes_downloader.cc

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

#include "ash/system/focus_mode/sounds/soundscape/soundscapes_downloader.h"

#include "ash/shell.h"
#include "ash/shell_delegate.h"
#include "ash/system/focus_mode/sounds/soundscape/soundscape_types.h"
#include "base/functional/callback.h"
#include "base/memory/weak_ptr.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/task_runner.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "services/network/public/cpp/simple_url_loader.h"

namespace ash {

namespace {

constexpr size_t kMaxDownloadBytes = 20 * 1024;

// Max number of retries for fetching the configuration.
constexpr int kMaxRetries = 3;

SoundscapesDownloader::Urls ProductionConfiguration(const std::string& locale) {
  SoundscapesDownloader::Urls configuration;
  configuration.locale = locale;
  configuration.host = GURL("https://gstatic.com/chromeos-focusmode/");
  configuration.config_path = "config.json";
  return configuration;
}

constexpr net::NetworkTrafficAnnotationTag kFocusSoundsNetworkTag =
    net::DefineNetworkTrafficAnnotation("focus_sounds_configuration", R"(
        semantics {
          sender: "Focus Sounds"
          description:
            "Retrieve the list of playlists, songs, and thumbnails for Focus"
            "Sounds in Focus Mode. Songs may be played while a user is in"
            "Focus Mode. Thumbnails may appear in the Focus Mode panel and in"
            "Media Controls."
          trigger:
            "The Focus Mode panel is opened"
          data: "None"
          user_data {
            type: NONE
          }
          internal {
            contacts {
              email: "[email protected]"
            }
          }
          destination: GOOGLE_OWNED_SERVICE
          last_reviewed: "2024-04-26"
        }
        policy {
         cookies_allowed: NO
         setting:
           "Cannot be disabled via settings."
         chrome_policy {
           FocusModeSoundsEnabled {
             FocusModeSoundsEnabled: "disabled"
           }
         }
        })");

std::unique_ptr<network::SimpleURLLoader> CreateSimpleURLLoader(
    const GURL& url) {
  auto resource_request = std::make_unique<network::ResourceRequest>();
  resource_request->url = url;
  resource_request->method = "GET";
  resource_request->credentials_mode = network::mojom::CredentialsMode::kOmit;

  return network::SimpleURLLoader::Create(std::move(resource_request),
                                          kFocusSoundsNetworkTag);
}

class SoundscapesDownloaderImpl : public SoundscapesDownloader {
 public:
  SoundscapesDownloaderImpl(
      SoundscapesDownloader::Urls config,
      scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory)
      : configuration_(config), url_loader_factory_(url_loader_factory) {}
  ~SoundscapesDownloaderImpl() override = default;

  void FetchConfiguration(ConfigurationCallback callback) override {
    GURL destination = configuration_.host.Resolve(configuration_.config_path);
    pending_request_ = CreateSimpleURLLoader(destination);
    network::SimpleURLLoader::BodyAsStringCallback handler =
        base::BindOnce(&SoundscapesDownloaderImpl::HandleConfigurationString,
                       weak_factory_.GetWeakPtr(), std::move(callback));
    const int retry_mode = network::SimpleURLLoader::RETRY_ON_5XX |
                           network::SimpleURLLoader::RETRY_ON_NETWORK_CHANGE |
                           network::SimpleURLLoader::RETRY_ON_NAME_NOT_RESOLVED;
    pending_request_->SetRetryOptions(kMaxRetries, retry_mode);
    pending_request_->DownloadToString(url_loader_factory_.get(),
                                       std::move(handler), kMaxDownloadBytes);
  }

  GURL ResolveUrl(std::string_view path) override {
    GURL resolved = configuration_.host.Resolve(path);
    if (!resolved.is_valid()) {
      // If the path segment results in an invalid url, use an empty one
      // instead to guard against accidental use.
      return GURL();
    }

    return resolved;
  }

 private:
  void HandleConfigurationString(ConfigurationCallback callback,
                                 std::optional<std::string> response_body) {
    // Move the pending request here so it's deleted when this function ends.
    std::unique_ptr<network::SimpleURLLoader> request =
        std::move(pending_request_);

    if (!response_body || response_body->empty()) {
      std::move(callback).Run(std::nullopt);
      return;
    }
    base::ThreadPool::PostTaskAndReplyWithResult(
        FROM_HERE,
        {base::TaskPriority::USER_VISIBLE,
         base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN},
        base::BindOnce(&SoundscapeConfiguration::ParseConfiguration,
                       configuration_.locale, *response_body),
        base::BindOnce(&SoundscapesDownloaderImpl::HandleParsedConfiguration,
                       weak_factory_.GetWeakPtr(), std::move(callback)));
  }

  void HandleParsedConfiguration(
      ConfigurationCallback callback,
      std::optional<SoundscapeConfiguration> configuration) {
    std::move(callback).Run(std::move(configuration));
  }

  std::unique_ptr<network::SimpleURLLoader> pending_request_;
  SoundscapesDownloader::Urls configuration_;
  scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory_;
  base::WeakPtrFactory<SoundscapesDownloaderImpl> weak_factory_{this};
};

}  // namespace

SoundscapesDownloader::Urls::Urls() = default;
SoundscapesDownloader::Urls::Urls(const Urls&) = default;
SoundscapesDownloader::Urls::~Urls() = default;

// static
std::unique_ptr<SoundscapesDownloader> SoundscapesDownloader::Create(
    const std::string& locale) {
  CHECK(!locale.empty());

  return std::make_unique<SoundscapesDownloaderImpl>(
      ProductionConfiguration(locale),
      Shell::Get()->shell_delegate()->GetBrowserProcessUrlLoaderFactory());
}

// static
std::unique_ptr<SoundscapesDownloader> SoundscapesDownloader::CreateForTesting(
    const SoundscapesDownloader::Urls& configuration,
    scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory) {
  return std::make_unique<SoundscapesDownloaderImpl>(configuration,
                                                     url_loader_factory);
}

}  // namespace ash