| // 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 "base/time/time.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 base::TimeDelta kPrintExpectationDelay = base::Seconds(3); |
| |
| } // 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_ = base::TimeTicks::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_ = base::TimeTicks::Now() - time_of_last_utterance_; |
| 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() {} |
| |
| 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()), |
| kPrintExpectationDelay); |
| |
| 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 ((base::TimeTicks::Now() - time_of_last_utterance_) < |
| kPrintExpectationDelay || |
| 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 |