chromium/chrome/browser/ash/accessibility/speech_monitor.cc

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

#include "chrome/browser/ash/accessibility/speech_monitor.h"

#include "base/containers/contains.h"
#include "base/run_loop.h"
#include "base/strings/pattern.h"
#include "base/strings/string_util.h"
#include "chrome/common/extensions/extension_constants.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/tts_controller.h"
#include "content/public/browser/tts_utterance.h"
#include "content/public/test/test_utils.h"

namespace ash {
namespace test {
namespace {

constexpr int kPrintExpectationDelayMs = 3000;

}  // namespace

SpeechMonitor::Expectation::Expectation(const std::string& text)
    : text_(text) {}
SpeechMonitor::Expectation::~Expectation() = default;
SpeechMonitor::Expectation::Expectation(const Expectation&) = default;

base::circular_deque<SpeechMonitorUtterance>::const_iterator
SpeechMonitor::Expectation::Matches(
    const base::circular_deque<SpeechMonitorUtterance>& queue) const {
  std::vector<std::string> all_text;
  for (auto it = queue.begin(); it != queue.end(); it++) {
    if (base::Contains(disallowed_text_, it->text)) {
      break;
    }

    all_text.push_back(it->text);
    std::string joined_all_text = base::JoinString(all_text, " ");
    bool text_match = as_pattern_
                          ? (base::MatchPattern(it->text, text_) ||
                             base::MatchPattern(joined_all_text, "*" + text_))
                          : (it->text == text_ ||
                             joined_all_text.find(text_) != std::string::npos);
    if (!text_match) {
      continue;
    }

    bool locale_match = !locale_ || it->lang == locale_;
    if (!locale_match) {
      continue;
    }

    return it;
  }
  return queue.end();
}

std::string SpeechMonitor::Expectation::ToString() const {
  std::string ret = "\"" + text_ + "\"";
  std::string options = OptionsToString();
  if (!options.empty()) {
    ret += " {" + options + "}";
  }
  return ret;
}

std::string SpeechMonitor::Expectation::OptionsToString() const {
  std::vector<std::string> option_str;
  if (as_pattern_) {
    option_str.push_back("pattern: true");
  }
  if (locale_) {
    option_str.push_back("locale: " + locale_.value());
  }
  if (disallowed_text_.size() > 0) {
    option_str.push_back("disallowed: [" +
                         base::JoinString(disallowed_text_, ", ") + "]");
  }
  return base::JoinString(option_str, ", ");
}

SpeechMonitor::SpeechMonitor() {
  content::TtsController::SkipAddNetworkChangeObserverForTests(true);
  content::TtsController::GetInstance()->SetTtsPlatform(this);
}

SpeechMonitor::~SpeechMonitor() {
  content::TtsController::GetInstance()->SetTtsPlatform(
      content::TtsPlatform::GetInstance());
  if (!replay_queue_.empty() || !replayed_queue_.empty())
    CHECK(replay_called_) << "Expectation was made, but Replay() not called.";
}

bool SpeechMonitor::PlatformImplSupported() {
  return true;
}

bool SpeechMonitor::PlatformImplInitialized() {
  return true;
}

void SpeechMonitor::Speak(int utterance_id,
                          const std::string& utterance,
                          const std::string& lang,
                          const content::VoiceData& voice,
                          const content::UtteranceContinuousParameters& params,
                          base::OnceCallback<void(bool)> on_speak_finished) {
  CHECK(!utterance.empty())
      << "If you're deliberately speaking the "
         "empty string in a test, that's probably not the correct way to "
         "achieve stopping speech. If it is unintended, it indicates a deeper "
         "underlying issue.";
  text_params_[utterance] = params;
  content::TtsController::GetInstance()->OnTtsEvent(
      utterance_id, content::TTS_EVENT_START, 0,
      static_cast<int>(utterance.size()), std::string());

  utterance_ = utterance;
  utterance_id_ = utterance_id;
  on_speak_finished_ = std::move(on_speak_finished);
  if (!send_word_events_and_wait_to_finish_) {
    // finish immediately.
    FinishSpeech();
    return;
  }

  std::size_t space = utterance.find(" ");
  while (space != std::string::npos) {
    // Send word events. This supports some Select-to-Speak tests.
    std::size_t next_space = utterance.find(" ", space + 1);
    int length =
        (next_space == std::string::npos ? utterance.size() : next_space) -
        space;
    content::TtsController::GetInstance()->OnTtsEvent(
        utterance_id, content::TTS_EVENT_WORD, space, length, std::string());
    base::RunLoop().RunUntilIdle();
    space = next_space;
  }
}

void SpeechMonitor::FinishSpeech() {
  CHECK(utterance_id_ != -1)
      << "Cannot FinishSpeech as Speak has not yet been called.";
  content::TtsController::GetInstance()->OnTtsEvent(
      utterance_id_, content::TTS_EVENT_END,
      static_cast<int>(utterance_.size()), 0, std::string());
  std::move(on_speak_finished_).Run(true);
  utterance_ = "";
  utterance_id_ = -1;
  on_speak_finished_.Reset();

  time_of_last_utterance_ = std::chrono::steady_clock::now();
}

bool SpeechMonitor::StopSpeaking() {
  ++stop_count_;
  return true;
}

bool SpeechMonitor::IsSpeaking() {
  return false;
}

void SpeechMonitor::GetVoices(std::vector<content::VoiceData>* out_voices) {
  out_voices->push_back(content::VoiceData());
  content::VoiceData& voice = out_voices->back();
  voice.native = true;
  voice.name = "SpeechMonitor";
  voice.engine_id = extension_misc::kGoogleSpeechSynthesisExtensionId;
  voice.events.insert(content::TTS_EVENT_END);
}

void SpeechMonitor::WillSpeakUtteranceWithVoice(
    content::TtsUtterance* utterance,
    const content::VoiceData& voice_data) {
  if (!utterance_queue_.empty() &&
      utterance_queue_.back().text == utterance->GetText() &&
      !base::Contains(repeated_speech_, utterance->GetText())) {
    repeated_speech_.push_back(utterance->GetText());
  }

  utterance_queue_.emplace_back(utterance->GetText(), utterance->GetLang());
  delay_for_last_utterance_ms_ = CalculateUtteranceDelayMS();
  MaybeContinueReplay();
}

void SpeechMonitor::LoadBuiltInTtsEngine(
    content::BrowserContext* browser_context) {}

std::string SpeechMonitor::GetError() {
  return error_;
}

void SpeechMonitor::ClearError() {
  error_ = std::string();
}

void SpeechMonitor::SetError(const std::string& error) {
  error_ = error;
}

void SpeechMonitor::Shutdown() {}

void SpeechMonitor::FinalizeVoiceOrdering(
    std::vector<content::VoiceData>& voices) {}

void SpeechMonitor::RefreshVoices() {}

content::ExternalPlatformDelegate*
SpeechMonitor::GetExternalPlatformDelegate() {
  return nullptr;
}

double SpeechMonitor::CalculateUtteranceDelayMS() {
  std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now();
  std::chrono::duration<double> time_span =
      std::chrono::duration_cast<std::chrono::duration<double>>(
          now - time_of_last_utterance_);
  return time_span.count() * 1000;
}

double SpeechMonitor::GetDelayForLastUtteranceMS() {
  return delay_for_last_utterance_ms_;
}

void SpeechMonitor::ExpectSpeech(const Expectation& expectation,
                                 const base::Location& location) {
  CHECK(!replay_loop_runner_.get());
  replay_queue_.push_back(
      {[this, expectation]() {
         auto itr = expectation.Matches(utterance_queue_);
         if (itr != utterance_queue_.end()) {
           // Erase all utterances that came before the
           // match as well as the match itself.
           utterance_queue_.erase(utterance_queue_.begin(), itr + 1);
           return true;
         }
         return false;
       },
       "ExpectSpeech(" + expectation.ToString() + ") " + location.ToString()});
}

void SpeechMonitor::ExpectSpeech(const std::string& text,
                                 const base::Location& location) {
  ExpectSpeech(Expectation(text), location);
}

void SpeechMonitor::ExpectSpeechPattern(const std::string& pattern,
                                        const base::Location& location) {
  ExpectSpeech(Expectation(pattern).AsPattern(), location);
}

void SpeechMonitor::ExpectNextSpeechIsNot(const std::string& text,
                                          const base::Location& location) {
  CHECK(!replay_loop_runner_.get());
  replay_queue_.push_back(
      {[this, text]() {
         if (utterance_queue_.empty())
           return false;

         return text != utterance_queue_.front().text;
       },
       "ExpectNextSpeechIsNot(\"" + text + "\") " + location.ToString()});
}

void SpeechMonitor::ExpectNextSpeechIsNotPattern(
    const std::string& pattern,
    const base::Location& location) {
  CHECK(!replay_loop_runner_.get());
  replay_queue_.push_back({[this, pattern]() {
                             if (utterance_queue_.empty())
                               return false;

                             return !base::MatchPattern(
                                 utterance_queue_.front().text, pattern);
                           },
                           "ExpectNextSpeechIsNotPattern(\"" + pattern +
                               "\") " + location.ToString()});
}

void SpeechMonitor::ExpectHadNoRepeatedSpeech(const base::Location& location) {
  CHECK(!replay_loop_runner_.get());
  replay_queue_.push_back(
      {[this]() { return repeated_speech_.empty(); },
       "ExpectHadNoRepeatedSpeech() " + location.ToString()});
}

void SpeechMonitor::Call(std::function<void()> func,
                         const base::Location& location) {
  CHECK(!replay_loop_runner_.get());
  replay_queue_.push_back({[func]() {
                             func();
                             return true;
                           },
                           "Call() " + location.ToString()});
}

void SpeechMonitor::Replay() {
  replay_called_ = true;
  MaybeContinueReplay();
}

void SpeechMonitor::MaybeContinueReplay() {
  // This method can be called prior to Replay() being called.
  if (!replay_called_)
    return;

  auto it = replay_queue_.begin();
  while (it != replay_queue_.end()) {
    ReplayArgs current = *it;
    it = replay_queue_.erase(it);
    if (current.first()) {
      // Careful here; the above callback may have triggered more speech which
      // causes |MaybeContinueReplay| to be called recursively. We have to
      // ensure to check |replay_queue_| here.
      if (replay_queue_.empty())
        break;

      replayed_queue_.push_back(current.second);
    } else {
      replay_queue_.insert(replay_queue_.begin(), current);
      it = replay_queue_.begin();
      break;
    }
  }

  if (!replay_queue_.empty()) {
    content::GetUIThreadTaskRunner({})->PostDelayedTask(
        FROM_HERE,
        base::BindOnce(&SpeechMonitor::MaybePrintExpectations,
                       weak_factory_.GetWeakPtr()),
        base::Milliseconds(kPrintExpectationDelayMs));

    if (!replay_loop_runner_.get()) {
      replay_loop_runner_ = new content::MessageLoopRunner();
      replay_loop_runner_->Run();
    }
  } else if (replay_queue_.empty() && replay_loop_runner_.get()) {
    replay_loop_runner_->Quit();
  }
}

void SpeechMonitor::MaybePrintExpectations() {
  if (CalculateUtteranceDelayMS() < kPrintExpectationDelayMs ||
      replay_queue_.empty())
    return;

  if (last_replay_queue_size_ == replay_queue_.size())
    return;

  last_replay_queue_size_ = replay_queue_.size();
  std::vector<std::string> replay_queue_descriptions;
  for (const auto& pair : replay_queue_)
    replay_queue_descriptions.push_back(pair.second);

  std::vector<std::string> utterance_queue_descriptions;
  for (const auto& item : utterance_queue_)
    utterance_queue_descriptions.push_back("\"" + item.text + "\"");

  std::stringstream output;
  output << "Still waiting for expectation(s).\n";
  if (!replay_queue_descriptions.empty()) {
    output << "Unsatisfied expectations...\n"
           << base::JoinString(replay_queue_descriptions, "\n");
  }
  if (!utterance_queue_descriptions.empty()) {
    output << "\n\npending speech utterances...\n"
           << base::JoinString(utterance_queue_descriptions, "\n");
  }
  if (!replayed_queue_.empty()) {
    output << "\n\nSatisfied expectations...\n"
           << base::JoinString(replayed_queue_, "\n");
  }
  if (!repeated_speech_.empty()) {
    output << "\n\nRepeated speech...\n"
           << base::JoinString(repeated_speech_, "\n");
  }

  LOG(ERROR) << output.str();
}

std::optional<content::UtteranceContinuousParameters>
SpeechMonitor::GetParamsForPreviouslySpokenTextPattern(
    const std::string& pattern) {
  for (const auto& [text, params] : text_params_) {
    if (base::MatchPattern(text, pattern)) {
      return params;
    }
  }
  return std::nullopt;
}

}  // namespace test
}  // namespace ash