| // Copyright 2020 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "src/ui/a11y/lib/screen_reader/speaker.h" |
| |
| #include <lib/async/default.h> |
| #include <lib/fpromise/bridge.h> |
| #include <lib/syslog/cpp/macros.h> |
| |
| #include "lib/fpromise/promise.h" |
| |
| namespace a11y { |
| namespace { |
| |
| using fuchsia::accessibility::tts::Utterance; |
| |
| // Concatenates all utterance strings into a single string. |
| std::string ConcatenateUtterances( |
| const std::vector<ScreenReaderMessageGenerator::UtteranceAndContext>& utterances) { |
| std::string utterance; |
| if (!utterances.empty()) { |
| auto it = utterances.begin(); |
| utterance = it->utterance.has_message() ? it->utterance.message() : ""; |
| it++; |
| while (it != utterances.end()) { |
| if (it->utterance.has_message()) { |
| utterance += " " + it->utterance.message(); |
| } |
| it++; |
| } |
| } |
| return utterance; |
| } |
| |
| } // namespace |
| |
| Speaker::Speaker(async::Executor* executor, fuchsia::accessibility::tts::EnginePtr* tts_engine_ptr, |
| std::unique_ptr<ScreenReaderMessageGenerator> screen_reader_message_generator) |
| : executor_(executor), |
| tts_engine_ptr_(tts_engine_ptr), |
| screen_reader_message_generator_(std::move(screen_reader_message_generator)) { |
| FX_DCHECK(tts_engine_ptr_); |
| } |
| |
| Speaker::~Speaker() { |
| if (epitaph_) { |
| // This logic here is necessary in order to provide a clean way for the Screen Reader to |
| // announce that it is turning off. Because this class generates promises that reference itself, |
| // and those promises run on a loop that runs after this object has been destroyed, we need a |
| // direct way of making a last message to be spoken. |
| auto utterance = screen_reader_message_generator_->GenerateUtteranceByMessageId(*epitaph_); |
| // There is no time to check back the results, so makes a best effort to speak whatever is here |
| // before sutting down. |
| (*tts_engine_ptr_)->Enqueue(std::move(utterance.utterance), [](auto...) {}); |
| (*tts_engine_ptr_)->Speak([](auto...) {}); |
| } |
| } |
| |
| fpromise::promise<> Speaker::SpeakNodePromise( |
| const fuchsia::accessibility::semantics::Node* node, Options options, |
| ScreenReaderMessageGenerator::ScreenReaderMessageContext message_context) { |
| auto utterances = |
| screen_reader_message_generator_->DescribeNode(node, std::move(message_context)); |
| auto task = std::make_shared<SpeechTask>(std::move(utterances)); |
| |
| return PrepareTask(task, options.interrupt, options.save_utterance); |
| } |
| |
| fpromise::promise<> Speaker::SpeakMessagePromise(Utterance utterance, Options options) { |
| std::vector<ScreenReaderMessageGenerator::UtteranceAndContext> utterances; |
| ScreenReaderMessageGenerator::UtteranceAndContext utterance_and_context; |
| utterance_and_context.utterance = std::move(utterance); |
| utterances.push_back(std::move(utterance_and_context)); |
| auto task = std::make_shared<SpeechTask>(std::move(utterances)); |
| |
| return PrepareTask(task, options.interrupt, options.save_utterance); |
| } |
| |
| fpromise::promise<> Speaker::SpeakMessageByIdPromise(fuchsia::intl::l10n::MessageIds message_id, |
| Options options) { |
| std::vector<ScreenReaderMessageGenerator::UtteranceAndContext> utterances; |
| utterances.emplace_back( |
| screen_reader_message_generator_->GenerateUtteranceByMessageId(message_id)); |
| FX_DCHECK(!utterances.empty()); |
| auto task = std::make_shared<SpeechTask>(std::move(utterances)); |
| |
| return PrepareTask(task, options.interrupt, options.save_utterance); |
| } |
| |
| fpromise::promise<> Speaker::SpeakNodeCanonicalizedLabelPromise( |
| const fuchsia::accessibility::semantics::Node* node, Options options) { |
| const std::string label = |
| node->has_attributes() && node->attributes().has_label() ? node->attributes().label() : ""; |
| std::vector<ScreenReaderMessageGenerator::UtteranceAndContext> utterances; |
| utterances.emplace_back(screen_reader_message_generator_->DescribeCharacterForSpelling(label)); |
| FX_DCHECK(!utterances.empty()); |
| auto task = std::make_shared<SpeechTask>(std::move(utterances)); |
| |
| return PrepareTask(task, options.interrupt, options.save_utterance); |
| } |
| |
| fpromise::promise<> Speaker::PrepareTask(std::shared_ptr<SpeechTask> task, bool interrupt, |
| bool save_utterance) { |
| return fpromise::make_promise( |
| [this, task, interrupt, save_utterance]() mutable -> fpromise::promise<> { |
| if (save_utterance) { |
| last_utterance_ = ConcatenateUtterances(task->utterances); |
| } |
| if (interrupt) { |
| decltype(queue_) empty; |
| std::swap(empty, queue_); |
| queue_.push(std::move(task)); |
| // This task trumps whatever is speaking and starts now, so it cancels any pending |
| // task. |
| return CancelTts(); |
| } |
| // Even when not interrupting, the task needs to be part of the queue. |
| queue_.push(std::move(task)); |
| if (queue_.size() == 1) { |
| // This is the only task in the queue, it can start right away. |
| return fpromise::make_ok_promise(); |
| } |
| return WaitInQueue(std::weak_ptr(queue_.back())); |
| }) |
| .and_then(DispatchUtterances(task, interrupt)) |
| .then( |
| [this, task](fpromise::result<>& result) { return EndSpeechTask(task, result.is_ok()); }); |
| } |
| |
| fpromise::promise<> Speaker::DispatchUtterances(std::shared_ptr<SpeechTask> task, bool interrupt) { |
| return fpromise::make_promise( |
| [this, weak_task = std::weak_ptr(task)]() mutable -> fpromise::promise<> { |
| return DispatchSingleUtterance(std::move(weak_task)); |
| }) |
| .and_then( |
| [this, weak_task = std::weak_ptr(task), interrupt]() mutable -> fpromise::promise<> { |
| auto task = weak_task.lock(); |
| if (!task) { |
| return fpromise::make_error_promise(); |
| } |
| if (static_cast<uint64_t>(task->utterance_index) < task->utterances.size()) { |
| return DispatchUtterances(std::move(task), interrupt); |
| } |
| return fpromise::make_ok_promise(); |
| }); |
| } |
| |
| fpromise::promise<> Speaker::DispatchSingleUtterance(std::weak_ptr<SpeechTask> weak_task) { |
| auto task = weak_task.lock(); |
| if (!task) { |
| return fpromise::make_error_promise(); |
| } |
| |
| // Create a promise representing the (optional) delay, and chain the utterance dispatch off of it. |
| fpromise::promise<> delay = fpromise::make_ok_promise(); |
| FX_DCHECK(static_cast<uint64_t>(task->utterance_index) < task->utterances.size()); |
| auto& utterance_and_context = task->utterances[task->utterance_index]; |
| if (utterance_and_context.delay > zx::duration(0)) { |
| delay = executor_->MakeDelayedPromise(utterance_and_context.delay); |
| } |
| |
| return delay |
| .and_then([weak_task = std::weak_ptr(task)]() mutable -> fpromise::result<Utterance> { |
| auto task = weak_task.lock(); |
| if (!task) { |
| return fpromise::error(); |
| } |
| FX_DCHECK(static_cast<uint64_t>(task->utterance_index) < task->utterances.size()); |
| auto& utterance_and_context = task->utterances[task->utterance_index]; |
| task->utterance_index++; |
| return fpromise::ok(std::move(utterance_and_context.utterance)); |
| }) |
| .and_then([this](Utterance& utterance) mutable -> fpromise::promise<> { |
| return EnqueueUtterance(std::move(utterance)); |
| }) |
| .and_then([this, weak_task = std::weak_ptr(task)]() mutable -> fpromise::promise<> { |
| auto task = weak_task.lock(); |
| if (!task) { |
| return fpromise::make_error_promise(); |
| } |
| return Speak(); |
| }); |
| } |
| |
| fpromise::promise<> Speaker::CancelTts() { |
| fpromise::bridge<> bridge; |
| (*tts_engine_ptr_)->Cancel([completer = std::move(bridge.completer)]() mutable { |
| completer.complete_ok(); |
| }); |
| return bridge.consumer.promise_or(fpromise::error()); |
| } |
| |
| fpromise::promise<> Speaker::EndSpeechTask(std::weak_ptr<SpeechTask> weak_task, bool success) { |
| return fpromise::make_promise([this, weak_task, success]() mutable -> fpromise::promise<> { |
| // If the task no longer exists, this means that it has already been deleted by another |
| // task. |
| auto task = weak_task.lock(); |
| if (!task) { |
| return fpromise::make_error_promise(); |
| } |
| |
| if (!queue_.empty() && queue_.front() == task) { |
| queue_.pop(); |
| } else { |
| FX_LOGS(ERROR) << "Tried to invoke EndSpeechTask more than once for the same task." |
| "Ignoring. We can recover, but this indicates a logic error elsewhere."; |
| FX_DCHECK(false); |
| } |
| |
| if (!queue_.empty()) { |
| // Informs the new first task of the queue that it can start running. |
| queue_.front()->starter.complete_ok(); |
| } |
| |
| if (!success) { |
| return fpromise::make_error_promise(); |
| } |
| |
| return fpromise::make_ok_promise(); |
| }); |
| } |
| |
| fpromise::promise<> Speaker::EnqueueUtterance(Utterance utterance) { |
| fpromise::bridge<> bridge; |
| (*tts_engine_ptr_) |
| ->Enqueue(std::move(utterance), |
| [completer = std::move(bridge.completer)]( |
| fuchsia::accessibility::tts::Engine_Enqueue_Result result) mutable { |
| if (result.is_err()) { |
| FX_LOGS(WARNING) << "Error returned while calling tts::Enqueue()."; |
| return completer.complete_error(); |
| } |
| completer.complete_ok(); |
| }); |
| return bridge.consumer.promise_or(fpromise::error()); |
| } |
| |
| fpromise::promise<> Speaker::Speak() { |
| fpromise::bridge<> bridge; |
| (*tts_engine_ptr_) |
| ->Speak([completer = std::move(bridge.completer)]( |
| fuchsia::accessibility::tts::Engine_Speak_Result result) mutable { |
| if (result.is_err()) { |
| // This is likely due to the screen reader "interrupting" itself. |
| FX_LOGS(WARNING) << "Speaker: Error returned while calling tts::Speak()."; |
| return completer.complete_error(); |
| } |
| completer.complete_ok(); |
| }); |
| return bridge.consumer.promise_or(fpromise::error()); |
| } |
| |
| fpromise::promise<> Speaker::WaitInQueue(std::weak_ptr<SpeechTask> weak_task) { |
| auto task = weak_task.lock(); |
| if (!task) { |
| return fpromise::make_error_promise(); |
| } |
| fpromise::bridge<> bridge; |
| // This completer will be invoked once this task reaches the front of the queue of tasks, ending |
| // the wait. |
| task->starter = std::move(bridge.completer); |
| return bridge.consumer.promise_or(fpromise::error()); |
| } |
| |
| Speaker::SpeechTask::SpeechTask( |
| std::vector<ScreenReaderMessageGenerator::UtteranceAndContext> utterances_arg) |
| : utterances(std::move(utterances_arg)) {} |
| |
| Speaker::SpeechTask::~SpeechTask() { |
| if (starter) { |
| // This fpromise::completer still holds the capability of completing this task, so it needs to |
| // inform any promise that is waiting on it that it has been abandoned. |
| starter.abandon(); |
| } |
| } |
| |
| } // namespace a11y |