chromium/chromeos/components/quick_answers/utils/spell_check_language.cc

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

#include "chromeos/components/quick_answers/utils/spell_check_language.h"

#include "base/files/file_util.h"
#include "base/logging.h"
#include "base/path_service.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/threading/scoped_blocking_call.h"
#include "chrome/common/chrome_paths.h"
#include "components/spellcheck/common/spellcheck_common.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/service_process_host.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"
#include "services/network/public/mojom/url_response_head.mojom.h"

namespace quick_answers {
namespace {

constexpr char kDownloadServerUrl[] =
    "https://redirector.gvt1.com/edgedl/chrome/dict/";

constexpr char kQuickAnswersDictionarySubDirName[] = "quick_answers";

constexpr net::NetworkTrafficAnnotationTag kNetworkTrafficAnnotationTag =
    net::DefineNetworkTrafficAnnotation("quick_answers_spellchecker", R"(
          semantics {
            sender: "Quick Answers Spellchecker"
            description:
              "Download spell checker dictionary for Quick Answers feature. "
              "The downloaded dictionaries are used to generate intents for "
              "selected text."
            trigger: "Eligible for Quick Answers feature"
            data:
              "The spell checking language identifier. No user identifier is "
              "sent other than user locales."
            destination: GOOGLE_OWNED_SERVICE
            internal {
              contacts {
                email: "[email protected]"
              }
            }
            user_data {
              type: OTHER
            }
            last_reviewed: "2023-06-30"
          }
          policy {
            cookies_allowed: NO
            setting:
              "Quick Answers can be enabled/disabled in ChromeOS Settings and"
              "is subject to eligibility requirements."
            chrome_policy {
              QuickAnswersEnabled {
                QuickAnswersEnabled: false
              }
            }
          })");

constexpr int kMaxRetries = 1;

base::FilePath GetDictionaryDirectoryPath() {
  base::FilePath dict_dir;
  if (!base::PathService::Get(chrome::DIR_APP_DICTIONARIES, &dict_dir)) {
    return base::FilePath();
  }
  return dict_dir.AppendASCII(kQuickAnswersDictionarySubDirName);
}

bool EnsureDictionaryDirectoryExists() {
  base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
                                                base::BlockingType::MAY_BLOCK);

  base::FilePath dict_dir = GetDictionaryDirectoryPath();

  if (dict_dir.empty()) {
    LOG(ERROR) << "Failed to resolve the dictionary directory path.";
    return false;
  }

  // `base::CreateDirectory` returns true if it successfully creates
  // the directory or if the directory already exists.
  return base::CreateDirectory(dict_dir);
}

GURL GetDictionaryURL(const std::string& file_name) {
  return GURL(std::string(kDownloadServerUrl) + base::ToLowerASCII(file_name));
}

base::File OpenDictionaryFile(const base::FilePath& file_path) {
  base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
                                                base::BlockingType::MAY_BLOCK);

  base::File file(file_path, base::File::FLAG_READ | base::File::FLAG_OPEN);
  return file;
}

void CloseDictionaryFile(base::File file) {
  base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
                                                base::BlockingType::MAY_BLOCK);
  file.Close();
}

bool RemoveDictionaryFile(const base::FilePath& file_path) {
  base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
                                                base::BlockingType::MAY_BLOCK);

  return base::DeleteFile(file_path);
}

}  // namespace

SpellCheckLanguage::SpellCheckLanguage(
    scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory)
    : task_runner_(base::ThreadPool::CreateSequencedTaskRunner(
          {base::MayBlock(), base::TaskPriority::USER_VISIBLE})),
      url_loader_factory_(url_loader_factory) {}

SpellCheckLanguage::~SpellCheckLanguage() = default;

void SpellCheckLanguage::Initialize(const std::string& language) {
  language_ = language;

  task_runner_->PostTaskAndReplyWithResult(
      FROM_HERE, base::BindOnce(&EnsureDictionaryDirectoryExists),
      base::BindOnce(&SpellCheckLanguage::OnDictionaryDirectoryExistsComplete,
                     weak_factory_.GetWeakPtr()));
}

void SpellCheckLanguage::CheckSpelling(const std::string& word,
                                       CheckSpellingCallback callback) {
  if (!dictionary_initialized_) {
    std::move(callback).Run(false);
    return;
  }

  dictionary_->CheckSpelling(word, std::move(callback));
}

void SpellCheckLanguage::InitializeSpellCheckService() {
  if (!service_) {
    service_ = content::ServiceProcessHost::Launch<mojom::SpellCheckService>(
        content::ServiceProcessHost::Options()
            .WithDisplayName("Quick answers spell check service")
            .Pass());
  }

  task_runner_->PostTaskAndReplyWithResult(
      FROM_HERE, base::BindOnce(&OpenDictionaryFile, dictionary_file_path_),
      base::BindOnce(&SpellCheckLanguage::OnOpenDictionaryFileComplete,
                     weak_factory_.GetWeakPtr()));
}

void SpellCheckLanguage::OnSimpleURLLoaderComplete(base::FilePath tmp_path) {
  int response_code = -1;
  if (loader_->ResponseInfo() && loader_->ResponseInfo()->headers)
    response_code = loader_->ResponseInfo()->headers->response_code();

  if (loader_->NetError() != net::OK || ((response_code / 100) != 2)) {
    LOG(ERROR) << "Failed to download the dictionary.";
    MaybeRetryInitialize();
    return;
  }

  task_runner_->PostTaskAndReplyWithResult(
      FROM_HERE, base::BindOnce(&base::PathExists, dictionary_file_path_),
      base::BindOnce(&SpellCheckLanguage::OnSaveDictionaryDataComplete,
                     weak_factory_.GetWeakPtr()));
}

void SpellCheckLanguage::OnDictionaryDirectoryExistsComplete(
    bool directory_exists) {
  if (!directory_exists) {
    LOG(ERROR) << "Failed to find or create the dictionary directory.";
    MaybeRetryInitialize();
    return;
  }

  base::FilePath dict_dir = GetDictionaryDirectoryPath();
  if (dict_dir.empty()) {
    LOG(ERROR) << "Failed to resolve the dictionary directory path.";
    MaybeRetryInitialize();
    return;
  }
  dictionary_file_path_ = spellcheck::GetVersionedFileName(language_, dict_dir);

  task_runner_->PostTaskAndReplyWithResult(
      FROM_HERE, base::BindOnce(&base::PathExists, dictionary_file_path_),
      base::BindOnce(&SpellCheckLanguage::OnPathExistsComplete,
                     weak_factory_.GetWeakPtr()));
}

void SpellCheckLanguage::OnDictionaryCreated(
    mojo::PendingRemote<mojom::SpellCheckDictionary> dictionary) {
  dictionary_.reset();

  if (dictionary.is_valid()) {
    dictionary_.Bind(std::move(dictionary));
    dictionary_initialized_ = true;
    return;
  }

  MaybeRetryInitialize();
}

void SpellCheckLanguage::MaybeRetryInitialize() {
  task_runner_->PostTaskAndReplyWithResult(
      FROM_HERE, base::BindOnce(&RemoveDictionaryFile, dictionary_file_path_),
      base::BindOnce(&SpellCheckLanguage::OnFileRemovedForRetry,
                     weak_factory_.GetWeakPtr()));
}

void SpellCheckLanguage::OnFileRemovedForRetry(bool file_removed) {
  if (num_retries_ >= kMaxRetries) {
    LOG(ERROR) << "Service initialize failed after max retries.";
    service_.reset();
    return;
  }

  if (!file_removed) {
    LOG(ERROR) << "Will not retry - could not remove the dictionary file.";
    service_.reset();
    return;
  }

  ++num_retries_;
  Initialize(language_);
}

void SpellCheckLanguage::OnPathExistsComplete(bool path_exists) {
  // If the dictionary is not available, try to download it from the server.
  if (!path_exists) {
    auto url =
        GetDictionaryURL(dictionary_file_path_.BaseName().MaybeAsASCII());

    auto resource_request = std::make_unique<network::ResourceRequest>();
    resource_request->url = url;
    resource_request->credentials_mode = network::mojom::CredentialsMode::kOmit;
    loader_ = network::SimpleURLLoader::Create(std::move(resource_request),
                                               kNetworkTrafficAnnotationTag);

    loader_->SetRetryOptions(
        /*max_retries=*/3,
        network::SimpleURLLoader::RetryMode::RETRY_ON_5XX |
            network::SimpleURLLoader::RETRY_ON_NETWORK_CHANGE);

    loader_->DownloadToFile(
        url_loader_factory_.get(),
        base::BindOnce(&SpellCheckLanguage::OnSimpleURLLoaderComplete,
                       base::Unretained(this)),
        dictionary_file_path_);
    return;
  }

  InitializeSpellCheckService();
}

void SpellCheckLanguage::OnSaveDictionaryDataComplete(bool dictionary_saved) {
  if (!dictionary_saved) {
    MaybeRetryInitialize();
    return;
  }

  InitializeSpellCheckService();
}

void SpellCheckLanguage::OnOpenDictionaryFileComplete(base::File file) {
  service_->CreateDictionary(
      file.Duplicate(), base::BindOnce(&SpellCheckLanguage::OnDictionaryCreated,
                                       base::Unretained(this)));

  task_runner_->PostTask(FROM_HERE,
                         base::BindOnce(&CloseDictionaryFile, std::move(file)));
}

}  // namespace quick_answers