// Copyright (c) 2011 The Chromium 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 <string>
#include <vector>
#include "base/float_util.h"
#include "base/json/json_writer.h"
#include "base/message_loop.h"
#include "base/values.h"
#include "chrome/browser/extensions/extension_event_router.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/extensions/extension_tts_api.h"
#include "chrome/browser/profiles/profile.h"
namespace util = extension_tts_api_util;
namespace {
const char kSpeechInterruptedError[] = "Utterance interrupted.";
const char kSpeechRemovedFromQueueError[] = "Utterance removed from queue.";
const int kSpeechCheckDelayIntervalMs = 100;
};
namespace events {
const char kOnSpeak[] = "experimental.tts.onSpeak";
const char kOnStop[] = "experimental.tts.onStop";
}; // namespace events
//
// ExtensionTtsPlatformImpl
//
std::string ExtensionTtsPlatformImpl::error() {
return error_;
}
void ExtensionTtsPlatformImpl::clear_error() {
error_ = std::string();
}
void ExtensionTtsPlatformImpl::set_error(const std::string& error) {
error_ = error;
}
//
// Utterance
//
// static
int Utterance::next_utterance_id_ = 0;
Utterance::Utterance(Profile* profile,
const std::string& text,
DictionaryValue* options,
Task* completion_task)
: profile_(profile),
id_(next_utterance_id_++),
text_(text),
rate_(-1.0),
pitch_(-1.0),
volume_(-1.0),
can_enqueue_(false),
completion_task_(completion_task) {
if (!options) {
// Use all default options.
options_.reset(new DictionaryValue());
return;
}
options_.reset(options->DeepCopy());
if (options->HasKey(util::kVoiceNameKey))
options->GetString(util::kVoiceNameKey, &voice_name_);
if (options->HasKey(util::kLocaleKey))
options->GetString(util::kLocaleKey, &locale_);
if (options->HasKey(util::kGenderKey))
options->GetString(util::kGenderKey, &gender_);
if (util::ReadNumberByKey(options, util::kRateKey, &rate_)) {
if (!base::IsFinite(rate_) || rate_ < 0.0 || rate_ > 1.0)
rate_ = -1.0;
}
if (util::ReadNumberByKey(options, util::kPitchKey, &pitch_)) {
if (!base::IsFinite(pitch_) || pitch_ < 0.0 || pitch_ > 1.0)
pitch_ = -1.0;
}
if (util::ReadNumberByKey(options, util::kVolumeKey, &volume_)) {
if (!base::IsFinite(volume_) || volume_ < 0.0 || volume_ > 1.0)
volume_ = -1.0;
}
if (options->HasKey(util::kEnqueueKey))
options->GetBoolean(util::kEnqueueKey, &can_enqueue_);
}
Utterance::~Utterance() {
DCHECK_EQ(completion_task_, static_cast<Task *>(NULL));
}
void Utterance::FinishAndDestroy() {
completion_task_->Run();
completion_task_ = NULL;
delete this;
}
//
// ExtensionTtsController
//
// static
ExtensionTtsController* ExtensionTtsController::GetInstance() {
return Singleton<ExtensionTtsController>::get();
}
ExtensionTtsController::ExtensionTtsController()
: ALLOW_THIS_IN_INITIALIZER_LIST(method_factory_(this)),
current_utterance_(NULL),
platform_impl_(NULL) {
}
ExtensionTtsController::~ExtensionTtsController() {
FinishCurrentUtterance();
ClearUtteranceQueue();
}
void ExtensionTtsController::SpeakOrEnqueue(Utterance* utterance) {
if (IsSpeaking() && utterance->can_enqueue()) {
utterance_queue_.push(utterance);
} else {
Stop();
SpeakNow(utterance);
}
}
std::string ExtensionTtsController::GetMatchingExtensionId(
Utterance* utterance) {
ExtensionService* service = utterance->profile()->GetExtensionService();
DCHECK(service);
ExtensionEventRouter* event_router =
utterance->profile()->GetExtensionEventRouter();
DCHECK(event_router);
const ExtensionList* extensions = service->extensions();
ExtensionList::const_iterator iter;
for (iter = extensions->begin(); iter != extensions->end(); ++iter) {
const Extension* extension = *iter;
if (!event_router->ExtensionHasEventListener(
extension->id(), events::kOnSpeak) ||
!event_router->ExtensionHasEventListener(
extension->id(), events::kOnStop)) {
continue;
}
const std::vector<Extension::TtsVoice>& tts_voices =
extension->tts_voices();
for (size_t i = 0; i < tts_voices.size(); ++i) {
const Extension::TtsVoice& voice = tts_voices[i];
if (!voice.voice_name.empty() &&
!utterance->voice_name().empty() &&
voice.voice_name != utterance->voice_name()) {
continue;
}
if (!voice.locale.empty() &&
!utterance->locale().empty() &&
voice.locale != utterance->locale()) {
continue;
}
if (!voice.gender.empty() &&
!utterance->gender().empty() &&
voice.gender != utterance->gender()) {
continue;
}
return extension->id();
}
}
return std::string();
}
void ExtensionTtsController::SpeakNow(Utterance* utterance) {
std::string extension_id = GetMatchingExtensionId(utterance);
if (!extension_id.empty()) {
current_utterance_ = utterance;
utterance->set_extension_id(extension_id);
ListValue args;
args.Set(0, Value::CreateStringValue(utterance->text()));
// Pass through all options to the speech engine, except for
// "enqueue", which the speech engine doesn't need to handle.
DictionaryValue* options = static_cast<DictionaryValue*>(
utterance->options()->DeepCopy());
if (options->HasKey(util::kEnqueueKey))
options->Remove(util::kEnqueueKey, NULL);
args.Set(1, options);
args.Set(2, Value::CreateIntegerValue(utterance->id()));
std::string json_args;
base::JSONWriter::Write(&args, false, &json_args);
utterance->profile()->GetExtensionEventRouter()->DispatchEventToExtension(
extension_id,
events::kOnSpeak,
json_args,
utterance->profile(),
GURL());
return;
}
GetPlatformImpl()->clear_error();
bool success = GetPlatformImpl()->Speak(
utterance->text(),
utterance->locale(),
utterance->gender(),
utterance->rate(),
utterance->pitch(),
utterance->volume());
if (!success) {
utterance->set_error(GetPlatformImpl()->error());
utterance->FinishAndDestroy();
return;
}
current_utterance_ = utterance;
// Check to see if it's still speaking; finish the utterance if not and
// start polling if so. Checking immediately helps to avoid flaky unit
// tests by forcing them to set expectations for IsSpeaking.
CheckSpeechStatus();
}
void ExtensionTtsController::Stop() {
if (current_utterance_ && !current_utterance_->extension_id().empty()) {
current_utterance_->profile()->GetExtensionEventRouter()->
DispatchEventToExtension(
current_utterance_->extension_id(),
events::kOnStop,
"[]",
current_utterance_->profile(),
GURL());
} else {
GetPlatformImpl()->clear_error();
GetPlatformImpl()->StopSpeaking();
}
if (current_utterance_)
current_utterance_->set_error(kSpeechInterruptedError);
FinishCurrentUtterance();
ClearUtteranceQueue();
}
void ExtensionTtsController::OnSpeechFinished(
int request_id, const std::string& error_message) {
// We may sometimes receive completion callbacks "late", after we've
// already finished the utterance (for example because another utterance
// interrupted or we got a call to Stop). It's also possible that a buggy
// extension has called this more than once. In either case it's safe to
// just ignore this call.
if (!current_utterance_ || request_id != current_utterance_->id())
return;
current_utterance_->set_error(error_message);
FinishCurrentUtterance();
SpeakNextUtterance();
}
bool ExtensionTtsController::IsSpeaking() const {
return current_utterance_ != NULL;
}
void ExtensionTtsController::FinishCurrentUtterance() {
if (current_utterance_) {
current_utterance_->FinishAndDestroy();
current_utterance_ = NULL;
}
}
void ExtensionTtsController::SpeakNextUtterance() {
// Start speaking the next utterance in the queue. Keep trying in case
// one fails but there are still more in the queue to try.
while (!utterance_queue_.empty() && !current_utterance_) {
Utterance* utterance = utterance_queue_.front();
utterance_queue_.pop();
SpeakNow(utterance);
}
}
void ExtensionTtsController::ClearUtteranceQueue() {
while (!utterance_queue_.empty()) {
Utterance* utterance = utterance_queue_.front();
utterance_queue_.pop();
utterance->set_error(kSpeechRemovedFromQueueError);
utterance->FinishAndDestroy();
}
}
void ExtensionTtsController::CheckSpeechStatus() {
if (!current_utterance_)
return;
if (!current_utterance_->extension_id().empty())
return;
if (GetPlatformImpl()->IsSpeaking() == false) {
FinishCurrentUtterance();
SpeakNextUtterance();
}
// If we're still speaking something (either the prevoius utterance or
// a new utterance), keep calling this method after another delay.
// TODO(dmazzoni): get rid of this as soon as all platform implementations
// provide completion callbacks rather than only supporting polling.
if (current_utterance_ && current_utterance_->extension_id().empty()) {
MessageLoop::current()->PostDelayedTask(
FROM_HERE, method_factory_.NewRunnableMethod(
&ExtensionTtsController::CheckSpeechStatus),
kSpeechCheckDelayIntervalMs);
}
}
void ExtensionTtsController::SetPlatformImpl(
ExtensionTtsPlatformImpl* platform_impl) {
platform_impl_ = platform_impl;
}
ExtensionTtsPlatformImpl* ExtensionTtsController::GetPlatformImpl() {
if (!platform_impl_)
platform_impl_ = ExtensionTtsPlatformImpl::GetInstance();
return platform_impl_;
}
//
// Extension API functions
//
bool ExtensionTtsSpeakFunction::RunImpl() {
std::string text;
EXTENSION_FUNCTION_VALIDATE(args_->GetString(0, &text));
DictionaryValue* options = NULL;
if (args_->GetSize() >= 2)
EXTENSION_FUNCTION_VALIDATE(args_->GetDictionary(1, &options));
Task* completion_task = NewRunnableMethod(
this, &ExtensionTtsSpeakFunction::SpeechFinished);
utterance_ = new Utterance(profile(), text, options, completion_task);
AddRef(); // Balanced in SpeechFinished().
ExtensionTtsController::GetInstance()->SpeakOrEnqueue(utterance_);
return true;
}
void ExtensionTtsSpeakFunction::SpeechFinished() {
error_ = utterance_->error();
bool success = error_.empty();
SendResponse(success);
Release(); // Balanced in RunImpl().
}
bool ExtensionTtsStopSpeakingFunction::RunImpl() {
ExtensionTtsController::GetInstance()->Stop();
return true;
}
bool ExtensionTtsIsSpeakingFunction::RunImpl() {
result_.reset(Value::CreateBooleanValue(
ExtensionTtsController::GetInstance()->IsSpeaking()));
return true;
}
bool ExtensionTtsSpeakCompletedFunction::RunImpl() {
int request_id;
std::string error_message;
EXTENSION_FUNCTION_VALIDATE(args_->GetInteger(0, &request_id));
if (args_->GetSize() >= 2)
EXTENSION_FUNCTION_VALIDATE(args_->GetString(1, &error_message));
ExtensionTtsController::GetInstance()->OnSpeechFinished(
request_id, error_message);
return true;
}