summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--chrome/browser/chromeos/cros/cros_mock.cc3
-rw-r--r--chrome/browser/chromeos/cros/speech_synthesis_library.cc11
-rw-r--r--chrome/browser/chromeos/cros/speech_synthesis_library.h15
-rw-r--r--chrome/browser/extensions/extension_function_dispatcher.cc1
-rw-r--r--chrome/browser/extensions/extension_tts_api.cc358
-rw-r--r--chrome/browser/extensions/extension_tts_api.h128
-rw-r--r--chrome/browser/extensions/extension_tts_api_chromeos.cc51
-rw-r--r--chrome/browser/extensions/extension_tts_api_util.cc15
-rw-r--r--chrome/browser/extensions/extension_tts_api_util.h20
-rw-r--r--chrome/browser/extensions/extension_tts_apitest.cc43
-rw-r--r--chrome/common/extensions/api/extension_api.json102
-rw-r--r--chrome/common/extensions/extension.cc58
-rw-r--r--chrome/common/extensions/extension.h10
-rw-r--r--chrome/common/extensions/extension_constants.cc17
-rw-r--r--chrome/common/extensions/extension_constants.h12
-rw-r--r--chrome/common/extensions/extension_manifests_unittest.cc34
-rw-r--r--chrome/renderer/resources/extension_process_bindings.js18
-rw-r--r--chrome/test/data/extensions/api_test/tts/chromeos/test.js4
-rw-r--r--chrome/test/data/extensions/api_test/tts/interrupt/test.js3
-rw-r--r--chrome/test/data/extensions/api_test/tts/provide/manifest.json20
-rw-r--r--chrome/test/data/extensions/api_test/tts/provide/test.html1
-rw-r--r--chrome/test/data/extensions/api_test/tts/provide/test.js113
-rw-r--r--chrome/test/data/extensions/api_test/tts/queue_interrupt/test.js10
-rw-r--r--chrome/test/data/extensions/manifest_tests/default_locale_invalid.json5
-rw-r--r--chrome/test/data/extensions/manifest_tests/default_locale_valid.json5
-rw-r--r--chrome/test/data/extensions/manifest_tests/tts_provider_invalid_1.json5
-rw-r--r--chrome/test/data/extensions/manifest_tests/tts_provider_invalid_2.json7
-rw-r--r--chrome/test/data/extensions/manifest_tests/tts_provider_invalid_3.json9
-rw-r--r--chrome/test/data/extensions/manifest_tests/tts_provider_invalid_4.json11
-rw-r--r--chrome/test/data/extensions/manifest_tests/tts_provider_invalid_5.json11
-rw-r--r--chrome/test/data/extensions/manifest_tests/tts_provider_invalid_6.json11
-rw-r--r--chrome/test/data/extensions/manifest_tests/tts_provider_invalid_7.json11
-rw-r--r--chrome/test/data/extensions/manifest_tests/tts_provider_valid.json14
33 files changed, 962 insertions, 174 deletions
diff --git a/chrome/browser/chromeos/cros/cros_mock.cc b/chrome/browser/chromeos/cros/cros_mock.cc
index bae8b9e..efdfbcc 100644
--- a/chrome/browser/chromeos/cros/cros_mock.cc
+++ b/chrome/browser/chromeos/cros/cros_mock.cc
@@ -385,6 +385,9 @@ void CrosMock::SetSpeechSynthesisLibraryExpectations() {
EXPECT_CALL(*mock_speech_synthesis_library_, Speak(_))
.WillOnce(Return(true))
.RetiresOnSaturation();
+ EXPECT_CALL(*mock_speech_synthesis_library_, IsSpeaking())
+ .Times(AnyNumber())
+ .WillRepeatedly(Return(true));
EXPECT_CALL(*mock_speech_synthesis_library_, StopSpeaking())
.WillOnce(Return(true))
.RetiresOnSaturation();
diff --git a/chrome/browser/chromeos/cros/speech_synthesis_library.cc b/chrome/browser/chromeos/cros/speech_synthesis_library.cc
index 859a986..9dc797e 100644
--- a/chrome/browser/chromeos/cros/speech_synthesis_library.cc
+++ b/chrome/browser/chromeos/cros/speech_synthesis_library.cc
@@ -10,6 +10,17 @@
namespace chromeos {
+// TODO(chaitanyag): rename to "locale" after making equivalent change in
+// Chrome OS code.
+const char SpeechSynthesisLibrary::kSpeechPropertyLocale[] = "name";
+
+const char SpeechSynthesisLibrary::kSpeechPropertyGender[] = "gender";
+const char SpeechSynthesisLibrary::kSpeechPropertyRate[] = "rate";
+const char SpeechSynthesisLibrary::kSpeechPropertyPitch[] = "pitch";
+const char SpeechSynthesisLibrary::kSpeechPropertyVolume[] = "volume";
+const char SpeechSynthesisLibrary::kSpeechPropertyEquals[] = "=";
+const char SpeechSynthesisLibrary::kSpeechPropertyDelimiter[] = ";";
+
class SpeechSynthesisLibraryImpl : public SpeechSynthesisLibrary {
public:
SpeechSynthesisLibraryImpl() {}
diff --git a/chrome/browser/chromeos/cros/speech_synthesis_library.h b/chrome/browser/chromeos/cros/speech_synthesis_library.h
index 5ba97d1..fff4409 100644
--- a/chrome/browser/chromeos/cros/speech_synthesis_library.h
+++ b/chrome/browser/chromeos/cros/speech_synthesis_library.h
@@ -16,14 +16,20 @@ class SpeechSynthesisLibrary {
typedef void(*InitStatusCallback)(bool success);
virtual ~SpeechSynthesisLibrary() {}
+
// Speaks the specified text.
virtual bool Speak(const char* text) = 0;
+
// Sets options for the subsequent speech synthesis requests.
+ // Use the constants below.
virtual bool SetSpeakProperties(const char* props) = 0;
+
// Stops speaking the current utterance.
virtual bool StopSpeaking() = 0;
+
// Checks if the engine is currently speaking.
virtual bool IsSpeaking() = 0;
+
// Starts the speech synthesis service and indicates through a callback if
// it started successfully.
virtual void InitTts(InitStatusCallback) = 0;
@@ -31,6 +37,15 @@ class SpeechSynthesisLibrary {
// Factory function, creates a new instance and returns ownership.
// For normal usage, access the singleton via CrosLibrary::Get().
static SpeechSynthesisLibrary* GetImpl(bool stub);
+
+ // Constants to be used with SetSpeakProperties.
+ static const char kSpeechPropertyLocale[];
+ static const char kSpeechPropertyGender[];
+ static const char kSpeechPropertyRate[];
+ static const char kSpeechPropertyPitch[];
+ static const char kSpeechPropertyVolume[];
+ static const char kSpeechPropertyEquals[];
+ static const char kSpeechPropertyDelimiter[];
};
} // namespace chromeos
diff --git a/chrome/browser/extensions/extension_function_dispatcher.cc b/chrome/browser/extensions/extension_function_dispatcher.cc
index 07140c5..3c98cf3 100644
--- a/chrome/browser/extensions/extension_function_dispatcher.cc
+++ b/chrome/browser/extensions/extension_function_dispatcher.cc
@@ -241,6 +241,7 @@ void FactoryRegistry::ResetFunctions() {
RegisterFunction<ExtensionTtsSpeakFunction>();
RegisterFunction<ExtensionTtsStopSpeakingFunction>();
RegisterFunction<ExtensionTtsIsSpeakingFunction>();
+ RegisterFunction<ExtensionTtsSpeakCompletedFunction>();
// Clipboard.
RegisterFunction<ExecuteCopyClipboardFunction>();
diff --git a/chrome/browser/extensions/extension_tts_api.cc b/chrome/browser/extensions/extension_tts_api.cc
index 322085f..c618db3 100644
--- a/chrome/browser/extensions/extension_tts_api.cc
+++ b/chrome/browser/extensions/extension_tts_api.cc
@@ -2,22 +2,35 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-#include "chrome/browser/extensions/extension_tts_api.h"
-
#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 kCrosLibraryNotLoadedError[] =
- "Cros shared library not loaded.";
+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_;
}
@@ -30,20 +43,79 @@ void ExtensionTtsPlatformImpl::set_error(const std::string& error) {
error_ = error;
}
+//
+// Utterance
+//
+
// static
-ExtensionTtsController* ExtensionTtsController::GetInstance() {
- return Singleton<ExtensionTtsController>::get();
+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));
}
-ExtensionTtsController::Utterance::Utterance()
- : rate(-1.0),
- pitch(-1.0),
- volume(-1.0),
- success_task(NULL),
- failure_task(NULL) {
+void Utterance::FinishAndDestroy() {
+ completion_task_->Run();
+ completion_task_ = NULL;
+ delete this;
}
-ExtensionTtsController::Utterance::~Utterance() {}
+//
+// ExtensionTtsController
+//
+
+// static
+ExtensionTtsController* ExtensionTtsController::GetInstance() {
+ return Singleton<ExtensionTtsController>::get();
+}
ExtensionTtsController::ExtensionTtsController()
: ALLOW_THIS_IN_INITIALIZER_LIST(method_factory_(this)),
@@ -51,11 +123,13 @@ ExtensionTtsController::ExtensionTtsController()
platform_impl_(NULL) {
}
-ExtensionTtsController::~ExtensionTtsController() {}
+ExtensionTtsController::~ExtensionTtsController() {
+ FinishCurrentUtterance();
+ ClearUtteranceQueue();
+}
-void ExtensionTtsController::SpeakOrEnqueue(
- Utterance* utterance, bool can_enqueue) {
- if (IsSpeaking() && can_enqueue) {
+void ExtensionTtsController::SpeakOrEnqueue(Utterance* utterance) {
+ if (IsSpeaking() && utterance->can_enqueue()) {
utterance_queue_.push(utterance);
} else {
Stop();
@@ -63,59 +137,167 @@ void ExtensionTtsController::SpeakOrEnqueue(
}
}
+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->language,
- utterance->gender,
- utterance->rate,
- utterance->pitch,
- utterance->volume);
+ utterance->text(),
+ utterance->locale(),
+ utterance->gender(),
+ utterance->rate(),
+ utterance->pitch(),
+ utterance->volume());
if (!success) {
- utterance->error = GetPlatformImpl()->error();
- utterance->failure_task->Run();
- delete utterance->success_task;
- delete utterance;
+ utterance->set_error(GetPlatformImpl()->error());
+ utterance->FinishAndDestroy();
return;
}
current_utterance_ = utterance;
- // Post a task to check if this utterance has completed after a delay.
- MessageLoop::current()->PostDelayedTask(
- FROM_HERE, method_factory_.NewRunnableMethod(
- &ExtensionTtsController::CheckSpeechStatus),
- kSpeechCheckDelayIntervalMs);
+ // 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() {
- GetPlatformImpl()->clear_error();
- GetPlatformImpl()->StopSpeaking();
+ 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, 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_->success_task->Run();
- delete current_utterance_->failure_task;
- delete 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->success_task->Run();
- delete utterance->failure_task;
- delete utterance;
+ utterance->set_error(kSpeechRemovedFromQueueError);
+ utterance->FinishAndDestroy();
}
}
@@ -123,21 +305,19 @@ void ExtensionTtsController::CheckSpeechStatus() {
if (!current_utterance_)
return;
+ if (!current_utterance_->extension_id().empty())
+ return;
+
if (GetPlatformImpl()->IsSpeaking() == false) {
FinishCurrentUtterance();
-
- // 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);
- }
+ SpeakNextUtterance();
}
// If we're still speaking something (either the prevoius utterance or
// a new utterance), keep calling this method after another delay.
- if (current_utterance_) {
+ // 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),
@@ -161,67 +341,25 @@ ExtensionTtsPlatformImpl* ExtensionTtsController::GetPlatformImpl() {
//
bool ExtensionTtsSpeakFunction::RunImpl() {
- utterance_ = new ExtensionTtsController::Utterance();
- bool can_enqueue = false;
-
- DictionaryValue* speak_options = NULL;
- EXTENSION_FUNCTION_VALIDATE(args_->GetString(0, &utterance_->text));
-
- if (args_->GetDictionary(1, &speak_options)) {
- if (speak_options->HasKey(util::kLanguageNameKey)) {
- speak_options->GetString(util::kLanguageNameKey, &utterance_->language);
- }
-
- if (speak_options->HasKey(util::kGenderKey)) {
- speak_options->GetString(util::kGenderKey, &utterance_->gender);
- }
-
- if (speak_options->HasKey(util::kEnqueueKey)) {
- speak_options->GetBoolean(util::kEnqueueKey, &can_enqueue);
- }
-
- if (util::ReadNumberByKey(
- speak_options, util::kRateKey, &utterance_->rate)) {
- if (!base::IsFinite(utterance_->rate) ||
- utterance_->rate < 0.0 ||
- utterance_->rate > 1.0) {
- utterance_->rate = -1.0;
- }
- }
-
- if (util::ReadNumberByKey(
- speak_options, util::kPitchKey, &utterance_->pitch)) {
- if (!base::IsFinite(utterance_->pitch) ||
- utterance_->pitch < 0.0 ||
- utterance_->pitch > 1.0) {
- utterance_->pitch = -1.0;
- }
- }
-
- if (util::ReadNumberByKey(
- speak_options, util::kVolumeKey, &utterance_->volume)) {
- if (!base::IsFinite(utterance_->volume) ||
- utterance_->volume < 0.0 ||
- utterance_->volume > 1.0) {
- utterance_->volume = -1.0;
- }
- }
- }
+ 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().
- utterance_->success_task = NewRunnableMethod(
- this, &ExtensionTtsSpeakFunction::SpeechFinished, true);
- utterance_->failure_task = NewRunnableMethod(
- this, &ExtensionTtsSpeakFunction::SpeechFinished, false);
- ExtensionTtsController::GetInstance()->SpeakOrEnqueue(
- utterance_, can_enqueue);
+ ExtensionTtsController::GetInstance()->SpeakOrEnqueue(utterance_);
return true;
}
-void ExtensionTtsSpeakFunction::SpeechFinished(bool success) {
- error_ = utterance_->error;
+void ExtensionTtsSpeakFunction::SpeechFinished() {
+ error_ = utterance_->error();
+ bool success = error_.empty();
SendResponse(success);
- Release(); // Balanced in Speak().
+ Release(); // Balanced in RunImpl().
}
bool ExtensionTtsStopSpeakingFunction::RunImpl() {
@@ -234,3 +372,15 @@ bool ExtensionTtsIsSpeakingFunction::RunImpl() {
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;
+}
diff --git a/chrome/browser/extensions/extension_tts_api.h b/chrome/browser/extensions/extension_tts_api.h
index 2e4e9bb..9f7da37 100644
--- a/chrome/browser/extensions/extension_tts_api.h
+++ b/chrome/browser/extensions/extension_tts_api.h
@@ -6,6 +6,7 @@
#define CHROME_BROWSER_EXTENSIONS_EXTENSION_TTS_API_H_
#include <queue>
+#include <string>
#include "base/singleton.h"
#include "base/task.h"
@@ -19,7 +20,7 @@ class ExtensionTtsPlatformImpl {
// Speak the given utterance with the given parameters if possible,
// and return true on success. Utterance will always be nonempty.
- // If the user does not specify the other values, language and gender
+ // If the user does not specify the other values, then locale and gender
// will be empty strings, and rate, pitch, and volume will be -1.0.
//
// The ExtensionTtsController will only try to speak one utterance at
@@ -28,7 +29,7 @@ class ExtensionTtsPlatformImpl {
// returns false before calling Speak again.
virtual bool Speak(
const std::string& utterance,
- const std::string& language,
+ const std::string& locale,
const std::string& gender,
double rate,
double pitch,
@@ -53,40 +54,102 @@ class ExtensionTtsPlatformImpl {
DISALLOW_COPY_AND_ASSIGN(ExtensionTtsPlatformImpl);
};
+// One speech utterance.
+class Utterance {
+ public:
+ // Construct an utterance given a profile, the text to speak,
+ // the options passed to tts.speak, and a completion task to call
+ // when the utterance is done speaking.
+ Utterance(Profile* profile,
+ const std::string& text,
+ DictionaryValue* options,
+ Task* completion_task);
+ ~Utterance();
+
+ // Calls the completion task and then destroys itself.
+ void FinishAndDestroy();
+
+ void set_error(const std::string& error) { error_ = error; }
+ void set_extension_id(const std::string& extension_id) {
+ extension_id_ = extension_id;
+ }
+
+ // Accessors
+ Profile* profile() { return profile_; }
+ const std::string& extension_id() { return extension_id_; }
+ int id() { return id_; }
+ const std::string& text() { return text_; }
+ const Value* options() { return options_.get(); }
+ const std::string& voice_name() { return voice_name_; }
+ const std::string& locale() { return locale_; }
+ const std::string& gender() { return gender_; }
+ double rate() { return rate_; }
+ double pitch() { return pitch_; }
+ double volume() { return volume_; }
+ bool can_enqueue() { return can_enqueue_; }
+ const std::string& error() { return error_; }
+
+ private:
+ // The profile that initiated this utterance.
+ Profile* profile_;
+
+ // The extension ID of the extension providing TTS for this utterance, or
+ // empty if native TTS is being used.
+ std::string extension_id_;
+
+ // The unique ID of this utterance, used to associate callback functions
+ // with utterances.
+ int id_;
+
+ // The id of the next utterance, so we can associate requests with
+ // responses.
+ static int next_utterance_id_;
+
+ // The text to speak.
+ std::string text_;
+
+ // The full options arg passed to tts.speak, which may include fields
+ // other than the ones we explicitly parse, below.
+ scoped_ptr<Value> options_;
+
+ // The parsed options.
+ std::string voice_name_;
+ std::string locale_;
+ std::string gender_;
+ double rate_;
+ double pitch_;
+ double volume_;
+ bool can_enqueue_;
+
+ // The error string to pass to the completion task. Will be empty if
+ // no error occurred.
+ std::string error_;
+
+ // The method to call when this utterance has completed speaking.
+ Task* completion_task_;
+};
+
// Singleton class that manages text-to-speech.
class ExtensionTtsController {
public:
// Get the single instance of this class.
static ExtensionTtsController* GetInstance();
- struct Utterance {
- Utterance();
- ~Utterance();
-
- std::string text;
- std::string language;
- std::string gender;
- double rate;
- double pitch;
- double volume;
-
- Task* success_task;
- Task* failure_task;
-
- std::string error;
- };
-
// Returns true if we're currently speaking an utterance.
bool IsSpeaking() const;
- // Speak the given utterance. If |can_enqueue| is true and another
- // utterance is in progress, adds it to the end of the queue. Otherwise,
- // interrupts any current utterance and speaks this one immediately.
- void SpeakOrEnqueue(Utterance* utterance, bool can_enqueue);
+ // Speak the given utterance. If the utterance's can_enqueue flag is true
+ // and another utterance is in progress, adds it to the end of the queue.
+ // Otherwise, interrupts any current utterance and speaks this one
+ // immediately.
+ void SpeakOrEnqueue(Utterance* utterance);
// Stop all utterances and flush the queue.
void Stop();
+ // Called when an extension finishes speaking an utterance.
+ void OnSpeechFinished(int request_id, std::string error_message);
+
// For unit testing.
void SetPlatformImpl(ExtensionTtsPlatformImpl* platform_impl);
@@ -112,6 +175,14 @@ class ExtensionTtsController {
// Finalize and delete the current utterance.
void FinishCurrentUtterance();
+ // Start speaking the next utterance in the queue.
+ void SpeakNextUtterance();
+
+ // Return the id string of the first extension with tts_voices in its
+ // manifest that matches the speech parameters of this utterance,
+ // or the empty string if none is found.
+ std::string GetMatchingExtensionId(Utterance* utterance);
+
ScopedRunnableMethodFactory<ExtensionTtsController> method_factory_;
friend struct DefaultSingletonTraits<ExtensionTtsController>;
@@ -136,8 +207,8 @@ class ExtensionTtsSpeakFunction : public AsyncExtensionFunction {
private:
~ExtensionTtsSpeakFunction() {}
virtual bool RunImpl();
- void SpeechFinished(bool success);
- ExtensionTtsController::Utterance* utterance_;
+ void SpeechFinished();
+ Utterance* utterance_;
DECLARE_EXTENSION_FUNCTION_NAME("experimental.tts.speak")
};
@@ -155,4 +226,11 @@ class ExtensionTtsIsSpeakingFunction : public SyncExtensionFunction {
DECLARE_EXTENSION_FUNCTION_NAME("experimental.tts.isSpeaking")
};
+class ExtensionTtsSpeakCompletedFunction : public SyncExtensionFunction {
+ private:
+ ~ExtensionTtsSpeakCompletedFunction() {}
+ virtual bool RunImpl();
+ DECLARE_EXTENSION_FUNCTION_NAME("experimental.tts.speakCompleted")
+};
+
#endif // CHROME_BROWSER_EXTENSIONS_EXTENSION_TTS_API_H_
diff --git a/chrome/browser/extensions/extension_tts_api_chromeos.cc b/chrome/browser/extensions/extension_tts_api_chromeos.cc
index a183612..4e6ce65 100644
--- a/chrome/browser/extensions/extension_tts_api_chromeos.cc
+++ b/chrome/browser/extensions/extension_tts_api_chromeos.cc
@@ -21,7 +21,7 @@ class ExtensionTtsPlatformImplChromeOs : public ExtensionTtsPlatformImpl {
public:
virtual bool Speak(
const std::string& utterance,
- const std::string& language,
+ const std::string& locale,
const std::string& gender,
double rate,
double pitch,
@@ -38,6 +38,10 @@ class ExtensionTtsPlatformImplChromeOs : public ExtensionTtsPlatformImpl {
ExtensionTtsPlatformImplChromeOs() {}
virtual ~ExtensionTtsPlatformImplChromeOs() {}
+ void AppendSpeakOption(std::string key,
+ std::string value,
+ std::string* options);
+
friend struct DefaultSingletonTraits<ExtensionTtsPlatformImplChromeOs>;
DISALLOW_COPY_AND_ASSIGN(ExtensionTtsPlatformImplChromeOs);
@@ -50,7 +54,7 @@ ExtensionTtsPlatformImpl* ExtensionTtsPlatformImpl::GetInstance() {
bool ExtensionTtsPlatformImplChromeOs::Speak(
const std::string& utterance,
- const std::string& language,
+ const std::string& locale,
const std::string& gender,
double rate,
double pitch,
@@ -63,31 +67,41 @@ bool ExtensionTtsPlatformImplChromeOs::Speak(
std::string options;
- if (!language.empty()) {
- util::AppendSpeakOption(
- std::string(util::kNameKey), language, &options);
+ if (!locale.empty()) {
+ AppendSpeakOption(
+ chromeos::SpeechSynthesisLibrary::kSpeechPropertyLocale,
+ locale,
+ &options);
}
if (!gender.empty()) {
- util::AppendSpeakOption(
- std::string(util::kGenderKey), gender, &options);
+ AppendSpeakOption(
+ chromeos::SpeechSynthesisLibrary::kSpeechPropertyGender,
+ gender,
+ &options);
}
if (rate >= 0.0) {
- util::AppendSpeakOption(
- std::string(util::kRateKey), DoubleToString(rate * 5), &options);
+ AppendSpeakOption(
+ chromeos::SpeechSynthesisLibrary::kSpeechPropertyRate,
+ DoubleToString(rate * 5),
+ &options);
}
if (pitch >= 0.0) {
// The TTS service allows a range of 0 to 2 for speech pitch.
- util::AppendSpeakOption(
- std::string(util::kPitchKey), DoubleToString(pitch * 2), &options);
+ AppendSpeakOption(
+ chromeos::SpeechSynthesisLibrary::kSpeechPropertyPitch,
+ DoubleToString(pitch * 2),
+ &options);
}
if (volume >= 0.0) {
// The TTS service allows a range of 0 to 5 for speech volume.
- util::AppendSpeakOption(
- std::string(util::kVolumeKey), DoubleToString(volume * 5), &options);
+ AppendSpeakOption(
+ chromeos::SpeechSynthesisLibrary::kSpeechPropertyVolume,
+ DoubleToString(volume * 5),
+ &options);
}
if (!options.empty()) {
@@ -118,6 +132,17 @@ bool ExtensionTtsPlatformImplChromeOs::IsSpeaking() {
return false;
}
+void ExtensionTtsPlatformImplChromeOs::AppendSpeakOption(
+ std::string key,
+ std::string value,
+ std::string* options) {
+ *options +=
+ key +
+ chromeos::SpeechSynthesisLibrary::kSpeechPropertyEquals +
+ value +
+ chromeos::SpeechSynthesisLibrary::kSpeechPropertyDelimiter;
+}
+
// static
ExtensionTtsPlatformImplChromeOs*
ExtensionTtsPlatformImplChromeOs::GetInstance() {
diff --git a/chrome/browser/extensions/extension_tts_api_util.cc b/chrome/browser/extensions/extension_tts_api_util.cc
index ac5d53b..b3ef18c 100644
--- a/chrome/browser/extensions/extension_tts_api_util.cc
+++ b/chrome/browser/extensions/extension_tts_api_util.cc
@@ -6,6 +6,14 @@
namespace extension_tts_api_util {
+const char kVoiceNameKey[] = "voiceName";
+const char kLocaleKey[] = "locale";
+const char kGenderKey[] = "gender";
+const char kRateKey[] = "rate";
+const char kPitchKey[] = "pitch";
+const char kVolumeKey[] = "volume";
+const char kEnqueueKey[] = "enqueue";
+
// Static.
bool ReadNumberByKey(DictionaryValue* dict,
const char* key,
@@ -28,11 +36,4 @@ bool ReadNumberByKey(DictionaryValue* dict,
return true;
}
-// Static.
-void AppendSpeakOption(std::string key,
- std::string value,
- std::string* options) {
- *options += key + kEqualStr + value + kDelimiter;
-}
-
} // namespace extension_tts_api_util.
diff --git a/chrome/browser/extensions/extension_tts_api_util.h b/chrome/browser/extensions/extension_tts_api_util.h
index cf0d702..9d13e75 100644
--- a/chrome/browser/extensions/extension_tts_api_util.h
+++ b/chrome/browser/extensions/extension_tts_api_util.h
@@ -11,23 +11,17 @@
namespace extension_tts_api_util {
-const char kNameKey[] = "name";
-const char kLanguageNameKey[] = "languageName";
-const char kGenderKey[] = "gender";
-const char kRateKey[] = "rate";
-const char kPitchKey[] = "pitch";
-const char kVolumeKey[] = "volume";
-const char kEnqueueKey[] = "enqueue";
-const char kEqualStr[] = "=";
-const char kDelimiter[] = ";";
+extern const char kVoiceNameKey[];
+extern const char kLocaleKey[];
+extern const char kGenderKey[];
+extern const char kRateKey[];
+extern const char kPitchKey[];
+extern const char kVolumeKey[];
+extern const char kEnqueueKey[];
bool ReadNumberByKey(DictionaryValue* dict,
const char* key,
double* ret_value);
-void AppendSpeakOption(std::string key,
- std::string value,
- std::string* options);
-
} // namespace extension_tts_api_util.
#endif // CHROME_BROWSER_EXTENSIONS_EXTENSION_TTS_API_UTIL_H_
diff --git a/chrome/browser/extensions/extension_tts_apitest.cc b/chrome/browser/extensions/extension_tts_apitest.cc
index 6f37404..f7ed0d8 100644
--- a/chrome/browser/extensions/extension_tts_apitest.cc
+++ b/chrome/browser/extensions/extension_tts_apitest.cc
@@ -16,6 +16,7 @@
#include "chrome/browser/chromeos/cros/cros_mock.h"
#endif
+using ::testing::AnyNumber;
using ::testing::CreateFunctor;
using ::testing::DoAll;
using ::testing::InSequence;
@@ -28,7 +29,7 @@ class MockExtensionTtsPlatformImpl : public ExtensionTtsPlatformImpl {
public:
MOCK_METHOD6(Speak,
bool(const std::string& utterance,
- const std::string& language,
+ const std::string& locale,
const std::string& gender,
double rate,
double pitch,
@@ -83,11 +84,20 @@ IN_PROC_BROWSER_TEST_F(TtsApiTest, PlatformSpeakKeepsSpeakingTwice) {
}
IN_PROC_BROWSER_TEST_F(TtsApiTest, PlatformSpeakInterrupt) {
+ // One utterances starts speaking, and then a second interrupts.
InSequence s;
EXPECT_CALL(mock_platform_impl_, StopSpeaking())
.WillOnce(Return(true));
EXPECT_CALL(mock_platform_impl_, Speak("text 1", _, _, _, _, _))
.WillOnce(Return(true));
+
+ // Ensure that the first utterance keeps going until it's interrupted.
+ EXPECT_CALL(mock_platform_impl_, IsSpeaking())
+ .Times(AnyNumber())
+ .WillRepeatedly(Return(true));
+
+ // Expect the second utterance and allow it to continue for two calls to
+ // IsSpeaking and then finish successfully.
EXPECT_CALL(mock_platform_impl_, StopSpeaking())
.WillOnce(Return(true));
EXPECT_CALL(mock_platform_impl_, Speak("text 2", _, _, _, _, _))
@@ -107,6 +117,14 @@ IN_PROC_BROWSER_TEST_F(TtsApiTest, PlatformSpeakQueueInterrupt) {
.WillOnce(Return(true));
EXPECT_CALL(mock_platform_impl_, Speak("text 1", _, _, _, _, _))
.WillOnce(Return(true));
+
+ // Ensure that the first utterance keeps going until it's interrupted.
+ EXPECT_CALL(mock_platform_impl_, IsSpeaking())
+ .Times(AnyNumber())
+ .WillRepeatedly(Return(true));
+
+ // Expect the third utterance and allow it to continue for two calls to
+ // IsSpeaking and then finish successfully.
EXPECT_CALL(mock_platform_impl_, StopSpeaking())
.WillOnce(Return(true));
EXPECT_CALL(mock_platform_impl_, Speak("text 3", _, _, _, _, _))
@@ -145,12 +163,16 @@ IN_PROC_BROWSER_TEST_F(TtsApiTest, PlatformSpeakError) {
.WillOnce(Return(true));
EXPECT_CALL(mock_platform_impl_, IsSpeaking())
.WillOnce(Return(false));
+ EXPECT_CALL(mock_platform_impl_, StopSpeaking())
+ .WillOnce(Return(true));
EXPECT_CALL(mock_platform_impl_, Speak(_, _, _, _, _, _))
.WillOnce(DoAll(
InvokeWithoutArgs(
CreateFunctor(&mock_platform_impl_,
&MockExtensionTtsPlatformImpl::SetErrorToEpicFail)),
Return(false)));
+ EXPECT_CALL(mock_platform_impl_, StopSpeaking())
+ .WillOnce(Return(true));
EXPECT_CALL(mock_platform_impl_, Speak(_, _, _, _, _, _))
.WillOnce(Return(true));
EXPECT_CALL(mock_platform_impl_, IsSpeaking())
@@ -158,6 +180,25 @@ IN_PROC_BROWSER_TEST_F(TtsApiTest, PlatformSpeakError) {
ASSERT_TRUE(RunExtensionTest("tts/speak_error")) << message_;
}
+IN_PROC_BROWSER_TEST_F(TtsApiTest, Provide) {
+ EXPECT_CALL(mock_platform_impl_, StopSpeaking())
+ .WillRepeatedly(Return(true));
+ EXPECT_CALL(mock_platform_impl_, IsSpeaking())
+ .WillRepeatedly(Return(false));
+
+ {
+ InSequence s;
+ EXPECT_CALL(mock_platform_impl_, Speak("native speech", _, _, _, _, _))
+ .WillOnce(Return(true));
+ EXPECT_CALL(mock_platform_impl_, Speak("native speech 2", _, _, _, _, _))
+ .WillOnce(Return(true));
+ EXPECT_CALL(mock_platform_impl_, Speak("native speech 3", _, _, _, _, _))
+ .WillOnce(Return(true));
+ }
+
+ ASSERT_TRUE(RunExtensionTest("tts/provide")) << message_;
+}
+
#if defined(OS_CHROMEOS)
IN_PROC_BROWSER_TEST_F(ExtensionApiTest, TtsChromeOs) {
CommandLine::ForCurrentProcess()->AppendSwitch(
diff --git a/chrome/common/extensions/api/extension_api.json b/chrome/common/extensions/api/extension_api.json
index bda4b49..8dac7c4 100644
--- a/chrome/common/extensions/api/extension_api.json
+++ b/chrome/common/extensions/api/extension_api.json
@@ -517,10 +517,15 @@
"optional": true,
"description": "If true, enqueues this utterance if TTS is already in progress. If false (the default), interrupts any current speech and flushes the speech queue before speaking this new utterance."
},
- "languageName": {
+ "voiceName": {
"type": "string",
"optional": true,
- "description": "The language name for synthesis specified in the form <language>-<locale>, e.g. en-US, en-GB, fr-CA, zh-CN, etc."
+ "description": "The name of the voice to use for synthesis."
+ },
+ "locale": {
+ "type": "string",
+ "optional": true,
+ "description": "The language and region code that specify the language and dialect to be used for synthesis, in the form <language>-<region>, e.g. en-US, en-GB, fr-CA, zh-CN, etc."
},
"gender": {
"type": "string",
@@ -584,6 +589,99 @@
]
}
]
+ },
+ {
+ "name": "speakCompleted",
+ "nodoc": true,
+ "type": "function",
+ "description": "A callback passed to the onSpeak event.",
+ "parameters": [
+ {
+ "type": "integer",
+ "name": "requestId"
+ },
+ {
+ "type": "string",
+ "optional": "true",
+ "name": "errorMessage"
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "onSpeak",
+ "type": "function",
+ "description": "Called when the user makes a call to tts.speak and the options matches one of the tts_voices from this extension's manifest.",
+ "parameters": [
+ {
+ "type": "string",
+ "name": "utterance",
+ "description": "The text to speak."
+ },
+ {
+ "type": "object",
+ "name": "options",
+ "description": "The speak options.",
+ "properties": {
+ "voiceName": {
+ "type": "string",
+ "optional": true,
+ "description": "The name of the voice to use for synthesis."
+ },
+ "locale": {
+ "type": "string",
+ "optional": true,
+ "description": "The language and region code that specify the language and dialect to be used for synthesis, in the form <language>-<region>, e.g. en-US, en-GB, fr-CA, zh-CN, etc."
+ },
+ "gender": {
+ "type": "string",
+ "optional": true,
+ "description": "Gender of voice for synthesized speech.",
+ "enum": ["male", "female"]
+ },
+ "rate": {
+ "type": "number",
+ "optional": true,
+ "minimum": 0,
+ "maximum": 1,
+ "description": "Speaking speed between 0 and 1 inclusive, with 0 being slowest and 1 being fastest."
+ },
+ "pitch": {
+ "type": "number",
+ "optional": true,
+ "minimum": 0,
+ "maximum": 1,
+ "description": "Speaking pitch between 0 and 1 inclusive, with 0 being lowest and 1 being highest."
+ },
+ "volume": {
+ "type": "number",
+ "optional": true,
+ "minimum": 0,
+ "maximum": 1,
+ "description": "Speaking volume between 0 and 1 inclusive, with 0 being lowest and 1 being highest."
+ }
+ }
+ },
+ {
+ "type": "function",
+ "name": "callback",
+ "description": "You must call this function when speaking is finished.",
+ "parameters": [
+ {
+ "type": "string",
+ "name": "error",
+ "optional": true,
+ "description": "Error message, which will be returned to the caller in chrome.extension.lastError."
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "onStop",
+ "type": "function",
+ "description": "Fired when a call is made to tts.stop and this extension may be in the middle of speaking."
}
]
},
diff --git a/chrome/common/extensions/extension.cc b/chrome/common/extensions/extension.cc
index f365ac6..fd4ee4c 100644
--- a/chrome/common/extensions/extension.cc
+++ b/chrome/common/extensions/extension.cc
@@ -1789,9 +1789,8 @@ bool Extension::InitFromValue(const DictionaryValue& source, bool require_key,
}
if (source.HasKey(keys::kDefaultLocale)) {
- if (!source.GetString(keys::kDefaultLocale,
- &default_locale_) ||
- default_locale_.empty()) {
+ if (!source.GetString(keys::kDefaultLocale, &default_locale_) ||
+ !l10n_util::IsValidLocaleSyntax(default_locale_)) {
*error = errors::kInvalidDefaultLocale;
return false;
}
@@ -1854,6 +1853,59 @@ bool Extension::InitFromValue(const DictionaryValue& source, bool require_key,
devtools_url_ = GetResourceURL(devtools_str);
}
+ // Initialize text-to-speech voices (optional).
+ if (source.HasKey(keys::kTts)) {
+ DictionaryValue* tts_dict;
+ if (!source.GetDictionary(keys::kTts, &tts_dict)) {
+ *error = errors::kInvalidTts;
+ return false;
+ }
+
+ if (tts_dict->HasKey(keys::kTtsVoices)) {
+ ListValue* tts_voices;
+ if (!tts_dict->GetList(keys::kTtsVoices, &tts_voices)) {
+ *error = errors::kInvalidTtsVoices;
+ return false;
+ }
+
+ for (size_t i = 0; i < tts_voices->GetSize(); i++) {
+ DictionaryValue* one_tts_voice;
+ if (!tts_voices->GetDictionary(i, &one_tts_voice)) {
+ *error = errors::kInvalidTtsVoices;
+ return false;
+ }
+
+ TtsVoice voice_data;
+ if (one_tts_voice->HasKey(keys::kTtsVoicesVoiceName)) {
+ if (!one_tts_voice->GetString(
+ keys::kTtsVoicesVoiceName, &voice_data.voice_name)) {
+ *error = errors::kInvalidTtsVoicesVoiceName;
+ return false;
+ }
+ }
+ if (one_tts_voice->HasKey(keys::kTtsVoicesLocale)) {
+ if (!one_tts_voice->GetString(
+ keys::kTtsVoicesLocale, &voice_data.locale) ||
+ !l10n_util::IsValidLocaleSyntax(voice_data.locale)) {
+ *error = errors::kInvalidTtsVoicesLocale;
+ return false;
+ }
+ }
+ if (one_tts_voice->HasKey(keys::kTtsVoicesGender)) {
+ if (!one_tts_voice->GetString(
+ keys::kTtsVoicesGender, &voice_data.gender) ||
+ (voice_data.gender != keys::kTtsGenderMale &&
+ voice_data.gender != keys::kTtsGenderFemale)) {
+ *error = errors::kInvalidTtsVoicesGender;
+ return false;
+ }
+ }
+
+ tts_voices_.push_back(voice_data);
+ }
+ }
+ }
+
// Initialize incognito behavior. Apps default to split mode, extensions
// default to spanning.
incognito_split_mode_ = is_app();
diff --git a/chrome/common/extensions/extension.h b/chrome/common/extensions/extension.h
index 4d680ef..a4cf829 100644
--- a/chrome/common/extensions/extension.h
+++ b/chrome/common/extensions/extension.h
@@ -100,6 +100,12 @@ class Extension : public base::RefCountedThreadSafe<Extension> {
bool is_public; // False if only this extension can load this plugin.
};
+ struct TtsVoice {
+ std::string voice_name;
+ std::string locale;
+ std::string gender;
+ };
+
// A permission is defined by its |name| (what is used in the manifest),
// and the |message_id| that's used by install/update UI.
struct Permission {
@@ -428,6 +434,7 @@ class Extension : public base::RefCountedThreadSafe<Extension> {
}
const std::string omnibox_keyword() const { return omnibox_keyword_; }
bool incognito_split_mode() const { return incognito_split_mode_; }
+ const std::vector<TtsVoice>& tts_voices() const { return tts_voices_; }
// App-related.
bool is_app() const { return is_app_; }
@@ -691,6 +698,9 @@ class Extension : public base::RefCountedThreadSafe<Extension> {
// The Omnibox keyword for this extension, or empty if there is none.
std::string omnibox_keyword_;
+ // List of text-to-speech voices that this extension provides, if any.
+ std::vector<TtsVoice> tts_voices_;
+
FRIEND_TEST_ALL_PREFIXES(ExtensionServiceTest,
UpdateExtensionPreservesLocation);
FRIEND_TEST_ALL_PREFIXES(ExtensionTest, LoadPageActionHelper);
diff --git a/chrome/common/extensions/extension_constants.cc b/chrome/common/extensions/extension_constants.cc
index 5e00d35..dd15afa 100644
--- a/chrome/common/extensions/extension_constants.cc
+++ b/chrome/common/extensions/extension_constants.cc
@@ -60,6 +60,13 @@ const char* kThemeImages = "images";
const char* kThemeTints = "tints";
const char* kToolstripPath = "path";
const char* kToolstrips = "toolstrips";
+const char* kTts = "tts";
+const char* kTtsGenderFemale = "female";
+const char* kTtsGenderMale = "male";
+const char* kTtsVoices = "voices";
+const char* kTtsVoicesGender = "gender";
+const char* kTtsVoicesLocale = "locale";
+const char* kTtsVoicesVoiceName = "voiceName";
const char* kType = "type";
const char* kUpdateURL = "update_url";
const char* kVersion = "version";
@@ -234,6 +241,16 @@ const char* kInvalidToolstrip =
"Invalid value for 'toolstrips[*]'";
const char* kInvalidToolstrips =
"Invalid value for 'toolstrips'.";
+const char* kInvalidTts =
+ "Invalid value for 'tts'.";
+const char* kInvalidTtsVoices =
+ "Invalid value for 'tts.voices'.";
+const char* kInvalidTtsVoicesGender =
+ "Invalid value for 'tts.voices[*].gender'.";
+const char* kInvalidTtsVoicesLocale =
+ "Invalid value for 'tts.voices[*].locale'.";
+const char* kInvalidTtsVoicesVoiceName =
+ "Invalid value for 'tts.voices[*].voiceName'.";
const char* kInvalidUpdateURL =
"Invalid value for update url: '[*]'.";
const char* kInvalidVersion =
diff --git a/chrome/common/extensions/extension_constants.h b/chrome/common/extensions/extension_constants.h
index 220cbb0..7f09630 100644
--- a/chrome/common/extensions/extension_constants.h
+++ b/chrome/common/extensions/extension_constants.h
@@ -65,6 +65,13 @@ namespace extension_manifest_keys {
extern const char* kThemeTints;
extern const char* kToolstripPath;
extern const char* kToolstrips;
+ extern const char* kTts;
+ extern const char* kTtsGenderFemale;
+ extern const char* kTtsGenderMale;
+ extern const char* kTtsVoices;
+ extern const char* kTtsVoicesGender;
+ extern const char* kTtsVoicesLocale;
+ extern const char* kTtsVoicesVoiceName;
extern const char* kType;
extern const char* kUpdateURL;
extern const char* kVersion;
@@ -161,6 +168,11 @@ namespace extension_manifest_errors {
extern const char* kInvalidThemeTints;
extern const char* kInvalidToolstrip;
extern const char* kInvalidToolstrips;
+ extern const char* kInvalidTts;
+ extern const char* kInvalidTtsVoices;
+ extern const char* kInvalidTtsVoicesGender;
+ extern const char* kInvalidTtsVoicesLocale;
+ extern const char* kInvalidTtsVoicesVoiceName;
extern const char* kInvalidUpdateURL;
extern const char* kInvalidVersion;
extern const char* kInvalidWebURL;
diff --git a/chrome/common/extensions/extension_manifests_unittest.cc b/chrome/common/extensions/extension_manifests_unittest.cc
index 9d653fe..f7c8f27 100644
--- a/chrome/common/extensions/extension_manifests_unittest.cc
+++ b/chrome/common/extensions/extension_manifests_unittest.cc
@@ -374,3 +374,37 @@ TEST_F(ExtensionManifestTest, DefaultPathForExtent) {
EXPECT_TRUE(extension->web_extent().ContainsURL(
GURL("http://www.google.com/monkey")));
}
+
+TEST_F(ExtensionManifestTest, DefaultLocale) {
+ LoadAndExpectError("default_locale_invalid.json",
+ extension_manifest_errors::kInvalidDefaultLocale);
+
+ scoped_refptr<Extension> extension(
+ LoadAndExpectSuccess("default_locale_valid.json"));
+ EXPECT_EQ("de-AT", extension->default_locale());
+}
+
+TEST_F(ExtensionManifestTest, TtsProvider) {
+ LoadAndExpectError("tts_provider_invalid_1.json",
+ extension_manifest_errors::kInvalidTts);
+ LoadAndExpectError("tts_provider_invalid_2.json",
+ extension_manifest_errors::kInvalidTtsVoices);
+ LoadAndExpectError("tts_provider_invalid_3.json",
+ extension_manifest_errors::kInvalidTtsVoices);
+ LoadAndExpectError("tts_provider_invalid_4.json",
+ extension_manifest_errors::kInvalidTtsVoicesVoiceName);
+ LoadAndExpectError("tts_provider_invalid_5.json",
+ extension_manifest_errors::kInvalidTtsVoicesLocale);
+ LoadAndExpectError("tts_provider_invalid_6.json",
+ extension_manifest_errors::kInvalidTtsVoicesLocale);
+ LoadAndExpectError("tts_provider_invalid_7.json",
+ extension_manifest_errors::kInvalidTtsVoicesGender);
+
+ scoped_refptr<Extension> extension(
+ LoadAndExpectSuccess("tts_provider_valid.json"));
+
+ ASSERT_EQ(1u, extension->tts_voices().size());
+ EXPECT_EQ("name", extension->tts_voices()[0].voice_name);
+ EXPECT_EQ("en-US", extension->tts_voices()[0].locale);
+ EXPECT_EQ("female", extension->tts_voices()[0].gender);
+}
diff --git a/chrome/renderer/resources/extension_process_bindings.js b/chrome/renderer/resources/extension_process_bindings.js
index 53975f8..43b6617 100644
--- a/chrome/renderer/resources/extension_process_bindings.js
+++ b/chrome/renderer/resources/extension_process_bindings.js
@@ -343,7 +343,7 @@ var chrome = chrome || {};
return result;
}
- function setupOmniboxEvents(extensionId) {
+ function setupOmniboxEvents() {
chrome.omnibox.onInputChanged.dispatch =
function(text, requestId) {
var suggestCallback = function(suggestions) {
@@ -353,6 +353,19 @@ var chrome = chrome || {};
};
}
+ function setupTtsEvents() {
+ chrome.experimental.tts.onSpeak.dispatch =
+ function(text, options, requestId) {
+ var callback = function(errorMessage) {
+ if (errorMessage)
+ chrome.experimental.tts.speakCompleted(requestId, errorMessage);
+ else
+ chrome.experimental.tts.speakCompleted(requestId);
+ };
+ chrome.Event.prototype.dispatch.apply(this, [text, options, callback]);
+ };
+ }
+
chromeHidden.onLoad.addListener(function (extensionId) {
if (!extensionId) {
return;
@@ -783,7 +796,8 @@ var chrome = chrome || {};
setupToolstripEvents(GetRenderViewId());
setupPopupEvents(GetRenderViewId());
setupHiddenContextMenuEvent(extensionId);
- setupOmniboxEvents(extensionId);
+ setupOmniboxEvents();
+ setupTtsEvents();
});
if (!chrome.experimental)
diff --git a/chrome/test/data/extensions/api_test/tts/chromeos/test.js b/chrome/test/data/extensions/api_test/tts/chromeos/test.js
index 9db8d96..ece6f97 100644
--- a/chrome/test/data/extensions/api_test/tts/chromeos/test.js
+++ b/chrome/test/data/extensions/api_test/tts/chromeos/test.js
@@ -9,13 +9,15 @@ chrome.test.runTests([
function testChromeOsSpeech() {
var callbacks = 0;
chrome.experimental.tts.speak('text 1', {}, function() {
- chrome.test.assertNoLastError();
+ chrome.test.assertEq('Utterance interrupted.',
+ chrome.extension.lastError.message);
callbacks++;
});
chrome.experimental.tts.speak('text 2', {}, function() {
chrome.test.assertNoLastError();
callbacks++;
if (callbacks == 2) {
+ chrome.test.assertNoLastError();
chrome.test.succeed();
} else {
chrome.test.fail();
diff --git a/chrome/test/data/extensions/api_test/tts/interrupt/test.js b/chrome/test/data/extensions/api_test/tts/interrupt/test.js
index 51759b2..390d68f 100644
--- a/chrome/test/data/extensions/api_test/tts/interrupt/test.js
+++ b/chrome/test/data/extensions/api_test/tts/interrupt/test.js
@@ -9,7 +9,8 @@ chrome.test.runTests([
function testAllSpeakCallbackFunctionsAreCalled() {
var callbacks = 0;
chrome.experimental.tts.speak('text 1', {'enqueue': false}, function() {
- chrome.test.assertNoLastError();
+ chrome.test.assertEq('Utterance interrupted.',
+ chrome.extension.lastError.message);
callbacks++;
});
chrome.experimental.tts.speak('text 2', {'enqueue': false}, function() {
diff --git a/chrome/test/data/extensions/api_test/tts/provide/manifest.json b/chrome/test/data/extensions/api_test/tts/provide/manifest.json
new file mode 100644
index 0000000..93bc98d
--- /dev/null
+++ b/chrome/test/data/extensions/api_test/tts/provide/manifest.json
@@ -0,0 +1,20 @@
+{
+ "name": "chrome.experimental.tts",
+ "version": "0.1",
+ "description": "browser test for chrome.experimental.tts API",
+ "background_page": "test.html",
+ "tts": {
+ "voices": [
+ {
+ "voiceName": "Alice",
+ "locale": "en-US",
+ "gender": "female"
+ },
+ {
+ "voiceName": "Pat",
+ "locale": "en-US"
+ }
+ ]
+ },
+ "permissions": ["experimental"]
+}
diff --git a/chrome/test/data/extensions/api_test/tts/provide/test.html b/chrome/test/data/extensions/api_test/tts/provide/test.html
new file mode 100644
index 0000000..46f4d74
--- /dev/null
+++ b/chrome/test/data/extensions/api_test/tts/provide/test.html
@@ -0,0 +1 @@
+<script src="test.js"></script>
diff --git a/chrome/test/data/extensions/api_test/tts/provide/test.js b/chrome/test/data/extensions/api_test/tts/provide/test.js
new file mode 100644
index 0000000..bc5e16a
--- /dev/null
+++ b/chrome/test/data/extensions/api_test/tts/provide/test.js
@@ -0,0 +1,113 @@
+// Copyright (c) 2010 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.
+
+// TTS api test for Chrome on ChromeOS.
+// browser_tests.exe --gtest_filter="TtsApiTest.*"
+
+if (!chrome.tts) {
+ chrome.tts = chrome.experimental.tts;
+}
+
+chrome.test.runTests([
+ function testNoListeners() {
+ // This call should go to native speech because we haven't registered
+ // any listeners.
+ chrome.tts.speak('native speech', {}, function() {
+ chrome.test.assertNoLastError();
+ chrome.test.succeed();
+ });
+ },
+ function testTtsProvider() {
+ // Register listeners for speech functions, enabling this extension
+ // to be a TTS provider.
+ var speakListener = function(utterance, options, callback) {
+ chrome.test.assertNoLastError();
+ chrome.test.assertEq('extension speech', utterance);
+ callback();
+ };
+ var stopListener = function() {};
+ chrome.tts.onSpeak.addListener(speakListener);
+ chrome.tts.onStop.addListener(stopListener);
+
+ // This call should go to our own speech provider.
+ chrome.tts.speak('extension speech', {}, function() {
+ chrome.test.assertNoLastError();
+ chrome.tts.onSpeak.removeListener(speakListener);
+ chrome.tts.onStop.removeListener(stopListener);
+ chrome.test.succeed();
+ });
+ },
+ function testVoiceMatching() {
+ // Count the number of times our callback functions have been called.
+ var callbacks = 0;
+ // Count the number of times our TTS provider has been called.
+ var speakListenerCalls = 0;
+
+ // Register listeners for speech functions.
+ var speakListener = function(utterance, options, callback) {
+ speakListenerCalls++;
+ callback();
+ };
+ var stopListener = function() {};
+ chrome.tts.onSpeak.addListener(speakListener);
+ chrome.tts.onStop.addListener(stopListener);
+
+ // These don't match the voices in the manifest, so they should
+ // go to native speech. The gmock assertions in TtsApiTest::Provide
+ // enforce that the native TTS handlers are called.
+ chrome.tts.speak('native speech 2',
+ {'voiceName': 'George', 'enqueue': true}, function() {
+ chrome.test.assertNoLastError();
+ callbacks++;
+ });
+ chrome.tts.speak('native speech 3',
+ {'locale': 'fr-FR', 'enqueue': true},
+ function() {
+ chrome.test.assertNoLastError();
+ callbacks++;
+ });
+
+ // These do match the voices in the manifest, so they should go to our
+ // own TTS provider.
+ chrome.tts.speak('extension speech 2',
+ {'voiceName': 'Alice', 'enqueue': true},
+ function() {
+ chrome.test.assertNoLastError();
+ callbacks++;
+ chrome.test.succeed();
+ });
+ chrome.tts.speak('extension speech 3',
+ {'voiceName': 'Pat', 'gender': 'male', 'enqueue': true},
+ function() {
+ chrome.test.assertNoLastError();
+ callbacks++;
+ chrome.tts.onSpeak.removeListener(speakListener);
+ chrome.tts.onStop.removeListener(stopListener);
+ if (callbacks == 4 && speakListenerCalls == 2) {
+ chrome.test.succeed();
+ }
+ });
+ },
+ function testTtsProviderError() {
+ // Register listeners for speech functions, but have speak return an
+ // error when it's used.
+ var speakListener = function(utterance, options, callback) {
+ chrome.test.assertEq('extension speech 4', utterance);
+ callback('extension tts error');
+ };
+ var stopListener = function() {};
+ chrome.tts.onSpeak.addListener(speakListener);
+ chrome.tts.onStop.addListener(stopListener);
+
+ // This should go to our own TTS provider, and we can check that we
+ // get the error message.
+ chrome.tts.speak('extension speech 4', {}, function() {
+ chrome.test.assertEq('extension tts error',
+ chrome.extension.lastError.message);
+ chrome.tts.onSpeak.removeListener(speakListener);
+ chrome.tts.onStop.removeListener(stopListener);
+ chrome.test.succeed();
+ });
+ }
+]);
diff --git a/chrome/test/data/extensions/api_test/tts/queue_interrupt/test.js b/chrome/test/data/extensions/api_test/tts/queue_interrupt/test.js
index c8137cf..e2317b2 100644
--- a/chrome/test/data/extensions/api_test/tts/queue_interrupt/test.js
+++ b/chrome/test/data/extensions/api_test/tts/queue_interrupt/test.js
@@ -7,13 +7,19 @@
chrome.test.runTests([
function testAllSpeakCallbackFunctionsAreCalled() {
+ // In this test, two utterances are queued, and then a third
+ // interrupts. The first gets interrupted, the second never gets spoken
+ // at all. The test expectations in extension_tts_apitest.cc ensure that
+ // the first call to tts.speak keeps going until it's interrupted.
var callbacks = 0;
chrome.experimental.tts.speak('text 1', {'enqueue': true}, function() {
- chrome.test.assertNoLastError();
+ chrome.test.assertEq('Utterance interrupted.',
+ chrome.extension.lastError.message);
callbacks++;
});
chrome.experimental.tts.speak('text 2', {'enqueue': true}, function() {
- chrome.test.assertNoLastError();
+ chrome.test.assertEq('Utterance removed from queue.',
+ chrome.extension.lastError.message);
callbacks++;
});
chrome.experimental.tts.speak('text 3', {'enqueue': false}, function() {
diff --git a/chrome/test/data/extensions/manifest_tests/default_locale_invalid.json b/chrome/test/data/extensions/manifest_tests/default_locale_invalid.json
new file mode 100644
index 0000000..b97077a
--- /dev/null
+++ b/chrome/test/data/extensions/manifest_tests/default_locale_invalid.json
@@ -0,0 +1,5 @@
+{
+ "name": "test",
+ "version": "1",
+ "default_locale": "German"
+}
diff --git a/chrome/test/data/extensions/manifest_tests/default_locale_valid.json b/chrome/test/data/extensions/manifest_tests/default_locale_valid.json
new file mode 100644
index 0000000..f3680e48
--- /dev/null
+++ b/chrome/test/data/extensions/manifest_tests/default_locale_valid.json
@@ -0,0 +1,5 @@
+{
+ "name": "test",
+ "version": "1",
+ "default_locale": "de-AT"
+}
diff --git a/chrome/test/data/extensions/manifest_tests/tts_provider_invalid_1.json b/chrome/test/data/extensions/manifest_tests/tts_provider_invalid_1.json
new file mode 100644
index 0000000..90e849e
--- /dev/null
+++ b/chrome/test/data/extensions/manifest_tests/tts_provider_invalid_1.json
@@ -0,0 +1,5 @@
+{
+ "name": "test",
+ "version": "1",
+ "tts": "shouldBeADict"
+}
diff --git a/chrome/test/data/extensions/manifest_tests/tts_provider_invalid_2.json b/chrome/test/data/extensions/manifest_tests/tts_provider_invalid_2.json
new file mode 100644
index 0000000..261f5d3
--- /dev/null
+++ b/chrome/test/data/extensions/manifest_tests/tts_provider_invalid_2.json
@@ -0,0 +1,7 @@
+{
+ "name": "test",
+ "version": "1",
+ "tts": {
+ "voices": "shouldBeAnArray"
+ }
+}
diff --git a/chrome/test/data/extensions/manifest_tests/tts_provider_invalid_3.json b/chrome/test/data/extensions/manifest_tests/tts_provider_invalid_3.json
new file mode 100644
index 0000000..b7443df
--- /dev/null
+++ b/chrome/test/data/extensions/manifest_tests/tts_provider_invalid_3.json
@@ -0,0 +1,9 @@
+{
+ "name": "test",
+ "version": "1",
+ "tts": {
+ "voices": [
+ "shouldBeADict"
+ ]
+ }
+}
diff --git a/chrome/test/data/extensions/manifest_tests/tts_provider_invalid_4.json b/chrome/test/data/extensions/manifest_tests/tts_provider_invalid_4.json
new file mode 100644
index 0000000..da41c61
--- /dev/null
+++ b/chrome/test/data/extensions/manifest_tests/tts_provider_invalid_4.json
@@ -0,0 +1,11 @@
+{
+ "name": "test",
+ "version": "1",
+ "tts": {
+ "voices": [
+ {
+ "voiceName": [ "Shouldn't be in an array" ]
+ }
+ ]
+ }
+}
diff --git a/chrome/test/data/extensions/manifest_tests/tts_provider_invalid_5.json b/chrome/test/data/extensions/manifest_tests/tts_provider_invalid_5.json
new file mode 100644
index 0000000..6297ab1
--- /dev/null
+++ b/chrome/test/data/extensions/manifest_tests/tts_provider_invalid_5.json
@@ -0,0 +1,11 @@
+{
+ "name": "test",
+ "version": "1",
+ "tts": {
+ "voices": [
+ {
+ "locale": ""
+ }
+ ]
+ }
+}
diff --git a/chrome/test/data/extensions/manifest_tests/tts_provider_invalid_6.json b/chrome/test/data/extensions/manifest_tests/tts_provider_invalid_6.json
new file mode 100644
index 0000000..ff45401
--- /dev/null
+++ b/chrome/test/data/extensions/manifest_tests/tts_provider_invalid_6.json
@@ -0,0 +1,11 @@
+{
+ "name": "test",
+ "version": "1",
+ "tts": {
+ "voices": [
+ {
+ "locale": "German"
+ }
+ ]
+ }
+}
diff --git a/chrome/test/data/extensions/manifest_tests/tts_provider_invalid_7.json b/chrome/test/data/extensions/manifest_tests/tts_provider_invalid_7.json
new file mode 100644
index 0000000..cba13d6
--- /dev/null
+++ b/chrome/test/data/extensions/manifest_tests/tts_provider_invalid_7.json
@@ -0,0 +1,11 @@
+{
+ "name": "test",
+ "version": "1",
+ "tts": {
+ "voices": [
+ {
+ "gender": "alien"
+ }
+ ]
+ }
+}
diff --git a/chrome/test/data/extensions/manifest_tests/tts_provider_valid.json b/chrome/test/data/extensions/manifest_tests/tts_provider_valid.json
new file mode 100644
index 0000000..8e15ad1
--- /dev/null
+++ b/chrome/test/data/extensions/manifest_tests/tts_provider_valid.json
@@ -0,0 +1,14 @@
+{
+ "name": "test",
+ "version": "1",
+ "tts": {
+ "voices": [
+ {
+ "voiceName": "name",
+ "locale": "en-US",
+ "gender": "female",
+ "other_key": "other_value"
+ }
+ ]
+ }
+}