diff options
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" + } + ] + } +} |